Browse Source

Insertion request event with append

pull/165/head
Ionite 1 year ago
parent
commit
7d4a068e7e
No known key found for this signature in database
  1. 75
      StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs
  2. 70
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs
  3. 22
      StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs
  4. 12
      StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs

75
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -12,6 +12,8 @@ using NLog;
using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Extensions;
using TextMateSharp.Grammars;
using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow;
namespace StabilityMatrix.Avalonia.Behaviors;
@ -94,15 +96,13 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
{
if (!IsEnabled || e.Text is not { } triggerText) return;
if (triggerText.All(char.IsLetterOrDigit))
if (triggerText.All(IsCompletionChar))
{
// Create completion window if its not already created
if (completionWindow == null)
{
Dispatcher.UIThread.Post(() =>
{
// Get the segment of the token the caret is currently in
if (GetCaretToken(textEditor) is not { } tokenSegment)
if (GetCaretCompletionToken() is not { } tokenSegment)
{
Logger.Trace("Token segment not found");
return;
@ -123,9 +123,22 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
};
completionWindow.Show();
});
}
}
else
{
// Disallowed chars, close completion window if its open
Logger.Trace($"Closing completion window: '{triggerText}' not a valid completion char");
completionWindow?.Close();
}
}
/// <summary>
/// Highlights the text segment in the text editor
/// </summary>
private void HighlightTextSegment(ISegment segment)
{
textEditor.TextArea.Selection = Selection.Create(textEditor.TextArea, segment);
}
private void TextArea_TextEntering(object? sender, TextInputEventArgs e)
@ -162,15 +175,59 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
}
/// <summary>
/// Gets a segment of the token the caret is currently in.
/// Gets a segment of the token the caret is currently in for completions.
/// Returns null if caret is not on a valid completion token (i.e. comments)
/// </summary>
private static ISegment? GetCaretToken(TextEditor textEditor)
private ISegment? GetCaretCompletionToken()
{
var caret = textEditor.CaretOffset;
// Get the line the caret is on
var line = textEditor.Document.GetLineByOffset(caret);
var lineText = textEditor.Document.GetText(line.Offset, line.Length);
// Tokenize
var result = TokenizerProvider.TokenizeLine(lineText);
var currentTokenIndex = -1;
IToken? currentToken = null;
// Get the token the caret is after
foreach (var (i, token) in result.Tokens.Enumerate())
{
// If we see a line comment token anywhere, return null
var isComment = token.Scopes.Any(s => s.Contains("comment.line"));
if (isComment)
{
Logger.Trace("Caret is in a comment");
return null;
}
// Find match
if (caret >= token.StartIndex && caret < token.EndIndex)
{
currentTokenIndex = i;
currentToken = token;
break;
}
}
// Still not found
if (currentToken is null || currentTokenIndex == -1)
{
Logger.Info($"Could not find token at caret offset {caret} for line {lineText.ToRepr}");
return null;
}
// Cap the offsets by the line offsets
return new TextSegment
{
StartOffset = Math.Max(currentToken.StartIndex, line.Offset),
EndOffset = Math.Min(currentToken.EndIndex, line.EndOffset)
};
// Search for the start and end of a token
// A token is defined as either alphanumeric chars or a space
var start = caret;
/*var start = caret;
while (start > 0 && IsCompletionChar(textEditor.Document.GetCharAt(start - 1)))
{
start--;
@ -182,6 +239,6 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
end++;
}
return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null;
return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null;*/
}
}

70
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionList.cs

@ -43,11 +43,11 @@ namespace StabilityMatrix.Avalonia.Controls.CodeCompletion;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class CompletionList : TemplatedControl
{
private CompletionListBox? _listBox;
public CompletionList()
{
DoubleTapped += OnDoubleTapped;
CompletionAcceptKeys = new[] { Key.Enter, Key.Tab, };
}
/// <summary>
@ -91,17 +91,33 @@ public class CompletionList : TemplatedControl
/// Is raised when the completion list indicates that the user has chosen
/// an entry to be completed.
/// </summary>
public event EventHandler? InsertionRequested;
public event EventHandler<InsertionRequestEventArgs>? InsertionRequested;
/// <summary>
/// Raised when the completion list indicates that it should be closed.
/// </summary>
public event EventHandler? CloseRequested;
/// <summary>
/// Raises the InsertionRequested event.
/// </summary>
public void RequestInsertion(EventArgs e)
public void RequestInsertion(ICompletionData item, RoutedEventArgs triggeringEvent, string? appendText = null)
{
InsertionRequested?.Invoke(this, e);
InsertionRequested?.Invoke(this, new InsertionRequestEventArgs
{
Item = item,
TriggeringEvent = triggeringEvent,
AppendText = appendText
});
}
private CompletionListBox? _listBox;
/// <summary>
/// Raises the CloseRequested event.
/// </summary>
public void RequestClose()
{
CloseRequested?.Invoke(this, EventArgs.Empty);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@ -129,9 +145,16 @@ public class CompletionList : TemplatedControl
}
/// <summary>
/// Gets or sets the array of keys that request insertion of the completion
/// Dictionary of keys that request insertion of the completion
/// mapped to strings that will be appended to the completion when selected.
/// The string may be empty.
/// </summary>
public Key[] CompletionAcceptKeys { get; set; }
public Dictionary<Key, string> CompletionAcceptKeys { get; init; } = new()
{
[Key.Enter] = "",
[Key.Tab] = "",
[Key.OemComma] = ",",
};
/// <summary>
/// Gets the scroll viewer used in this list box.
@ -161,6 +184,7 @@ public class CompletionList : TemplatedControl
/// Handles a key press. Used to let the completion list handle key presses while the
/// focus is still on the text editor.
/// </summary>
[SuppressMessage("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault")]
public void HandleKey(KeyEventArgs e)
{
if (_listBox == null)
@ -197,10 +221,19 @@ public class CompletionList : TemplatedControl
break;
default:
// Check insertion keys
if (CompletionAcceptKeys.Contains(e.Key) && CurrentList?.Count > 0)
if (CompletionAcceptKeys.TryGetValue(e.Key, out var appendText)
&& CurrentList?.Count > 0)
{
e.Handled = true;
RequestInsertion(e);
if (SelectedItem is { } item)
{
RequestInsertion(item, e, appendText);
}
else
{
RequestClose();
}
}
break;
@ -218,7 +251,15 @@ public class CompletionList : TemplatedControl
)
{
e.Handled = true;
RequestInsertion(e);
if (SelectedItem is { } item)
{
RequestInsertion(item, e);
}
else
{
RequestClose();
}
}
}
@ -331,6 +372,13 @@ public class CompletionList : TemplatedControl
var matchingItems = FilterItems(listToFilter, query);
// Close if no items match
if (matchingItems.Count == 0)
{
RequestClose();
return;
}
// Fast path if both only 1 item, and item is the same
if (FilteredCompletionData.Count == 1
&& matchingItems.Count == 1

22
StabilityMatrix.Avalonia/Controls/CodeCompletion/CompletionWindow.axaml.cs

@ -155,17 +155,32 @@ public class CompletionWindow : CompletionWindowBase
#endregion
private void CompletionList_InsertionRequested(object? sender, EventArgs e)
private void CompletionList_InsertionRequested(object? sender, InsertionRequestEventArgs e)
{
Hide();
// The window must close before Complete() is called.
// If the Complete callback pushes stacked input handlers, we don't want to pop those when the CC window closes.
var item = CompletionList.SelectedItem;
item?.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, EndOffset - StartOffset), e);
var length = EndOffset - StartOffset;
e.Item.Complete(TextArea, new AnchorSegment(TextArea.Document, StartOffset, length), e);
// Append text if requested
if (e.AppendText is { } appendText)
{
var end = StartOffset + e.Item.Text.Length;
TextArea.Document.Insert(end, appendText);
TextArea.Caret.Offset = end + appendText.Length;
}
}
private void CompletionList_CloseRequested(object? sender, EventArgs e)
{
Hide();
}
private void AttachEvents()
{
CompletionList.CloseRequested += CompletionList_CloseRequested;
CompletionList.InsertionRequested += CompletionList_InsertionRequested;
CompletionList.SelectionChanged += CompletionList_SelectionChanged;
TextArea.Caret.PositionChanged += CaretPositionChanged;
@ -176,6 +191,7 @@ public class CompletionWindow : CompletionWindowBase
/// <inheritdoc/>
protected override void DetachEvents()
{
CompletionList.CloseRequested -= CompletionList_CloseRequested;
CompletionList.InsertionRequested -= CompletionList_InsertionRequested;
CompletionList.SelectionChanged -= CompletionList_SelectionChanged;
TextArea.Caret.PositionChanged -= CaretPositionChanged;

12
StabilityMatrix.Avalonia/Controls/CodeCompletion/InsertionRequestEventArgs.cs

@ -0,0 +1,12 @@
using System;
using Avalonia.Interactivity;
namespace StabilityMatrix.Avalonia.Controls.CodeCompletion;
public class InsertionRequestEventArgs : EventArgs
{
public required ICompletionData Item { get; init; }
public required RoutedEventArgs TriggeringEvent { get; init; }
public string? AppendText { get; init; }
}
Loading…
Cancel
Save