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. 105
      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

105
StabilityMatrix.Avalonia/Behaviors/TextEditorCompletionBehavior.cs

@ -12,6 +12,8 @@ using NLog;
using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Controls.CodeCompletion;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Core.Extensions;
using TextMateSharp.Grammars;
using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow;
namespace StabilityMatrix.Avalonia.Behaviors; namespace StabilityMatrix.Avalonia.Behaviors;
@ -94,38 +96,49 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
{ {
if (!IsEnabled || e.Text is not { } triggerText) return; 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 // Create completion window if its not already created
if (completionWindow == null) if (completionWindow == null)
{ {
Dispatcher.UIThread.Post(() => // Get the segment of the token the caret is currently in
if (GetCaretCompletionToken() is not { } tokenSegment)
{ {
// Get the segment of the token the caret is currently in Logger.Trace("Token segment not found");
if (GetCaretToken(textEditor) is not { } tokenSegment) return;
{ }
Logger.Trace("Token segment not found");
return;
}
var token = textEditor.Document.GetText(tokenSegment); var token = textEditor.Document.GetText(tokenSegment);
Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment);
completionWindow = CreateCompletionWindow(textEditor.TextArea); completionWindow = CreateCompletionWindow(textEditor.TextArea);
completionWindow.StartOffset = tokenSegment.Offset; completionWindow.StartOffset = tokenSegment.Offset;
completionWindow.EndOffset = tokenSegment.EndOffset; completionWindow.EndOffset = tokenSegment.EndOffset;
completionWindow.UpdateQuery(token); completionWindow.UpdateQuery(token);
completionWindow.Closed += delegate completionWindow.Closed += delegate
{ {
completionWindow = null; completionWindow = null;
}; };
completionWindow.Show(); 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) private void TextArea_TextEntering(object? sender, TextInputEventArgs e)
@ -162,15 +175,59 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
} }
/// <summary> /// <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> /// </summary>
private static ISegment? GetCaretToken(TextEditor textEditor) private ISegment? GetCaretCompletionToken()
{ {
var caret = textEditor.CaretOffset; 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 // Search for the start and end of a token
// A token is defined as either alphanumeric chars or a space // 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))) while (start > 0 && IsCompletionChar(textEditor.Document.GetCharAt(start - 1)))
{ {
start--; start--;
@ -182,6 +239,6 @@ public class TextEditorCompletionBehavior : Behavior<TextEditor>
end++; 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")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class CompletionList : TemplatedControl public class CompletionList : TemplatedControl
{ {
private CompletionListBox? _listBox;
public CompletionList() public CompletionList()
{ {
DoubleTapped += OnDoubleTapped; DoubleTapped += OnDoubleTapped;
CompletionAcceptKeys = new[] { Key.Enter, Key.Tab, };
} }
/// <summary> /// <summary>
@ -91,17 +91,33 @@ public class CompletionList : TemplatedControl
/// Is raised when the completion list indicates that the user has chosen /// Is raised when the completion list indicates that the user has chosen
/// an entry to be completed. /// an entry to be completed.
/// </summary> /// </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> /// <summary>
/// Raises the InsertionRequested event. /// Raises the InsertionRequested event.
/// </summary> /// </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) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
@ -129,9 +145,16 @@ public class CompletionList : TemplatedControl
} }
/// <summary> /// <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> /// </summary>
public Key[] CompletionAcceptKeys { get; set; } public Dictionary<Key, string> CompletionAcceptKeys { get; init; } = new()
{
[Key.Enter] = "",
[Key.Tab] = "",
[Key.OemComma] = ",",
};
/// <summary> /// <summary>
/// Gets the scroll viewer used in this list box. /// 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 /// Handles a key press. Used to let the completion list handle key presses while the
/// focus is still on the text editor. /// focus is still on the text editor.
/// </summary> /// </summary>
[SuppressMessage("ReSharper", "SwitchStatementHandlesSomeKnownEnumValuesWithDefault")]
public void HandleKey(KeyEventArgs e) public void HandleKey(KeyEventArgs e)
{ {
if (_listBox == null) if (_listBox == null)
@ -197,10 +221,19 @@ public class CompletionList : TemplatedControl
break; break;
default: default:
// Check insertion keys // 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; e.Handled = true;
RequestInsertion(e);
if (SelectedItem is { } item)
{
RequestInsertion(item, e, appendText);
}
else
{
RequestClose();
}
} }
break; break;
@ -218,7 +251,15 @@ public class CompletionList : TemplatedControl
) )
{ {
e.Handled = true; 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); 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 // Fast path if both only 1 item, and item is the same
if (FilteredCompletionData.Count == 1 if (FilteredCompletionData.Count == 1
&& matchingItems.Count == 1 && matchingItems.Count == 1

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

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