using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia; using Avalonia.Input; using Avalonia.Threading; using Avalonia.Xaml.Interactivity; using AvaloniaEdit; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; using NLog; using StabilityMatrix.Avalonia.Controls.CodeCompletion; using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models.TagCompletion; using CompletionWindow = StabilityMatrix.Avalonia.Controls.CodeCompletion.CompletionWindow; namespace StabilityMatrix.Avalonia.Behaviors; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class TextEditorCompletionBehavior : Behavior { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private TextEditor textEditor = null!; private CompletionWindow? completionWindow; public static readonly StyledProperty CompletionProviderProperty = AvaloniaProperty.Register(nameof(CompletionProvider)); public ICompletionProvider CompletionProvider { get => GetValue(CompletionProviderProperty); set => SetValue(CompletionProviderProperty, value); } public static readonly StyledProperty TokenizerProviderProperty = AvaloniaProperty.Register( "TokenizerProvider"); public ITokenizerProvider TokenizerProvider { get => GetValue(TokenizerProviderProperty); set => SetValue(TokenizerProviderProperty, value); } public static readonly StyledProperty IsEnabledProperty = AvaloniaProperty.Register( "IsEnabled", true); public bool IsEnabled { get => GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } protected override void OnAttached() { base.OnAttached(); if (AssociatedObject is not { } editor) { throw new NullReferenceException("AssociatedObject is null"); } textEditor = editor; textEditor.TextArea.TextEntered += TextArea_TextEntered; textEditor.TextArea.TextEntering += TextArea_TextEntering; } protected override void OnDetaching() { base.OnDetaching(); textEditor.TextArea.TextEntered -= TextArea_TextEntered; textEditor.TextArea.TextEntering -= TextArea_TextEntering; } private CompletionWindow CreateCompletionWindow(TextArea textArea) { var window = new CompletionWindow(textArea, CompletionProvider, TokenizerProvider) { WindowManagerAddShadowHint = false, CloseWhenCaretAtBeginning = true, CloseAutomatically = true, IsLightDismissEnabled = true, CompletionList = { IsFiltering = true } }; return window; } private void TextArea_TextEntered(object? sender, TextInputEventArgs e) { if (!IsEnabled || e.Text is not { } triggerText) return; if (triggerText.All(char.IsLetterOrDigit)) { // 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) { Logger.Trace("Token segment not found"); return; } var token = textEditor.Document.GetText(tokenSegment); Logger.Trace("Using token {Token} ({@Segment})", token, tokenSegment); completionWindow = CreateCompletionWindow(textEditor.TextArea); completionWindow.StartOffset = tokenSegment.Offset; completionWindow.EndOffset = tokenSegment.EndOffset; completionWindow.UpdateQuery(token); completionWindow.Closed += delegate { completionWindow = null; }; completionWindow.Show(); }); } } } private void TextArea_TextEntering(object? sender, TextInputEventArgs e) { if (completionWindow is null) return; /*Dispatcher.UIThread.Post(() => { // When completion window is open, parse and update token offsets if (GetCaretToken(textEditor) is not { } tokenSegment) { Logger.Trace("Token segment not found"); return; } completionWindow.StartOffset = tokenSegment.Offset; completionWindow.EndOffset = tokenSegment.EndOffset; });*/ /*if (e.Text?.Length > 0) { if (!char.IsLetterOrDigit(e.Text[0])) { // Whenever a non-letter is typed while the completion window is open, // insert the currently selected element. completionWindow?.CompletionList.RequestInsertion(e); } }*/ // Do not set e.Handled=true. // We still want to insert the character that was typed. } private static bool IsCompletionChar(char c) { return char.IsLetterOrDigit(c) || c == '_' || c == '-'; } /// /// Gets a segment of the token the caret is currently in. /// private static ISegment? GetCaretToken(TextEditor textEditor) { var caret = textEditor.CaretOffset; // Search for the start and end of a token // A token is defined as either alphanumeric chars or a space var start = caret; while (start > 0 && IsCompletionChar(textEditor.Document.GetCharAt(start - 1))) { start--; } var end = caret; while (end < textEditor.Document.TextLength && IsCompletionChar(textEditor.Document.GetCharAt(end))) { end++; } return start < end ? new TextSegment { StartOffset = start, EndOffset = end } : null; } }