From dceae57bbd8a56bc375c2bdd04f28a745a4d03b9 Mon Sep 17 00:00:00 2001 From: Hendrik Mennen Date: Wed, 3 Jul 2024 00:03:24 +0200 Subject: [PATCH] inlayhint functionality --- .../EditorExtensions/ExtendedTextEditor.cs | 10 +- .../EditorExtensions/InlayHintLineText.cs | 30 +++++ .../EditorExtensions/InlayHintRenderer.cs | 118 ++++++++++++++++++ .../LanguageService/ILanguageService.cs | 1 + .../LanguageService/LanguageServiceBase.cs | 5 + .../LanguageService/LanguageServiceLsp.cs | 23 ++++ .../TypeAssistanceLanguageService.cs | 55 ++++---- 7 files changed, 205 insertions(+), 37 deletions(-) create mode 100644 src/OneWare.Essentials/EditorExtensions/InlayHintLineText.cs create mode 100644 src/OneWare.Essentials/EditorExtensions/InlayHintRenderer.cs diff --git a/src/OneWare.Essentials/EditorExtensions/ExtendedTextEditor.cs b/src/OneWare.Essentials/EditorExtensions/ExtendedTextEditor.cs index ec23a89..8c59cc1 100644 --- a/src/OneWare.Essentials/EditorExtensions/ExtendedTextEditor.cs +++ b/src/OneWare.Essentials/EditorExtensions/ExtendedTextEditor.cs @@ -22,9 +22,11 @@ public class ExtendedTextEditor : TextEditor public TextMarkerService MarkerService { get; } public TextModificationService ModificationService { get; } - private ElementGenerator ElementGenerator { get; } + // private ElementGenerator ElementGenerator { get; } public FoldingManager? FoldingManager { get; private set; } + public InlayHintRenderer InlayHintRenderer { get; } + public ExtendedTextEditor() { // //Avoid Styles to improve performance @@ -42,13 +44,14 @@ public ExtendedTextEditor() TextArea.TextView.LinkTextUnderline = true; TextArea.RightClickMovesCaret = true; - ElementGenerator = new ElementGenerator(); BracketRenderer = new BracketHighlightRenderer(TextArea.TextView); LineRenderer = new LineHighlightRenderer(this); + //ElementGenerator = new ElementGenerator(); //MergeService = new MergeService(this, ElementGenerator); WordRenderer = new WordHighlightRenderer(TextArea.TextView); MarkerService = new TextMarkerService(Document); ModificationService = new TextModificationService(TextArea.TextView); + InlayHintRenderer = new InlayHintRenderer(this); TextArea.TextView.BackgroundRenderers.Add(BracketRenderer); TextArea.TextView.BackgroundRenderers.Add(LineRenderer); @@ -57,7 +60,8 @@ public ExtendedTextEditor() TextArea.TextView.BackgroundRenderers.Add(MarkerService); TextArea.TextView.LineTransformers.Add(ModificationService); - TextArea.TextView.ElementGenerators.Add(ElementGenerator); + //TextArea.TextView.ElementGenerators.Add(ElementGenerator); + TextArea.TextView.ElementGenerators.Add(InlayHintRenderer); } protected override void OnDocumentChanged(DocumentChangedEventArgs e) diff --git a/src/OneWare.Essentials/EditorExtensions/InlayHintLineText.cs b/src/OneWare.Essentials/EditorExtensions/InlayHintLineText.cs new file mode 100644 index 0000000..91211b0 --- /dev/null +++ b/src/OneWare.Essentials/EditorExtensions/InlayHintLineText.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using AvaloniaEdit.Rendering; + +namespace OneWare.Essentials.EditorExtensions; + +public class InlayHintLineText : VisualLineText +{ + /// + /// Creates a visual line text element with the specified length. + /// It uses the and its + /// to find the actual text string. + /// + public InlayHintLineText(VisualLine parentVisualLine, int length) : base(parentVisualLine, length) + { + + } + + /// + public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) + { + this.TextRunProperties.SetForegroundBrush(context.TextView.LinkTextForegroundBrush); + this.TextRunProperties.SetBackgroundBrush(context.TextView.LinkTextBackgroundBrush); + + if (context.TextView.LinkTextUnderline) + this.TextRunProperties.SetTextDecorations(TextDecorations.Underline); + return base.CreateTextRun(startVisualColumn, context); + } +} \ No newline at end of file diff --git a/src/OneWare.Essentials/EditorExtensions/InlayHintRenderer.cs b/src/OneWare.Essentials/EditorExtensions/InlayHintRenderer.cs new file mode 100644 index 0000000..c6c2836 --- /dev/null +++ b/src/OneWare.Essentials/EditorExtensions/InlayHintRenderer.cs @@ -0,0 +1,118 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using AvaloniaEdit; +using AvaloniaEdit.Document; +using AvaloniaEdit.Rendering; +using DynamicData; + +namespace OneWare.Essentials.EditorExtensions; + +public class InlayHint +{ + public TextLocation Location { get; init; } + + //Will be used internally + public int Offset { get; set; } = -1; + + public string Text { get; init; } = string.Empty; +} + +public class InlayHintRenderer : VisualLineElementGenerator +{ + private readonly TextEditor _editor; + private readonly List _hints = []; + private readonly List _hintControls = []; + + public InlayHintRenderer(TextEditor editor) + { + _editor = editor; + } + + public void SetInlineHints(IEnumerable hints) + { + _hints.Clear(); + _hintControls.Clear(); + + _hints.AddRange(hints); + + var foreground = Application.Current!.FindResource(Application.Current!.RequestedThemeVariant, "ThemeForegroundLowBrush") as IBrush; + var background = Application.Current!.FindResource(Application.Current!.RequestedThemeVariant, "ThemeBackgroundBrush") as IBrush; + + foreach (var hint in _hints) + { + _hintControls.Add(new Border() + { + Margin = new Thickness(1, 0, 5, 0), + Background = background, + CornerRadius = new CornerRadius(3), + VerticalAlignment = VerticalAlignment.Center, + Child = new TextBlock() + { + Text = hint.Text, + Foreground = foreground, + Margin = new Thickness(2,0), + VerticalAlignment = VerticalAlignment.Center + } + }); + } + + _editor.TextArea.TextView.Redraw(); + } + + public void ClearInlineHints() + { + _hints.Clear(); + _editor.TextArea.TextView.Redraw(); + } + + private static InlayHint? FindNextPosition(List positions, TextLocation startPosition) + { + int left = 0; + int right = positions.Count - 1; + + while (left <= right) + { + int mid = left + (right - left) / 2; + int comparison = positions[mid].Location.CompareTo(startPosition); + + if (comparison >= 0) + { + if (mid == 0 || positions[mid - 1].Location.CompareTo(startPosition) < 0) + { + return positions[mid]; + } + right = mid - 1; + } + else + { + left = mid + 1; + } + } + + return null; + } + + public override int GetFirstInterestedOffset(int startOffset) + { + var position = _editor.Document.GetLocation(startOffset); + var next = FindNextPosition(_hints, position); + + if(next == null) + return -1; + + next.Offset = _editor.Document.GetOffset(next.Location); + return next.Offset; + } + + public override VisualLineElement? ConstructElement(int offset) + { + var index = _hints.FindIndex(hint => hint.Offset == offset); + + if (index < 0) return null; + + var control = _hintControls[index]; + return new InlineObjectElement(0, control); + } +} \ No newline at end of file diff --git a/src/OneWare.Essentials/LanguageService/ILanguageService.cs b/src/OneWare.Essentials/LanguageService/ILanguageService.cs index aaef372..ac52665 100644 --- a/src/OneWare.Essentials/LanguageService/ILanguageService.cs +++ b/src/OneWare.Essentials/LanguageService/ILanguageService.cs @@ -56,6 +56,7 @@ public interface ILanguageService public Task RequestSymbolsAsync(string fullPath); public Task?> RequestDocumentColorAsync(string fullPath); public Task?> RequestSemanticTokensFullAsync(string fullPath); + public Task RequestInlayHintsAsync(string fullPath, Range range); public Task RequestFormattingAsync(string fullPath); public Task RequestRangeFormattingAsync(string fullPath, Range range); public Task ExecuteCommandAsync(Command cmd); diff --git a/src/OneWare.Essentials/LanguageService/LanguageServiceBase.cs b/src/OneWare.Essentials/LanguageService/LanguageServiceBase.cs index a1ea250..ba1137f 100644 --- a/src/OneWare.Essentials/LanguageService/LanguageServiceBase.cs +++ b/src/OneWare.Essentials/LanguageService/LanguageServiceBase.cs @@ -162,6 +162,11 @@ public virtual void RefreshTextDocument(string fullPath, string newText) return Task.FromResult?>(null); } + public virtual Task RequestInlayHintsAsync(string fullPath, Range range) + { + return Task.FromResult(null); + } + public virtual Task RequestFormattingAsync(string fullPath) { return Task.FromResult(null); diff --git a/src/OneWare.Essentials/LanguageService/LanguageServiceLsp.cs b/src/OneWare.Essentials/LanguageService/LanguageServiceLsp.cs index 30a1a0c..430d277 100644 --- a/src/OneWare.Essentials/LanguageService/LanguageServiceLsp.cs +++ b/src/OneWare.Essentials/LanguageService/LanguageServiceLsp.cs @@ -496,6 +496,29 @@ public override Task ExecuteCommandAsync(Command cmd) return null; } + public override async Task RequestInlayHintsAsync(string fullPath, Range range) + { + if (Client?.ServerSettings.Capabilities.InlayHintProvider == null) return null; + try + { + var inlayHintContainer = await Client.RequestInlayHints(new InlayHintParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = fullPath + }, + Range = range + }); + return inlayHintContainer; + } + catch (Exception e) + { + ContainerLocator.Container.Resolve()?.Error(e.Message, e); + } + + return null; + } + public override async Task RequestCompletionAsync(string fullPath, Position pos, CompletionTriggerKind triggerKind, string? triggerChar) { var cts = new CancellationTokenSource(); diff --git a/src/OneWare.Essentials/LanguageService/TypeAssistanceLanguageService.cs b/src/OneWare.Essentials/LanguageService/TypeAssistanceLanguageService.cs index b494c45..620148f 100644 --- a/src/OneWare.Essentials/LanguageService/TypeAssistanceLanguageService.cs +++ b/src/OneWare.Essentials/LanguageService/TypeAssistanceLanguageService.cs @@ -24,6 +24,7 @@ using Prism.Ioc; using CompletionList = OmniSharp.Extensions.LanguageServer.Protocol.Models.CompletionList; using IFile = OneWare.Essentials.Models.IFile; +using InlayHint = OneWare.Essentials.EditorExtensions.InlayHint; using Location = OmniSharp.Extensions.LanguageServer.Protocol.Models.Location; using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; @@ -108,6 +109,7 @@ protected virtual void Timer_Tick(object? sender, EventArgs e) //Execute slower actions after the caret was not changed for 100ms _ = GetDocumentHighlightAsync(); + _ = UpdateInlayHintsAsync(); } if (_lastCompletionItemChangedTime > _lastCompletionItemResolveTime && @@ -144,6 +146,7 @@ protected override void OnAssistanceActivated() Service.DidOpenTextDocument(CurrentFile.FullPath, Editor.CurrentDocument.Text); _ = UpdateSemanticTokensAsync(); + _ = UpdateInlayHintsAsync(); } protected override void OnAssistanceDeactivated() @@ -152,6 +155,7 @@ protected override void OnAssistanceDeactivated() base.OnAssistanceDeactivated(); Editor.Editor.ModificationService.ClearModification("caretHighlight"); Editor.Editor.ModificationService.ClearModification("semanticTokens"); + Editor.Editor.InlayHintRenderer.ClearInlineHints(); } protected virtual void DocumentChanged(object? sender, DocumentChangeEventArgs e) @@ -586,41 +590,24 @@ protected virtual async Task UpdateSemanticTokensAsync() { Editor.Editor.ModificationService.ClearModification("semanticTokens"); } + } + + protected virtual async Task UpdateInlayHintsAsync() + { + var inlayHintContainer = await Service.RequestInlayHintsAsync(CurrentFile.FullPath, new Range(0,0, CodeBox.Document.LineCount, 0)); - // LastDocumentSymbols = await Service.RequestSymbolsAsync(CurrentFile.FullPath); - // - // var languageManager = ContainerLocator.Container.Resolve(); - // - // if (LastDocumentSymbols is not null) - // { - // var segments = LastDocumentSymbols - // .Where(x => x.IsDocumentSymbolInformation && x.SymbolInformation != null) - // .Select(x => - // { - // if (x.IsDocumentSymbol && x.DocumentSymbol != null) - // { - // if (!languageManager.CurrentEditorThemeColors.TryGetValue(x.DocumentSymbol.Kind, - // out var brush)) return null; - // return x.DocumentSymbol.Range.GenerateTextModification(Editor.CurrentDocument, brush); - // } - // if (x.IsDocumentSymbolInformation && x.SymbolInformation != null) - // { - // if (!languageManager.CurrentEditorThemeColors.TryGetValue(x.SymbolInformation.Kind, out var brush)) return null; - // if (!x.SymbolInformation.Location.Uri.GetFileSystemPath().EqualPaths(CurrentFile.FullPath)) return null; - // return x.SymbolInformation.Location.Range.GenerateTextModification(Editor.CurrentDocument, brush); - // } - // return null; - // }) - // .Where(x => x is not null) - // .Cast() - // .ToArray(); - // - // Editor.Editor.ModificationService.SetModification("symbols", segments); - // } - // else - // { - // Editor.Editor.ModificationService.ClearModification("symbols"); - // } + if (inlayHintContainer is not null) + { + Editor.Editor.InlayHintRenderer.SetInlineHints(inlayHintContainer.Select(x => new InlayHint() + { + Location = new TextLocation(x.Position.Line + 1, x.Position.Character + 1), + Text = x.Label.ToString() + })); + } + else + { + Editor.Editor.InlayHintRenderer.ClearInlineHints(); + } } protected virtual async Task ShowSignatureHelpAsync(SignatureHelpTriggerKind triggerKind, string? triggerChar,