From afa0db072b154068384ecec8fb72ef41a38453d4 Mon Sep 17 00:00:00 2001 From: "marco.silipo" Date: Mon, 24 Feb 2020 21:08:50 +0100 Subject: [PATCH] Added basic code completion --- Arma.Studio.Data/ArmA.Studio.Data.csproj | 7 +++ .../TextEditor/ICodeCompletable.cs | 14 +++++ .../TextEditor/ICodeCompletionInfo.cs | 25 ++++++++ .../TextEditor/WordCompletionInfo.cs | 55 ++++++++++++++++++ Arma.Studio.Data/packages.config | 4 ++ Arma.Studio.SqfEditor/SqfEditor.cs | 34 ++++++++++- Arma.Studio/ArmA.Studio.csproj | 1 + Arma.Studio/Extensions.cs | 58 +++++++++++++++++++ Arma.Studio/UI/CompletionData.cs | 39 +++++++++++++ Arma.Studio/UI/TextEditorDataContext.cs | 57 ++++++++++++++++++ 10 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 Arma.Studio.Data/TextEditor/ICodeCompletable.cs create mode 100644 Arma.Studio.Data/TextEditor/ICodeCompletionInfo.cs create mode 100644 Arma.Studio.Data/TextEditor/WordCompletionInfo.cs create mode 100644 Arma.Studio.Data/packages.config create mode 100644 Arma.Studio/UI/CompletionData.cs diff --git a/Arma.Studio.Data/ArmA.Studio.Data.csproj b/Arma.Studio.Data/ArmA.Studio.Data.csproj index ca52562..1777bb8 100644 --- a/Arma.Studio.Data/ArmA.Studio.Data.csproj +++ b/Arma.Studio.Data/ArmA.Studio.Data.csproj @@ -36,6 +36,7 @@ + @@ -79,7 +80,9 @@ True Language.resx + + @@ -99,6 +102,7 @@ + @@ -172,5 +176,8 @@ Designer + + + \ No newline at end of file diff --git a/Arma.Studio.Data/TextEditor/ICodeCompletable.cs b/Arma.Studio.Data/TextEditor/ICodeCompletable.cs new file mode 100644 index 0000000..628f27e --- /dev/null +++ b/Arma.Studio.Data/TextEditor/ICodeCompletable.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Arma.Studio.Data.TextEditor +{ + public interface ICodeCompletable + { + IEnumerable GetAutoCompleteInfos(string text, int caretOffset); + bool IsSeparatorCharacter(char c); + } +} diff --git a/Arma.Studio.Data/TextEditor/ICodeCompletionInfo.cs b/Arma.Studio.Data/TextEditor/ICodeCompletionInfo.cs new file mode 100644 index 0000000..f6ba5ed --- /dev/null +++ b/Arma.Studio.Data/TextEditor/ICodeCompletionInfo.cs @@ -0,0 +1,25 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace Arma.Studio.Data.TextEditor +{ + public interface ICodeCompletionInfo + { + ImageSource ImageSource { get; } + + string Text { get; } + + object Content { get; } + + object Description { get; } + + double Priority { get; } + + abstract string Complete(string input); + } +} diff --git a/Arma.Studio.Data/TextEditor/WordCompletionInfo.cs b/Arma.Studio.Data/TextEditor/WordCompletionInfo.cs new file mode 100644 index 0000000..56e1a9e --- /dev/null +++ b/Arma.Studio.Data/TextEditor/WordCompletionInfo.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace Arma.Studio.Data.TextEditor +{ + public class WordCompletionInfo : ICodeCompletionInfo + { + + public ImageSource ImageSource => null; + public string Text { get; private set; } + public object Content { get; private set; } + public object Description { get; private set; } + public double Priority => 0; + + public WordCompletionInfo(string word) + { + this.Text = word; + this.Content = new System.Windows.Controls.TextBlock { Text = word }; + } + public WordCompletionInfo(string word, string description) + { + this.Text = word; + this.Description = description; + this.Content = new System.Windows.Controls.TextBlock { Text = word }; + } + + public WordCompletionInfo(string ltype, string @operator, string rtype, string description) + { + this.Text = @operator; + this.Description = description; + var panel = new System.Windows.Controls.StackPanel { Orientation = System.Windows.Controls.Orientation.Horizontal }; + if (!String.IsNullOrWhiteSpace(ltype) ) + { + var textblock_ltype = new System.Windows.Controls.TextBlock { Text = ltype, FontStyle = System.Windows.FontStyles.Italic, Margin = new System.Windows.Thickness(0, 0, 6, 0) }; + panel.Children.Add(textblock_ltype); + } + var textblock_operator = new System.Windows.Controls.TextBlock { Text = @operator, FontWeight = System.Windows.FontWeights.Bold }; + panel.Children.Add(textblock_operator); + if (!String.IsNullOrWhiteSpace(rtype)) + { + var textblock_rtype = new System.Windows.Controls.TextBlock { Text = rtype, FontStyle = System.Windows.FontStyles.Italic, Margin = new System.Windows.Thickness(6, 0, 0, 0) }; + panel.Children.Add(textblock_rtype); + } + this.Content = panel; + } + public string Complete(string input) + { + return this.Text; + } + } +} diff --git a/Arma.Studio.Data/packages.config b/Arma.Studio.Data/packages.config new file mode 100644 index 0000000..af62e49 --- /dev/null +++ b/Arma.Studio.Data/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Arma.Studio.SqfEditor/SqfEditor.cs b/Arma.Studio.SqfEditor/SqfEditor.cs index aa15de9..cc5077a 100644 --- a/Arma.Studio.SqfEditor/SqfEditor.cs +++ b/Arma.Studio.SqfEditor/SqfEditor.cs @@ -12,7 +12,7 @@ namespace Arma.Studio.SqfEditor { - public class SqfEditor : ITextEditor, ILintable, IFoldable + public class SqfEditor : ITextEditor, ILintable, IFoldable, ICodeCompletable { private SqfVm.ClrVirtualmachine Virtualmachine { get; } public SqfEditor() @@ -253,6 +253,38 @@ void recursive() } } } + + #endregion + #region ICodeCompletable + public IEnumerable GetAutoCompleteInfos(string text, int caretOffset) + { + // find start of word + var start = caretOffset-1; + while (start > 0 && !this.IsSeparatorCharacter(text[start])) + { + start--; + } + start++; + var word = (start == text.Length || start < 0) ? String.Empty : text.Substring(start, caretOffset - start); + return PluginMain.SqfDefinitionsFile + .ConcatAll() + .Where((it) => it.Name.StartsWith(word, StringComparison.InvariantCultureIgnoreCase)) + .Select((it) => + { + switch(it) + { + case SqfDefinitionsFile.Binary binary: + return new WordCompletionInfo(binary.Left, binary.Name, binary.Right, string.Empty); + case SqfDefinitionsFile.Unary unary: + return new WordCompletionInfo(string.Empty, unary.Name, unary.Right, string.Empty); + case SqfDefinitionsFile.Nular nular: + return new WordCompletionInfo(string.Empty, nular.Name, string.Empty, string.Empty); + default: + return new WordCompletionInfo(it.Name); + } + }); + } + public bool IsSeparatorCharacter(char c) => char.IsWhiteSpace(c) || new char[] { ',', '(', ')', '[', ']', '{', '}', ';' }.Contains(c); #endregion } } diff --git a/Arma.Studio/ArmA.Studio.csproj b/Arma.Studio/ArmA.Studio.csproj index 186a0a8..cea798a 100644 --- a/Arma.Studio/ArmA.Studio.csproj +++ b/Arma.Studio/ArmA.Studio.csproj @@ -116,6 +116,7 @@ + diff --git a/Arma.Studio/Extensions.cs b/Arma.Studio/Extensions.cs index ba41280..e5843f1 100644 --- a/Arma.Studio/Extensions.cs +++ b/Arma.Studio/Extensions.cs @@ -1,4 +1,5 @@ using Arma.Studio.Data.TextEditor; +using ICSharpCode.AvalonEdit; using ICSharpCode.AvalonEdit.Document; using System; using System.Collections.Generic; @@ -34,5 +35,62 @@ public static ISegment GetSegment(this LintInfo lintInfo, TextDocument document) EndOffset = offset + lintInfo.Length }; } + public static int GetStartOffset(this TextEditor editor) + { + int off = editor.CaretOffset; + if (off <= 0 || off > editor.Document.TextLength) + { + return off; + } + + + int start; + + //find start + for (start = off - 1; start >= 0; start--) + { + char c = editor.Document.GetCharAt(start); + if (Char.IsWhiteSpace(c)) + { + start++; + return start; + } + } + return 0; + } + /// + /// Tries to find the start of a word. + /// + /// A valid instance. + /// The method to be used to test the characters. Should return True unless the char is not valid. + /// + public static int GetStartOffset(this TextEditor editor, Func isSeparatorCharacter = null) + { + if (isSeparatorCharacter == null) + { + isSeparatorCharacter = Char.IsLetter; + } + int off = editor.CaretOffset; + if (off <= 0 || off > editor.Document.TextLength) + { + return off; + } + + + int start; + + // find start + for (start = off - 1; start >= 0; start--) + { + char c = editor.Document.GetCharAt(start); + if (isSeparatorCharacter(c)) + { + start++; + return start; + } + } + return 0; + } + } } diff --git a/Arma.Studio/UI/CompletionData.cs b/Arma.Studio/UI/CompletionData.cs new file mode 100644 index 0000000..452b2d2 --- /dev/null +++ b/Arma.Studio/UI/CompletionData.cs @@ -0,0 +1,39 @@ +using Arma.Studio.Data.TextEditor; +using ICSharpCode.AvalonEdit.CodeCompletion; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Editing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace Arma.Studio.UI +{ + public class CompletionData : ICompletionData + { + private readonly ICodeCompletionInfo InnerCompletionInfo; + public ImageSource Image => this.InnerCompletionInfo.ImageSource; + + public string Text => this.InnerCompletionInfo.Text; + + public object Content => this.InnerCompletionInfo.Content; + + public object Description => this.InnerCompletionInfo.Description; + + public double Priority => this.InnerCompletionInfo.Priority; + + public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs) + { + var textSegment = textArea.Document.GetText(completionSegment); + var text = this.InnerCompletionInfo.Complete(textSegment); + textArea.Document.Replace(completionSegment, text); + } + + public CompletionData(ICodeCompletionInfo codeCompletionInfo) + { + this.InnerCompletionInfo = codeCompletionInfo; + } + } +} diff --git a/Arma.Studio/UI/TextEditorDataContext.cs b/Arma.Studio/UI/TextEditorDataContext.cs index 25ed315..daba7a5 100644 --- a/Arma.Studio/UI/TextEditorDataContext.cs +++ b/Arma.Studio/UI/TextEditorDataContext.cs @@ -3,6 +3,7 @@ using Arma.Studio.Data.TextEditor; using Arma.Studio.Data.UI; using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.CodeCompletion; using ICSharpCode.AvalonEdit.Document; using ICSharpCode.AvalonEdit.Folding; using ICSharpCode.AvalonEdit.Highlighting; @@ -68,6 +69,16 @@ public TextEditor TextEditorControl } private TextEditor _TextEditorControl; private Tuple _TextEditorControl_ScrollToLine; + public CompletionWindow CompletionWindow + { + get => this._CompletionWindow; + set + { + this._CompletionWindow = value; + this.RaisePropertyChanged(); + } + } + private CompletionWindow _CompletionWindow; //IHighlightingDefinition public bool IsReadOnly @@ -426,6 +437,26 @@ public void OnInitialized(FrameworkElement sender, EventArgs e) textEditor.TextArea.LeftMargins.Insert(0, bpm); textEditor.TextArea.LeftMargins.Insert(1, new RuntimeExecutionMargin(this)); this.FoldingManager = ICSharpCode.AvalonEdit.Folding.FoldingManager.Install(textEditor.TextArea); + + textEditor.TextArea.TextEntering += this.TextArea_TextEntering; + textEditor.TextArea.TextEntered += this.TextArea_TextEntered; ; + } + } + + private void TextArea_TextEntered(object sender, TextCompositionEventArgs e) + { + this.ShowAutoCompletion(); + } + + private void TextArea_TextEntering(object sender, TextCompositionEventArgs e) + { + if (e.Text.Length > 0 && this.CompletionWindow != null) + { + char c = e.Text[0]; + if (!char.IsLetterOrDigit(c) && c != '_') + { + this.CompletionWindow.CompletionList.RequestInsertion(e); + } } } @@ -518,7 +549,33 @@ public void OnPreviewKeyDown(UIElement sender, KeyEventArgs e) e.Handled = true; } break; + case Key.Space when Keyboard.Modifiers == ModifierKeys.Control: + { + this.ShowAutoCompletion(); + e.Handled = true; + } + break; + } + } + private void ShowAutoCompletion() + { + if (this.TextEditorInstance is ICodeCompletable codeCompletable) + { + if (this.CompletionWindow is null) + { + this.CompletionWindow = new CompletionWindow(this.TextEditorControl.TextArea); + this.CompletionWindow.Closed += delegate { + this.CompletionWindow = null; + }; + this.CompletionWindow.StartOffset = this.TextEditorControl.GetStartOffset(codeCompletable.IsSeparatorCharacter); + this.CompletionWindow.EndOffset = this.TextEditorControl.CaretOffset; + this.CompletionWindow.Show(); + } + this.CompletionWindow.StartOffset = this.TextEditorControl.GetStartOffset(codeCompletable.IsSeparatorCharacter); + var data = this.CompletionWindow.CompletionList.CompletionData; + data.Clear(); + data.AddRange(codeCompletable.GetAutoCompleteInfos(this.TextDocument.Text, this.TextEditorControl.CaretOffset).Select((it) => new CompletionData(it))); } } }