namespace Storm.CodeCompletion
{
// TODO: make the user able to show lists of some items.
// For example have an enum where the user can indicate
// whether he/she wants to use the full list or a custom
// list, and if the user wants to use a custom lists,
// load it from a set property and display it instead.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Storm;
using Storm.CodeCompletion;
using Storm.TextEditor;
using Storm.TextEditor.ContentInfo;
using Storm.TextEditor.Controls;
using Storm.TextEditor.Controls.Core;
using Storm.TextEditor.Controls.Core.Globalization;
using Storm.TextEditor.Controls.Core.Timers;
using Storm.TextEditor.Controls.IntelliMouse;
using Storm.TextEditor.Document;
using Storm.TextEditor.Document.Exporters;
using Storm.TextEditor.Forms;
using Storm.TextEditor.Interacting;
using Storm.TextEditor.Painting;
using Storm.TextEditor.Parsing;
using Storm.TextEditor.Parsing.Base;
using Storm.TextEditor.Parsing.Classes;
using Storm.TextEditor.Parsing.Language;
using Storm.TextEditor.Preset;
using Storm.TextEditor.Preset.Painting;
using Storm.TextEditor.Preset.TextDraw;
using Storm.TextEditor.Printing;
using Storm.TextEditor.Utilities;
using Storm.TextEditor.Win32;
using Storm.Win32;
/// <summary>
/// Defines how a member in the CodeCompletion should act.
/// </summary>
public enum MemberType
{
/// <summary>
/// Act normally - the member is able to be removed.
/// </summary>
Normal = 0,
/// <summary>
/// The member will be unable to be removed. This can be used for static members.
/// </summary>
Unremovable = 1,
}
/// <summary>
/// Defines what the CodeCompletion displays.
/// </summary>
public enum ListDisplay
{
/// <summary>
/// The CodeCompletion will display all of its members.
/// </summary>
Full = 0,
/// <summary>
/// The CodeCompletion will only display a list of given members.
/// </summary>
Special = 1,
}
/// <summary>
/// Describes how the code completer should auto complete a word.
/// </summary>
public enum CompleteType
{
/// <summary>
/// Complete the word normally.
/// </summary>
Normal = 0,
/// <summary>
/// Complete the word as if a parenthesis had been typed.
/// </summary>
Parenthesis = 1,
}
/// <summary>
/// Class used for bringing CodeCompletion support to the Storm.TextEditor -
/// when the user types, the CodeCompletion can suggest what he wants to type
/// based on what he already typed.
/// </summary>
[ToolboxItem(false)]
[ToolboxBitmap(typeof(CodeCompletion), "storm16.bmp")]
[Description("Class used for bringing CodeCompletion support to the Storm.TextEditor - when the user types, the CodeCompletion can suggest what he wants to type based on what he already typed.")]
public class CodeCompletion
: Panel
{
#region Fields
private TreeView _memberAst = null;
private ToolTip _memberTooltip = null;
private GListBox _memberList = null;
private TextEditor _textEditor = null;
private ListDisplay _listDisplay = ListDisplay.Full;
private ListBox.ObjectCollection _items = null;
private List<GListBoxItem> _displayList = null;
private bool _shouldUpdateList = false;
private bool _mayAutoComplete = false;
private string _typedText = "";
private int _keyCount = 0;
private int _keyCountBounds = 0;
private GListBoxItem _lastSelectedItem = null;
#endregion
#region Properties
/// <summary>
/// Gets or sets the ImageList of the CodeCompletion.
/// </summary>
[Browsable(true)]
[Description("Gets or sets the ImageList of the CodeCompletion.")]
public ImageList ImageList
{
get { return _memberList.ImageList; }
set { _memberList.ImageList = value; }
}
/// <summary>
/// Gets or sets how the CodeCompletion should display.
/// </summary>
[Browsable(false)]
[Description("Gets or sets how the CodeCompletion should display.")]
public ListDisplay ListDisplay
{
get { return _listDisplay; }
set
{
_listDisplay = value;
_shouldUpdateList = true;
}
}
/// <summary>
/// Gets or sets the list of GListBoxItem that the CodeCompletion will display instead of the full list
/// when ListDisplay is set to ListDisplay.Special.
/// </summary>
[Browsable(false)]
[Description("Gets or sets the list of GListBoxItem that the CodeCompletion will display instead of the full list when ListDisplay is set to ListDisplay.Special.")]
public List<GListBoxItem> DisplayList
{
get { return _displayList; }
set
{
_displayList = value;
_shouldUpdateList = true;
}
}
#endregion
#region Win32
[DllImport("user32.dll")]
public static extern bool InvalidateRect(IntPtr hwnd,
IntPtr lpRect, bool bErase);
#endregion
#region Methods
#region Public
/// <summary>
/// Adds a GListBoxItem to the CodeCompletion.
/// </summary>
/// <param name="item">GListBoxItem to add.</param>
/// <param name="overrideIfExists">If true, overrides existing member(s) with the same Text as the new item.</param>
/// <returns>Whether the adding of the GListBoxItem was a success.</returns>
public bool AddMemberItem(GListBoxItem item, bool overrideIfExists)
{
bool result = true;
if (overrideIfExists == true)
{
// Loop through all GListBoxItems in the memberList and remove all items
// with the same Text as the new item.
foreach (GListBoxItem memberItem in _memberAst.Nodes)
{
if (memberItem.Text == item.Text)
_memberAst.Nodes.Remove(memberItem);
}
// Add the item to the memberList.
_memberAst.Nodes.Add(item);
}
else
{
// Check if the memberList already contains an item with the same text.
// If it does, we can't add the item to the memberList.
foreach (GListBoxItem memberItem in _memberAst.Nodes)
{
if (memberItem.Text == item.Text)
{
result = false;
break;
}
}
// Check if the loop through the items found a match.
if (result == true)
_memberAst.Nodes.Add(item);
}
return result;
}
/// <summary>
/// Removes a specified GListBoxItem from the CodeCompletion.
/// </summary>
/// <param name="item">Item to remove.</param>
public void RemoveMemberItem(GListBoxItem item)
{
if (_memberAst.Nodes.Contains(item) == true)
_memberAst.Nodes.Remove(item);
}
/// <summary>
/// Removes all removable items of the CodeCompletion.
/// </summary>
public void RemoveAllMemberItems()
{
foreach (GListBoxItem item in _memberAst.Nodes)
{
if (item.MemberType == MemberType.Normal)
_memberAst.Nodes.Remove(item);
}
}
/// <summary>
/// Sorts all members of the list alphabetically.
/// A call to method is needed when the list should be updated.
/// </summary>
public void SortAlphabetically()
{
_memberList.Items.Clear();
GListBoxItem[] items = new GListBoxItem[_memberAst.Nodes.Count + 1];
int n = 0;
while ((n < _memberAst.Nodes.Count))
{
GListBoxItem memberItem = new GListBoxItem
(((GListBoxItem)_memberAst.Nodes[n]).Text,
((GListBoxItem)_memberAst.Nodes[n]).ImageIndex,
((GListBoxItem)_memberAst.Nodes[n]).MemberType,
((GListBoxItem)_memberAst.Nodes[n]).Description,
((GListBoxItem)_memberAst.Nodes[n]).Declaration);
memberItem.Tag = _memberAst.Nodes[n].Tag;
memberItem.Name = _memberAst.Nodes[n].Name;
items[n] = memberItem;
n += 1;
}
Array.Sort(items);
n = 1;
while ((n < items.Length))
{
GListBoxItem item = new GListBoxItem(items[n].Text,
items[n].ImageIndex, items[n].MemberType,
items[n].Description, items[n].Declaration);
item.Tag = (string)items[n].Tag;
_memberList.Items.Add(item);
_memberList.Invalidate();
n += 1;
}
}
#endregion
#region Private
#region Initialization
/// <summary>
/// Initializes the Controls used by the CodeCompletion.
/// </summary>
private void InitControls()
{
_memberTooltip = new ToolTip();
_memberList = new GListBox();
_memberAst = new TreeView();
#region ListBox
// Initialize visual fields of the list.
_memberList.DrawMode = DrawMode.OwnerDrawFixed;
_memberList.ImageList = null;
// Initialize screen fields of the list.
_memberList.Location = new Point(100, 76);
_memberList.Size = new Size(210, 174);
_memberList.Dock = DockStyle.Fill;
// Initialize base settings for the list.
_memberList.FormattingEnabled = true;
_memberList.ItemHeight = 17;
_memberList.Name = "_memberList";
_memberList.Parent = this;
// Initialize other fields of the list.
_memberList.TabIndex = 9;
_memberList.Visible = true;
#endregion
#region AST
// Initialize visual fields of the TreeView.
_memberAst.FullRowSelect = true;
_memberAst.PathSeparator = ".";
_memberAst.LineColor = Color.FromArgb(109, 109, 111);
// Initialize screen fields of the TreeView.
_memberAst.Location = new Point(329, 112);
_memberAst.Size = new Size(355, 216);
// Initialize other fields of the TreeView.
_memberAst.Name = "_memberAst";
_memberAst.TabIndex = 8;
_memberAst.Visible = false;
#endregion
// Finally add our Controls so they can be usable.
Controls.Add(_memberList);
}
#endregion
#region Wrappers
private Point GetPositionFromCaretIndex()
{
return new Point(_textEditor.Caret.Position.X,
_textEditor.Caret.Position.Y);
}
#endregion
#region Text/Base EventHandlers
private void OnKeyDown(object sender, KeyEventArgs e)
{
_mayAutoComplete = e.KeyCode == Keys.Space && this.Visible == true;
if (_mayAutoComplete == true)
{
// Space bar has been pressed - AutoComplete.
this.Hide();
_memberTooltip.Hide(_textEditor);
_textEditor.Focus();
// Don't auto complete if the typed word is already complete.
if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
{
if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
.CompleteType == CompleteType.Parenthesis)
{
this.SelectItem(CompleteType.Parenthesis);
}
else
this.SelectItem(CompleteType.Normal);
}
SendKeys.Send(" ");
_mayAutoComplete = false;
_typedText = "";
_keyCount = 0;
e.Handled = true;
}
}
private void OnListDoubleClick(object sender, EventArgs e)
{
if (_memberList.SelectedItem != null)
{
// Don't auto complete if the typed word is already complete.
if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
{
if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
.CompleteType == CompleteType.Parenthesis)
{
this.SelectItem(CompleteType.Parenthesis);
}
else
this.SelectItem(CompleteType.Normal);
}
this.Hide();
_memberTooltip.Hide(_textEditor);
_textEditor.Focus();
}
}
private void OnListItemChanged(object sender, EventArgs e)
{
// Show tooltip of the selected item.
Font font = new Font(_textEditor.FontName, _textEditor.FontSize);
float fontHeight = font.GetHeight();
Point point = GetPositionFromCaretIndex();
point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
point.X += (100 + Width + 2);
_memberTooltip.Show((string)((GListBoxItem)_memberList.
SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
SelectedItem).Declaration, _textEditor, point.X, point.Y, 6000);
_textEditor.Focus();
}
private void OnTextEditorClick(object sender, EventArgs e)
{
this.Hide();
_memberTooltip.Hide(_textEditor);
_textEditor.Focus();
}
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (this.Enabled == true)
{
if (_shouldUpdateList == true)
{
if (_listDisplay == ListDisplay.Special && _displayList != null)
{
_items = _memberList.Items;
_memberList.Items.Clear();
_memberAst.Nodes.Clear();
foreach (GListBoxItem item in _displayList)
{
_memberAst.Nodes.Add(item);
_memberList.Items.Add(item);
}
this.SortAlphabetically();
}
else
{
_memberList.Items.Clear();
_memberAst.Nodes.Clear();
foreach (object item in _items)
{
_memberAst.Nodes.Add(item as GListBoxItem);
_memberList.Items.Add(item as GListBoxItem);
}
this.SortAlphabetically();
}
_shouldUpdateList = false;
}
Font font = new Font(_textEditor.FontName, _textEditor.FontSize);
float fontHeight = (font.GetHeight());
// LockWindowUpdate to prevent flickering in the TextEditor parent.
Win32.LockWindowUpdate(this._textEditor.Handle);
if (e.KeyCode == Keys.Up)
{
// The up key moves up our member list, if
// the list is visible.
if (this.Visible == true)
{
if (_memberList.SelectedIndex > 0)
{
_memberList.SelectedIndex -= 1;
e.Handled = true;
}
}
return;
}
if (e.KeyCode == Keys.Down)
{
// The up key moves down our member list, if
// the list is visible.
if (this.Visible == true)
{
if (_memberList.SelectedIndex <
_memberList.Items.Count - 1)
{
_memberList.SelectedIndex += 1;
e.Handled = true;
}
}
else
{
_textEditor.Refresh();
}
return;
}
if (e.KeyCode == Keys.Oemcomma)
{
if (this.Visible == true)
{
_typedText = "";
_keyCount = 0;
_memberList.Refresh();
_memberTooltip.Show((string)((GListBoxItem)_memberList.
SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
SelectedItem).Declaration, _textEditor, 6000);
return;
}
}
if (e.KeyCode == Keys.Tab)
{
if (this.Visible == true)
{
// Tab key has been pressed - AutoComplete.
this.Hide();
_memberTooltip.Hide(_textEditor);
// Don't auto complete if the typed word is already complete.
if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
{
if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
.CompleteType == CompleteType.Parenthesis)
{
this.SelectItem(CompleteType.Parenthesis);
}
else
this.SelectItem(CompleteType.Normal);
}
_typedText = "";
_keyCount = 0;
e.Handled = true;
return;
}
}
if (e.KeyCode == Keys.Space)
{
if (this.Visible == true)
{
this.Hide();
_memberTooltip.Hide(_textEditor);
return;
}
}
if (e.KeyCode == Keys.D9 && e.Shift == true)
{
// Close bracket key pressed, hide the displayed item tooltip.
if (this.Visible == true)
{
// Don't auto complete if the typed word is already complete.
if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
this.SelectItem(CompleteType.Parenthesis);
_textEditor.Document.InsertText(")", _textEditor.Caret.Position.X, _textEditor.Caret.Position.Y);
_textEditor.Caret.MoveRight(false);
}
_memberTooltip.Hide(_textEditor);
this.Hide();
_keyCount = 0;
_typedText = "";
return;
}
if (e.KeyCode == Keys.D8 && e.Shift == true)
{
if (_memberList.SelectedItem != null && this.Visible == true)
{
// Show tooltip of the selected item.
// Don't auto complete if the typed word is already complete.
if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
this.SelectItem(CompleteType.Parenthesis);
_textEditor.Document.InsertText("(", _textEditor.Caret.Position.X, _textEditor.Caret.Position.Y);
_textEditor.Caret.MoveRight(false);
Point point = GetPositionFromCaretIndex();
point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
point.X += (100 + this.Width + 2);
_memberTooltip.Show((string)((GListBoxItem)_memberList.
SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
SelectedItem).Declaration, _textEditor, point.X, point.Y, 6000);
this.Hide();
_keyCount = 0;
_typedText = "";
}
return;
}
// Keep track of the current character, used
// for tracking whether to hide the list of members,
// when the delete button is pressed.
int i = _textEditor.Selection.SelStart;
string currentChar = "";
if (i > 0)
{
currentChar = _textEditor.Document.
Text.Substring(i - 1, 1);
}
if ((e.KeyValue > 31 && e.KeyValue < 127 ||
e.KeyCode == Keys.Back) &&
e.KeyCode != Keys.Space &&
e.KeyCode != Keys.Left &&
e.KeyCode != Keys.Right)
{
// Display the member listview if there are any items in it.
char val = (char)e.KeyValue;
_typedText = GetLastWord();
if (_typedText == "")
{
// There's obviously no typed text left - hide.
_memberTooltip.Hide(_textEditor);
this.Hide();
// We have to remember to unlock the Window.
Win32.LockWindowUpdate((IntPtr)0);
return;
}
_keyCount += 1;
if (_keyCount >= _keyCountBounds)
{
_keyCount = 0;
// Find the position of the caret.
Point tooltipPoint = GetPositionFromCaretIndex();
// Setting the position of the tooltip.
tooltipPoint.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
tooltipPoint.X += (100 + this.Width + 2);
// Reset the list's position to match the wanted.
Point point = GetPositionFromCaretIndex();
point.X += 100;
point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
this.Location = point;
// Letter or number typed, search for it in the list.
// Loop through all the items in the list, looking
// for one that starts with the letters typed.
i = 1;
bool mayShow = false;
int toSelect = -1;
int n = 0;
while (((n >= _memberList.Items.Count)) == false)
{
// Find dots in the itemString.
// If there's dots, find their
// index and remove everything
// before the dot and itself.
int dotIndex = 0;
string itemString = _memberList.Items[n].
ToString().ToLower();
if (itemString.Contains("."))
{
dotIndex = itemString.IndexOf(".");
itemString = itemString.Substring(dotIndex + 1);
}
// We use + 1 because we need to exclude
// the dot from the string.
if (itemString.StartsWith(_typedText.ToLower()))
{
toSelect = n;
mayShow = true;
// Break out of the loop.
n = _memberList.Items.Count;
}
n += 1;
}
if (mayShow == true && toSelect > -1)
{
this.Show();
this.BringToFront();
_memberList.SelectedIndex = toSelect;
_memberTooltip.Show((string)((GListBoxItem)_memberList.
SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
SelectedItem).Declaration, _textEditor, tooltipPoint.X, tooltipPoint.Y, 6000);
}
else
{
if (Visible == true)
this.Hide();
}
}
if (_lastSelectedItem != null &&
e.KeyCode == Keys.Space)
{
_memberList.SelectedItem = _lastSelectedItem;
}
}
Win32.LockWindowUpdate((IntPtr)0);
}
}
#endregion
#region API
/// <summary>
/// Autofills the selected item in the member listbox, by
/// taking everything before and after the "." in the richtextbox,
/// and appending the word in the middle.
/// </summary>
private void SelectItem(CompleteType completeType)
{
if (_memberList.SelectedItem != null)
{
// Use a pretty fancy method to remove the already typed text.
int curN = _typedText.Length;
if (completeType == CompleteType.Normal)
{
while (curN > 0)
{
_textEditor.Caret.MoveLeft(true);
curN--;
}
}
else
{
while (curN >= 0)
{
_textEditor.Caret.MoveLeft(true);
curN--;
}
}
_textEditor.Selection.Text = "";
// Here ends the fancy method. :)
// Here we will simply insert some text at the caret's position.
TextPoint pos = _textEditor.Caret.Position;
_lastSelectedItem = (GListBoxItem)_memberList.SelectedItem;
_textEditor.Document.InsertText(_lastSelectedItem.Text, pos.X, pos.Y);
// We will now have another while-loop to make our selection end
// at the point that the inserted word ends.
curN = _lastSelectedItem.Text.Length;
while (curN > 0)
{
_textEditor.Caret.MoveRight(false);
curN--;
}
}
}
/// <summary>
/// Returns the Word that the Caret is currently at.
/// </summary>
/// <returns>The previous word from the Caret position.</returns>
private string GetLastWord()
{
Win32.LockWindowUpdate(_textEditor.Handle);
string word = "";
int selStart = _textEditor.Selection.SelStart;
_textEditor.Selection.SelStart--;
if (_textEditor.Caret.CurrentWord != null)
word = _textEditor.Caret.CurrentWord.Text;
_textEditor.Selection.SelStart = selStart;
Win32.LockWindowUpdate((IntPtr)0);
return word;
}
#endregion
#endregion
#endregion
/// <summary>
/// Initializes the CodeCompletion.
/// </summary>
/// <param name="textEditor">TextEditor for the CodeCompletion to
/// register events for.</param>
public CodeCompletion(TextEditor textEditor)
{
// First set the CodeCompletion TextEditor parent.
_textEditor = textEditor;
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.SetStyle(ControlStyles.Selectable, false);
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.ResizeRedraw, true);
// Initialize our Controls.
InitControls();
// Initialize visual setup of the CodeCompletion.
BackColor = SystemColors.Window;
Font = new Font("Tahoma", 8.25f, FontStyle.Regular);
_memberList.Font = Font;
// EventHandlers.
_textEditor.KeyUp += OnKeyUp;
_textEditor.KeyDown += OnKeyDown;
_textEditor.MouseDown += OnTextEditorClick;
_memberList.DoubleClick += OnListDoubleClick;
_memberList.SelectedIndexChanged += OnListItemChanged;
// We will have to re-focus the TextEditor before we can let
// the user go on using it.
_textEditor.Focus();
}
}
}