rework completions v1
This commit is contained in:
@@ -15,6 +15,8 @@ using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.FindSymbols;
|
||||
using Microsoft.CodeAnalysis.Host.Mef;
|
||||
using Microsoft.CodeAnalysis.MSBuild;
|
||||
using Microsoft.CodeAnalysis.Options;
|
||||
using Microsoft.CodeAnalysis.PooledObjects;
|
||||
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Razor.SemanticTokens;
|
||||
@@ -26,7 +28,6 @@ using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NuGet.Frameworks;
|
||||
using Roslyn.LanguageServer.Protocol;
|
||||
using SharpIDE.Application.Features.Analysis.FixLoaders;
|
||||
using SharpIDE.Application.Features.Analysis.ProjectLoader;
|
||||
using SharpIDE.Application.Features.Analysis.Razor;
|
||||
@@ -34,11 +35,14 @@ using SharpIDE.Application.Features.Build;
|
||||
using SharpIDE.Application.Features.FileWatching;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using static Roslyn.Utilities.EnumerableExtensions;
|
||||
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
|
||||
using CompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem;
|
||||
using CompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
|
||||
using CompletionOptions = Microsoft.CodeAnalysis.Completion.CompletionOptions;
|
||||
using Diagnostic = Microsoft.CodeAnalysis.Diagnostic;
|
||||
using DiagnosticSeverity = Microsoft.CodeAnalysis.DiagnosticSeverity;
|
||||
using VSInternalClientCapabilities = Roslyn.LanguageServer.Protocol.VSInternalClientCapabilities;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
@@ -598,15 +602,15 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService
|
||||
// We store the document here, so that we have the correct version of the document when we compute completions
|
||||
// This may not be the best way to do this, but it seems to work okay. It may only be a problem because I continue to update the doc in the workspace as the user continues typing, filtering the completion
|
||||
// I could possibly pause updating the document while the completion list is open, but that seems more complex - handling accepted vs cancelled completions etc
|
||||
public record IdeCompletionListResult(Document Document, CompletionList CompletionList);
|
||||
public async Task<IdeCompletionListResult> GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, LinePosition linePosition)
|
||||
public record IdeCompletionListResult(Document Document, CompletionList CompletionList, LinePosition LinePosition);
|
||||
public async Task<IdeCompletionListResult> GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, LinePosition linePosition, CompletionTrigger completionTrigger)
|
||||
{
|
||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetCodeCompletionsForDocumentAtPosition)}");
|
||||
await _solutionLoadedTcs.Task;
|
||||
var document = await GetDocumentForSharpIdeFile(fileModel);
|
||||
Guard.Against.Null(document, nameof(document));
|
||||
var completions = await GetCompletionsAsync(document, linePosition).ConfigureAwait(false);
|
||||
return new IdeCompletionListResult(document, completions);
|
||||
var (completions, triggerLinePosition) = await GetCompletionsAsync(document, linePosition, completionTrigger).ConfigureAwait(false);
|
||||
return new IdeCompletionListResult(document, completions, triggerLinePosition);
|
||||
}
|
||||
|
||||
// TODO: Pass in LinePositionSpan for refactorings that span multiple characters, e.g. extract method
|
||||
@@ -722,31 +726,82 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService
|
||||
return codeActions.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static async Task<CompletionList> GetCompletionsAsync(Document document, LinePosition linePosition, CancellationToken cancellationToken = default)
|
||||
private static async Task<(CompletionList completions, LinePosition triggerLinePosition)> GetCompletionsAsync(Document document, LinePosition linePosition, CompletionTrigger completionTrigger, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var completionService = CompletionService.GetService(document);
|
||||
if (completionService is null) throw new InvalidOperationException("Completion service is not available for the document.");
|
||||
|
||||
var sourceText = await document.GetTextAsync(cancellationToken);
|
||||
var position = sourceText.Lines.GetPosition(linePosition);
|
||||
var completions = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken);
|
||||
//var filterItems = completionService.FilterItems(document, completions.ItemsList.AsImmutable(), "va");
|
||||
return completions;
|
||||
var completions = await completionService.GetCompletionsAsync(document, position, completionTrigger, cancellationToken: cancellationToken);
|
||||
var triggerLinePosition = sourceText.GetLinePosition(completions.Span.Start);
|
||||
return (completions, triggerLinePosition);
|
||||
}
|
||||
|
||||
// Currently unused
|
||||
private async Task<bool> ShouldTriggerCompletionAsync(SharpIdeFile file, LinePosition linePosition, CompletionTrigger completionTrigger, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> ShouldTriggerCompletionAsync(SharpIdeFile file, string documentText, LinePosition linePosition, CompletionTrigger completionTrigger, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _solutionLoadedTcs.Task;
|
||||
var document = await GetDocumentForSharpIdeFile(file, cancellationToken);
|
||||
var completionService = CompletionService.GetService(document);
|
||||
if (completionService is null) throw new InvalidOperationException("Completion service is not available for the document.");
|
||||
|
||||
var sourceText = await document.GetTextAsync(cancellationToken);
|
||||
//var sourceText = await document.GetTextAsync(cancellationToken);
|
||||
var sourceText = SourceText.From(documentText, Encoding.UTF8);
|
||||
var position = sourceText.Lines.GetPosition(linePosition);
|
||||
var shouldTrigger = completionService.ShouldTriggerCompletion(sourceText, position, completionTrigger);
|
||||
var shouldTrigger = completionService.ShouldTriggerCompletion(document.Project, document.Project.Services, sourceText, position, completionTrigger, CompletionOptions.Default, document.Project.Solution.Options ?? OptionSet.Empty);
|
||||
return shouldTrigger;
|
||||
}
|
||||
|
||||
public static ImmutableArray<SharpIdeCompletionItem> FilterCompletions(SharpIdeFile file, string documentText, LinePosition linePosition, CompletionList completionList, CompletionTrigger completionTrigger, CompletionFilterReason filterReason)
|
||||
{
|
||||
var sourceText = SourceText.From(documentText, Encoding.UTF8);
|
||||
var position = sourceText.Lines.GetPosition(linePosition);
|
||||
|
||||
var filterSpanLength = position - completionList.Span.Start;
|
||||
// user has backspaced past the trigger span
|
||||
if (filterSpanLength < 0) return [];
|
||||
var filterSpan = new TextSpan(completionList.Span.Start, length: filterSpanLength);
|
||||
completionList = completionList.WithSpan(filterSpan);
|
||||
|
||||
var filteredCompletionItems = FilterCompletionList(completionList, completionTrigger, filterReason, sourceText);
|
||||
return filteredCompletionItems;
|
||||
}
|
||||
|
||||
private static ImmutableArray<SharpIdeCompletionItem> FilterCompletionList(CompletionList completionList, CompletionTrigger completionTrigger, CompletionFilterReason filterReason, SourceText sourceText)
|
||||
{
|
||||
var filterText = sourceText.GetSubText(completionList.Span).ToString();
|
||||
Console.WriteLine($"Filter text: '{filterText}'");
|
||||
|
||||
// Use pattern matching to determine which items are most relevant out of the calculated items.
|
||||
using var _ = ArrayBuilder<MatchResult>.GetInstance(out var matchResultsBuilder);
|
||||
var index = 0;
|
||||
using var helper = new PatternMatchHelper(filterText);
|
||||
foreach (var item in completionList.ItemsList)
|
||||
{
|
||||
if (helper.TryCreateMatchResult(
|
||||
item,
|
||||
completionTrigger.Kind,
|
||||
filterReason,
|
||||
recentItemIndex: -1,
|
||||
includeMatchSpans: true,
|
||||
index,
|
||||
out var matchResult))
|
||||
{
|
||||
matchResultsBuilder.Add(matchResult);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we sort the list based on the pattern matching result.
|
||||
matchResultsBuilder.Sort(MatchResult.SortingComparer);
|
||||
|
||||
var filteredList = matchResultsBuilder.SelectAsArray(matchResult =>
|
||||
new SharpIdeCompletionItem(matchResult.CompletionItem, matchResult.PatternMatch?.MatchedSpans));
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
|
||||
public async Task<(string updatedText, SharpIdeFileLinePosition sharpIdeFileLinePosition)> GetCompletionApplyChanges(SharpIdeFile file, CompletionItem completionItem, Document document, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//var documentId = _workspace!.CurrentSolution.GetDocumentIdsWithFilePath(file.Path).Single();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
public readonly record struct SharpIdeCompletionItem(CompletionItem CompletionItem, ImmutableArray<TextSpan>? MatchedSpans)
|
||||
{
|
||||
public readonly CompletionItem CompletionItem = CompletionItem;
|
||||
public readonly ImmutableArray<TextSpan>? MatchedSpans = MatchedSpans;
|
||||
}
|
||||
@@ -2,9 +2,7 @@ using System.Collections.Immutable;
|
||||
using Godot;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.Shared.TestHooks;
|
||||
using Microsoft.CodeAnalysis.Tags;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.CodeAnalysis.Threading;
|
||||
using ObservableCollections;
|
||||
@@ -21,7 +19,6 @@ using SharpIDE.Application.Features.NavigationHistory;
|
||||
using SharpIDE.Application.Features.Run;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
using SharpIDE.Godot.Features.Problems;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||
@@ -57,18 +54,6 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
[Inject] private readonly IdeNavigationHistoryService _navigationHistoryService = null!;
|
||||
[Inject] private readonly EditorCaretPositionService _editorCaretPositionService = null!;
|
||||
|
||||
private readonly List<string> _codeCompletionTriggers =
|
||||
[
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
"_", "<", ".", "#"
|
||||
];
|
||||
private readonly List<string> _additionalCodeCompletionPrefixes =
|
||||
[
|
||||
//"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
//"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"(", ",", "=", "\t", ":"
|
||||
];
|
||||
|
||||
public SharpIdeCodeEdit()
|
||||
{
|
||||
_selectionChangedQueue = new AsyncBatchingWorkQueue(TimeSpan.FromMilliseconds(150), ProcessSelectionChanged, IAsynchronousOperationListener.Instance, CancellationToken.None);
|
||||
@@ -76,14 +61,10 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// _filter_code_completion_candidates_impl uses these prefixes to determine where the completions menu is allowed to show.
|
||||
// It is quite annoying as we cannot override it via _FilterCodeCompletionCandidates, as we would lose the filtering as well.
|
||||
// Currently, it is not possible to show completions on a new line at col 0
|
||||
CodeCompletionPrefixes = [.._codeCompletionTriggers, .._additionalCodeCompletionPrefixes];
|
||||
SyntaxHighlighter = _syntaxHighlighter;
|
||||
_popupMenu = GetNode<PopupMenu>("CodeFixesMenu");
|
||||
_popupMenu.IdPressed += OnCodeFixSelected;
|
||||
CodeCompletionRequested += OnCodeCompletionRequested;
|
||||
CustomCodeCompletionRequested.Subscribe(OnCodeCompletionRequested);
|
||||
CodeFixesRequested += OnCodeFixesRequested;
|
||||
BreakpointToggled += OnBreakpointToggled;
|
||||
CaretChanged += OnCaretChanged;
|
||||
@@ -233,6 +214,25 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}");
|
||||
_currentFile.IsDirty.Value = true;
|
||||
await _fileChangedService.SharpIdeFileChanged(_currentFile, Text, FileChangeType.IdeUnsavedChange);
|
||||
if (pendingCompletionTrigger is not null)
|
||||
{
|
||||
var cursorPosition = GetCaretPosition();
|
||||
var linePosition = new LinePosition(cursorPosition.line, cursorPosition.col);
|
||||
completionTrigger = pendingCompletionTrigger;
|
||||
pendingCompletionTrigger = null;
|
||||
var shouldTriggerCompletion = await _roslynAnalysis.ShouldTriggerCompletionAsync(_currentFile, Text, linePosition, completionTrigger!.Value);
|
||||
GD.Print($"Code completion trigger typed: '{completionTrigger.Value.Character}' at {linePosition.Line}:{linePosition.Character} should trigger: {shouldTriggerCompletion}");
|
||||
if (shouldTriggerCompletion)
|
||||
{
|
||||
await OnCodeCompletionRequested(completionTrigger.Value);
|
||||
}
|
||||
}
|
||||
else if (pendingCompletionFilterReason is not null)
|
||||
{
|
||||
var filterReason = pendingCompletionFilterReason.Value;
|
||||
pendingCompletionFilterReason = null;
|
||||
await CustomFilterCodeCompletionCandidates(filterReason);
|
||||
}
|
||||
__?.Dispose();
|
||||
});
|
||||
}
|
||||
@@ -382,17 +382,19 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
};
|
||||
UnderlineRange(line, startCol, endCol, color);
|
||||
}
|
||||
DrawCompletionsPopup();
|
||||
}
|
||||
|
||||
// public override Array<Dictionary> _FilterCodeCompletionCandidates(Array<Dictionary> candidates)
|
||||
// {
|
||||
// return base._FilterCodeCompletionCandidates(candidates);
|
||||
// }
|
||||
|
||||
// This only gets invoked if the Node is focused
|
||||
public override void _GuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton { Pressed: true } mouseEvent)
|
||||
if (@event is InputEventMouseMotion) return;
|
||||
if (CompletionsPopupTryConsumeGuiInput(@event))
|
||||
{
|
||||
AcceptEvent();
|
||||
return;
|
||||
}
|
||||
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left or MouseButton.Right } mouseEvent)
|
||||
{
|
||||
var (col, line) = GetLineColumnAtPos((Vector2I)mouseEvent.Position);
|
||||
var current = _navigationHistoryService.Current.Value;
|
||||
@@ -402,44 +404,6 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
_navigationHistoryService.RecordNavigation(_currentFile, new SharpIdeFileLinePosition(line, col));
|
||||
}
|
||||
}
|
||||
else if (@event is InputEventKey { Pressed: true } keyEvent)
|
||||
{
|
||||
var codeCompletionSelectedIndex = GetCodeCompletionSelectedIndex();
|
||||
var isCodeCompletionPopupOpen = codeCompletionSelectedIndex is not -1;
|
||||
if (keyEvent is { Keycode: Key.Backspace, CtrlPressed: false })
|
||||
{
|
||||
|
||||
}
|
||||
if (keyEvent is { Keycode: Key.Delete, CtrlPressed: false })
|
||||
{
|
||||
|
||||
}
|
||||
else if (keyEvent.Unicode != 0)
|
||||
{
|
||||
var unicodeString = char.ConvertFromUtf32((int)keyEvent.Unicode);
|
||||
if (isCodeCompletionPopupOpen && unicodeString is " ")
|
||||
{
|
||||
Callable.From(() => CancelCodeCompletion()).CallDeferred();
|
||||
}
|
||||
else if (isCodeCompletionPopupOpen is false && _codeCompletionTriggers.Contains(unicodeString, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
void OnAction()
|
||||
{
|
||||
TextChanged -= OnAction;
|
||||
Callable.From(() => RequestCodeCompletion(true)).CallDeferred();
|
||||
}
|
||||
// TODO: This is flawed - we currently retrieve completions after TextChanged fires, but OnTextChange returns before the workspace is actually updated, so we may ask for completions for stale text.
|
||||
TextChanged += OnAction; // We need to wait for the text to actually change before requesting completions
|
||||
}
|
||||
}
|
||||
}
|
||||
// else if (@event.IsActionPressed("ui_text_completion_query"))
|
||||
// {
|
||||
// GD.Print("Entering CompletionQueryBuiltin _GuiInput");
|
||||
// AcceptEvent();
|
||||
// //GetViewport().SetInputAsHandled();
|
||||
// Callable.From(() => RequestCodeCompletion(true)).CallDeferred();
|
||||
// }
|
||||
}
|
||||
|
||||
public override void _UnhandledKeyInput(InputEvent @event)
|
||||
@@ -548,75 +512,6 @@ public partial class SharpIdeCodeEdit : CodeEdit
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public override void _ConfirmCodeCompletion(bool replace)
|
||||
{
|
||||
var selectedIndex = GetCodeCompletionSelectedIndex();
|
||||
var selectedText = GetCodeCompletionOption(selectedIndex);
|
||||
if (selectedText is null) return;
|
||||
var completionItem = selectedText["default_value"].As<GodotObjectContainer<IdeCompletionItem>>().Item;
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
await _ideApplyCompletionService.ApplyCompletion(_currentFile, completionItem.CompletionItem, completionItem.Document);
|
||||
});
|
||||
CancelCodeCompletion();
|
||||
}
|
||||
|
||||
private record struct IdeCompletionItem(CompletionItem CompletionItem, Document Document);
|
||||
private void OnCodeCompletionRequested()
|
||||
{
|
||||
var (caretLine, caretColumn) = GetCaretPosition();
|
||||
|
||||
GD.Print($"Code completion requested at line {caretLine}, column {caretColumn}");
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
var linePos = new LinePosition(caretLine, caretColumn);
|
||||
|
||||
var completionsResult = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos);
|
||||
var completionOptions = new List<(CodeCompletionKind kind, string displayText, Texture2D? icon, GodotObjectContainer<IdeCompletionItem> refCountedContainer)>(completionsResult.CompletionList.ItemsList.Count);
|
||||
|
||||
foreach (var completionItem in completionsResult.CompletionList.ItemsList)
|
||||
{
|
||||
var symbolKindString = CollectionExtensions.GetValueOrDefault(completionItem.Properties, "SymbolKind");
|
||||
var symbolKind = symbolKindString is null ? null : (SymbolKind?)int.Parse(symbolKindString);
|
||||
var wellKnownTags = completionItem.Tags;
|
||||
var typeKindString = completionItem.Tags.ElementAtOrDefault(0);
|
||||
var accessibilityModifierString = completionItem.Tags.Skip(1).FirstOrDefault(); // accessibility is not always supplied, and I don't think there's actually any guarantee on the order of tags. See WellKnownTags and WellKnownTagArrays
|
||||
TypeKind? typeKind = Enum.TryParse<TypeKind>(typeKindString, out var tk) ? tk : null;
|
||||
Accessibility? accessibilityModifier = Enum.TryParse<Accessibility>(accessibilityModifierString, out var am) ? am : null;
|
||||
var godotCompletionType = symbolKind switch
|
||||
{
|
||||
SymbolKind.Method => CodeCompletionKind.Function,
|
||||
SymbolKind.NamedType => CodeCompletionKind.Class,
|
||||
SymbolKind.Local => CodeCompletionKind.Variable,
|
||||
SymbolKind.Parameter => CodeCompletionKind.Variable,
|
||||
SymbolKind.Property => CodeCompletionKind.Member,
|
||||
SymbolKind.Field => CodeCompletionKind.Member,
|
||||
_ => CodeCompletionKind.PlainText
|
||||
};
|
||||
var isKeyword = wellKnownTags.Contains(WellKnownTags.Keyword);
|
||||
var isExtensionMethod = wellKnownTags.Contains(WellKnownTags.ExtensionMethod);
|
||||
var isMethod = wellKnownTags.Contains(WellKnownTags.Method);
|
||||
if (symbolKind is null && (isMethod || isExtensionMethod)) symbolKind = SymbolKind.Method;
|
||||
var icon = GetIconForCompletion(symbolKind, typeKind, accessibilityModifier, isKeyword);
|
||||
var ideItem = new IdeCompletionItem(completionItem, completionsResult.Document);
|
||||
// TODO: This is a GodotObjectContainer to avoid errors with the RefCountedContainer?? But the workaround 100% causes a memory leak as these are never freed, unlike RefCounted. Do this better
|
||||
var refContainer = new GodotObjectContainer<IdeCompletionItem>(ideItem);
|
||||
|
||||
completionOptions.Add((godotCompletionType, completionItem.DisplayText, icon, refContainer));
|
||||
}
|
||||
await this.InvokeAsync(() =>
|
||||
{
|
||||
foreach (var (godotCompletionType, displayText, icon, refCountedContainer) in completionOptions)
|
||||
{
|
||||
AddCodeCompletionOption(godotCompletionType, displayText, displayText, icon: icon, value: refCountedContainer);
|
||||
}
|
||||
UpdateCodeCompletionOptions(true);
|
||||
//RequestCodeCompletion(true);
|
||||
});
|
||||
GD.Print($"Found {completionsResult.CompletionList.ItemsList.Count} completions, displaying menu");
|
||||
});
|
||||
}
|
||||
|
||||
private (int line, int col) GetCaretPosition(bool startAt1 = false)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,6 @@ gutters_draw_breakpoints_gutter = true
|
||||
gutters_draw_executing_lines = true
|
||||
gutters_draw_line_numbers = true
|
||||
delimiter_strings = Array[String]([])
|
||||
code_completion_enabled = true
|
||||
code_completion_prefixes = Array[String]([".", " "])
|
||||
indent_automatic = true
|
||||
auto_brace_completion_enabled = true
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
using Godot;
|
||||
using System.Collections.Immutable;
|
||||
using Ardalis.GuardClauses;
|
||||
using Godot;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.Tags;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
|
||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||
|
||||
@@ -18,6 +25,25 @@ public partial class SharpIdeCodeEdit
|
||||
private readonly Texture2D _enumIcon = ResourceLoader.Load<Texture2D>("uid://8mdxo65qepqv");
|
||||
private readonly Texture2D _delegateIcon = ResourceLoader.Load<Texture2D>("uid://c83pv25rdescy");
|
||||
|
||||
private Texture2D? GetIconForCompletion(SharpIdeCompletionItem sharpIdeCompletionItem)
|
||||
{
|
||||
var completionItem = sharpIdeCompletionItem.CompletionItem;
|
||||
var symbolKindString = CollectionExtensions.GetValueOrDefault(completionItem.Properties, "SymbolKind");
|
||||
var symbolKind = symbolKindString is null ? null : (SymbolKind?)int.Parse(symbolKindString);
|
||||
var wellKnownTags = completionItem.Tags;
|
||||
var typeKindString = completionItem.Tags.ElementAtOrDefault(0);
|
||||
var accessibilityModifierString = completionItem.Tags.Skip(1).FirstOrDefault(); // accessibility is not always supplied, and I don't think there's actually any guarantee on the order of tags. See WellKnownTags and WellKnownTagArrays
|
||||
TypeKind? typeKind = Enum.TryParse<TypeKind>(typeKindString, out var tk) ? tk : null;
|
||||
Accessibility? accessibilityModifier = Enum.TryParse<Accessibility>(accessibilityModifierString, out var am) ? am : null;
|
||||
|
||||
var isKeyword = wellKnownTags.Contains(WellKnownTags.Keyword);
|
||||
var isExtensionMethod = wellKnownTags.Contains(WellKnownTags.ExtensionMethod);
|
||||
var isMethod = wellKnownTags.Contains(WellKnownTags.Method);
|
||||
if (symbolKind is null && (isMethod || isExtensionMethod)) symbolKind = SymbolKind.Method;
|
||||
var icon = GetIconForCompletion(symbolKind, typeKind, accessibilityModifier, isKeyword);
|
||||
return icon;
|
||||
}
|
||||
|
||||
private Texture2D? GetIconForCompletion(SymbolKind? symbolKind, TypeKind? typeKind, Accessibility? accessibility, bool isKeyword)
|
||||
{
|
||||
if (isKeyword) return _keywordIcon;
|
||||
@@ -40,4 +66,80 @@ public partial class SharpIdeCodeEdit
|
||||
};
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
private EventWrapper<CompletionTrigger, Task> CustomCodeCompletionRequested { get; } = new(_ => Task.CompletedTask);
|
||||
private CompletionList? completionList;
|
||||
private Document? completionResultDocument;
|
||||
private CompletionTrigger? completionTrigger;
|
||||
private CompletionTrigger? pendingCompletionTrigger;
|
||||
private CompletionFilterReason? pendingCompletionFilterReason;
|
||||
|
||||
private readonly List<string> _codeCompletionTriggers =
|
||||
[
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
|
||||
"_", "<", ".", "#"
|
||||
];
|
||||
|
||||
private void ResetCompletionPopupState()
|
||||
{
|
||||
_codeCompletionOptions = ImmutableArray<SharpIdeCompletionItem>.Empty;
|
||||
completionList = null;
|
||||
completionResultDocument = null;
|
||||
completionTrigger = null;
|
||||
_completionTriggerPosition = null;
|
||||
_codeCompletionCurrentSelected = 0;
|
||||
_codeCompletionForceItemCenter = -1;
|
||||
}
|
||||
|
||||
private async Task CustomFilterCodeCompletionCandidates(CompletionFilterReason filterReason)
|
||||
{
|
||||
if (completionList is null || completionList.ItemsList.Count is 0) return;
|
||||
var cursorPosition = GetCaretPosition();
|
||||
var linePosition = new LinePosition(cursorPosition.line, cursorPosition.col);
|
||||
var filteredCompletions = RoslynAnalysis.FilterCompletions(_currentFile, Text, linePosition, completionList, completionTrigger!.Value, filterReason);
|
||||
_codeCompletionOptions = filteredCompletions;
|
||||
await this.InvokeAsync(QueueRedraw);
|
||||
}
|
||||
|
||||
private async Task OnCodeCompletionRequested(CompletionTrigger completionTrigger)
|
||||
{
|
||||
var (caretLine, caretColumn) = GetCaretPosition();
|
||||
|
||||
GD.Print($"Code completion requested at line {caretLine}, column {caretColumn}");
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
var linePos = new LinePosition(caretLine, caretColumn);
|
||||
|
||||
var completionsResult = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos, completionTrigger);
|
||||
|
||||
// We can't draw until we get this position
|
||||
_completionTriggerPosition = await this.InvokeAsync(() => GetPosAtLineColumn(completionsResult.LinePosition.Line, completionsResult.LinePosition.Character));
|
||||
|
||||
completionList = completionsResult.CompletionList;
|
||||
completionResultDocument = completionsResult.Document;
|
||||
var filterReason = completionTrigger.Kind switch
|
||||
{
|
||||
CompletionTriggerKind.Insertion => CompletionFilterReason.Insertion,
|
||||
CompletionTriggerKind.Deletion => CompletionFilterReason.Deletion,
|
||||
CompletionTriggerKind.InvokeAndCommitIfUnique => CompletionFilterReason.Other,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(completionTrigger.Kind), completionTrigger.Kind, null),
|
||||
};
|
||||
await CustomFilterCodeCompletionCandidates(filterReason);
|
||||
GD.Print($"Found {completionsResult.CompletionList.ItemsList.Count} completions, displaying menu");
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplySelectedCodeCompletion()
|
||||
{
|
||||
var selectedIndex = _codeCompletionCurrentSelected;
|
||||
var completionItem = _codeCompletionOptions[selectedIndex];
|
||||
var document = completionResultDocument;
|
||||
_ = Task.GodotRun(async () =>
|
||||
{
|
||||
Guard.Against.Null(document);
|
||||
await _ideApplyCompletionService.ApplyCompletion(_currentFile, completionItem.CompletionItem, document);
|
||||
});
|
||||
ResetCompletionPopupState();
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
using System.Collections.Immutable;
|
||||
using Godot;
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
|
||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||
|
||||
public partial class SharpIdeCodeEdit
|
||||
{
|
||||
private ImmutableArray<SharpIdeCompletionItem> _codeCompletionOptions = [];
|
||||
|
||||
private Rect2I _codeCompletionRect = new Rect2I();
|
||||
private Rect2I _codeCompletionScrollRect = new Rect2I();
|
||||
private Vector2I _codeHintMinsize = new Vector2I();
|
||||
private Vector2I? _completionTriggerPosition;
|
||||
private int _codeCompletionLineOfs = 0;
|
||||
private int _codeCompletionForceItemCenter = -1;
|
||||
private int _codeCompletionCurrentSelected = 0;
|
||||
private bool _isCodeCompletionScrollHovered = false;
|
||||
private bool _isCodeCompletionScrollPressed = false;
|
||||
private const int MaxLines = 7;
|
||||
|
||||
private int? GetCompletionOptionAtPoint(Vector2I point)
|
||||
{
|
||||
if (!_codeCompletionRect.HasPoint(point)) return null;
|
||||
|
||||
int rowHeight = GetLineHeight();
|
||||
int relativeY = point.Y - _codeCompletionRect.Position.Y;
|
||||
int lineIndex = relativeY / rowHeight + _codeCompletionLineOfs;
|
||||
if (lineIndex < 0 || lineIndex >= _codeCompletionOptions.Length) return null;
|
||||
|
||||
return lineIndex;
|
||||
}
|
||||
|
||||
private void DrawCompletionsPopup()
|
||||
{
|
||||
var drawCodeCompletion = _codeCompletionOptions.Length > 0;
|
||||
var drawCodeHint = false;
|
||||
var codeHintDrawBelow = false;
|
||||
|
||||
if (!drawCodeCompletion) return;
|
||||
|
||||
// originally from theme cache
|
||||
const int codeCompletionIconSeparation = 4;
|
||||
var codeCompletionMinimumSize = new Vector2I(50, 50);
|
||||
var lineSpacing = 2;
|
||||
var themeScrollWidth = 6;
|
||||
//
|
||||
|
||||
var font = GetThemeFont(ThemeStringNames.Font);
|
||||
var fontSize = GetThemeFontSize(ThemeStringNames.FontSize);
|
||||
var ci = GetCanvasItem();
|
||||
var availableCompletions = _codeCompletionOptions.Length; // TODO: Actually get from completions
|
||||
var completionsToDisplay = Math.Min(availableCompletions, MaxLines);
|
||||
var rowHeight = GetLineHeight();
|
||||
var iconAreaSize = new Vector2I(rowHeight, rowHeight);
|
||||
|
||||
var completionMaxWidth = 200;
|
||||
var codeCompletionLongestLine = Math.Min(completionMaxWidth,
|
||||
_codeCompletionOptions.MaxBy(s => s.CompletionItem.DisplayText.Length)!.CompletionItem.DisplayText.Length * fontSize);
|
||||
codeCompletionLongestLine = 500;
|
||||
|
||||
_codeCompletionRect.Size = new Vector2I(
|
||||
codeCompletionLongestLine + codeCompletionIconSeparation + iconAreaSize.X + 2,
|
||||
completionsToDisplay * rowHeight
|
||||
);
|
||||
|
||||
var caretPos = (Vector2I)GetCaretDrawPos();
|
||||
var totalHeight = codeCompletionMinimumSize.Y + _codeCompletionRect.Size.Y;
|
||||
float minY = caretPos.Y - rowHeight;
|
||||
float maxY = caretPos.Y + rowHeight + totalHeight;
|
||||
|
||||
// if (drawCodeHint)
|
||||
// {
|
||||
// if (codeHintDrawBelow)
|
||||
// {
|
||||
// maxY += codeHintMinsize.Y;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// minY -= codeHintMinsize. Y;
|
||||
// }
|
||||
// }
|
||||
|
||||
bool canFitCompletionAbove = minY > totalHeight;
|
||||
var sharpIdeCodeEditSize = GetSize();
|
||||
bool canFitCompletionBelow = maxY <= sharpIdeCodeEditSize.Y;
|
||||
|
||||
bool shouldPlaceAbove = !canFitCompletionBelow && canFitCompletionAbove;
|
||||
|
||||
if (!canFitCompletionBelow && !canFitCompletionAbove)
|
||||
{
|
||||
float spaceAbove = caretPos.Y - rowHeight;
|
||||
float spaceBelow = sharpIdeCodeEditSize.Y - caretPos.Y;
|
||||
shouldPlaceAbove = spaceAbove > spaceBelow;
|
||||
|
||||
// Reduce the line count and recalculate heights to better fit the completion popup.
|
||||
float spaceAvail;
|
||||
if (shouldPlaceAbove)
|
||||
{
|
||||
spaceAvail = spaceAbove - codeCompletionMinimumSize.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
spaceAvail = spaceBelow - codeCompletionMinimumSize.Y;
|
||||
}
|
||||
|
||||
int maxLinesFit = Mathf.Max(1, (int)(spaceAvail / rowHeight));
|
||||
completionsToDisplay = Mathf.Min(completionsToDisplay, maxLinesFit);
|
||||
_codeCompletionRect.Size = new Vector2I(_codeCompletionRect.Size.X, completionsToDisplay * rowHeight);
|
||||
totalHeight = codeCompletionMinimumSize.Y + _codeCompletionRect.Size.Y;
|
||||
}
|
||||
|
||||
if (shouldPlaceAbove)
|
||||
{
|
||||
_codeCompletionRect.Position = new Vector2I(
|
||||
_codeCompletionRect.Position.X,
|
||||
(caretPos.Y - totalHeight - rowHeight) + lineSpacing
|
||||
);
|
||||
if (drawCodeHint && !codeHintDrawBelow)
|
||||
{
|
||||
_codeCompletionRect.Position = new Vector2I(
|
||||
_codeCompletionRect.Position.X,
|
||||
_codeCompletionRect.Position.Y - _codeHintMinsize.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_codeCompletionRect.Position = new Vector2I(
|
||||
_codeCompletionRect.Position.X,
|
||||
caretPos.Y + (lineSpacing / 2)
|
||||
);
|
||||
if (drawCodeHint && codeHintDrawBelow)
|
||||
{
|
||||
_codeCompletionRect.Position = new Vector2I(
|
||||
_codeCompletionRect.Position.X,
|
||||
_codeCompletionRect.Position.Y + _codeHintMinsize.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var scrollWidth = availableCompletions > MaxLines ? themeScrollWidth : 0;
|
||||
|
||||
// TODO: Fix
|
||||
var codeCompletionBase = "";
|
||||
|
||||
const int iconOffset = 25;
|
||||
// Desired X position for the popup to start at
|
||||
int desiredX = _completionTriggerPosition!.Value.X - iconOffset;
|
||||
|
||||
// Calculate the maximum X allowed so the popup stays inside the parent
|
||||
int maxX = (int)sharpIdeCodeEditSize.X - _codeCompletionRect.Size.X - scrollWidth;
|
||||
|
||||
// Clamp the X position so it never overflows to the right
|
||||
int finalX = Math.Min(desiredX, maxX);
|
||||
|
||||
_codeCompletionRect.Position = new Vector2I(finalX, _codeCompletionRect.Position.Y);
|
||||
|
||||
//var completionStyle = GetThemeStylebox(ThemeStringNames.Completion);
|
||||
// I don't know what this is used for, but it puts a weird block box around the completions
|
||||
// completionStyle.Draw(
|
||||
// ci,
|
||||
// new Rect2(
|
||||
// codeCompletionRect.Position - completionStyle.GetOffset(),
|
||||
// codeCompletionRect.Size + codeCompletionMinimumSize + new Vector2I(scrollWidth, 0)
|
||||
// )
|
||||
// );
|
||||
|
||||
var codeCompletionBackgroundColor = GetThemeColor(ThemeStringNames.CompletionBackgroundColor);
|
||||
if (codeCompletionBackgroundColor.A > 0.01f)
|
||||
{
|
||||
RenderingServer.Singleton.CanvasItemAddRect(
|
||||
ci,
|
||||
new Rect2(_codeCompletionRect.Position, _codeCompletionRect.Size + new Vector2I(scrollWidth, 0)),
|
||||
codeCompletionBackgroundColor
|
||||
);
|
||||
}
|
||||
|
||||
_codeCompletionScrollRect.Position = _codeCompletionRect.Position + new Vector2I(_codeCompletionRect.Size.X, 0);
|
||||
_codeCompletionScrollRect.Size = new Vector2I(scrollWidth, _codeCompletionRect.Size.Y);
|
||||
|
||||
_codeCompletionLineOfs = Mathf.Clamp(
|
||||
(_codeCompletionForceItemCenter < 0 ? _codeCompletionCurrentSelected : _codeCompletionForceItemCenter) -
|
||||
completionsToDisplay / 2,
|
||||
0,
|
||||
availableCompletions - completionsToDisplay
|
||||
);
|
||||
|
||||
var codeCompletionSelectedColor = GetThemeColor(ThemeStringNames.CompletionSelectedColor);
|
||||
RenderingServer.Singleton.CanvasItemAddRect(
|
||||
ci,
|
||||
new Rect2(
|
||||
new Vector2(
|
||||
_codeCompletionRect.Position.X,
|
||||
_codeCompletionRect.Position.Y + (_codeCompletionCurrentSelected - _codeCompletionLineOfs) * rowHeight
|
||||
),
|
||||
new Vector2(_codeCompletionRect.Size.X, rowHeight)
|
||||
),
|
||||
codeCompletionSelectedColor
|
||||
);
|
||||
|
||||
// TODO: Cache
|
||||
string lang = OS.GetLocale();
|
||||
for (int i = 0; i < completionsToDisplay; i++)
|
||||
{
|
||||
int l = _codeCompletionLineOfs + i;
|
||||
if (l < 0 || l >= availableCompletions)
|
||||
{
|
||||
GD.PushError($"Invalid line index: {l}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var sharpIdeCompletionItem = _codeCompletionOptions[l];
|
||||
var displayText = sharpIdeCompletionItem.CompletionItem.DisplayText;
|
||||
TextLine tl = new TextLine();
|
||||
tl.AddString(
|
||||
displayText,
|
||||
font,
|
||||
fontSize,
|
||||
lang
|
||||
);
|
||||
|
||||
float yofs = (rowHeight - tl.GetSize().Y) / 2;
|
||||
Vector2 titlePos = new Vector2(
|
||||
_codeCompletionRect.Position.X,
|
||||
_codeCompletionRect.Position.Y + i * rowHeight + yofs
|
||||
);
|
||||
|
||||
/* Draw completion icon if it is valid. */
|
||||
var icon = GetIconForCompletion(sharpIdeCompletionItem);
|
||||
Rect2 iconArea = new Rect2(
|
||||
new Vector2(_codeCompletionRect.Position.X, _codeCompletionRect.Position.Y + i * rowHeight),
|
||||
iconAreaSize
|
||||
);
|
||||
|
||||
if (icon != null)
|
||||
{
|
||||
Vector2 iconSize = iconArea.Size * 0.7f;
|
||||
icon.DrawRect(
|
||||
ci,
|
||||
new Rect2(
|
||||
iconArea.Position + (iconArea.Size - iconSize) / 2,
|
||||
iconSize
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
titlePos.X = iconArea.Position.X + iconArea.Size.X + codeCompletionIconSeparation;
|
||||
|
||||
tl.Width = _codeCompletionRect.Size.X - (iconAreaSize.X + codeCompletionIconSeparation);
|
||||
|
||||
tl.Alignment = HorizontalAlignment.Left;
|
||||
|
||||
|
||||
Vector2 matchPos = new Vector2(
|
||||
_codeCompletionRect.Position.X + iconAreaSize.X + codeCompletionIconSeparation,
|
||||
_codeCompletionRect.Position.Y + i * rowHeight
|
||||
);
|
||||
|
||||
foreach (var matchSegment in sharpIdeCompletionItem.MatchedSpans ?? [])
|
||||
{
|
||||
float matchOffset = font.GetStringSize(
|
||||
displayText.Substr(0, matchSegment.Start),
|
||||
HorizontalAlignment.Left,
|
||||
-1,
|
||||
fontSize
|
||||
).X;
|
||||
|
||||
float matchLen = font.GetStringSize(
|
||||
displayText.Substr(matchSegment.Start, matchSegment.Length),
|
||||
HorizontalAlignment.Left,
|
||||
-1,
|
||||
fontSize
|
||||
).X;
|
||||
|
||||
RenderingServer.Singleton.CanvasItemAddRect(
|
||||
ci,
|
||||
new Rect2(matchPos + new Vector2(matchOffset, 0), new Vector2(matchLen, rowHeight)),
|
||||
GetThemeColor(ThemeStringNames.CompletionExistingColor)
|
||||
);
|
||||
}
|
||||
|
||||
var fontColour = Colors.White;
|
||||
tl.Draw(ci, titlePos, fontColour);
|
||||
}
|
||||
|
||||
/* Draw a small scroll rectangle to show a position in the options. */
|
||||
if (scrollWidth > 0)
|
||||
{
|
||||
Color scrollColor = _isCodeCompletionScrollHovered || _isCodeCompletionScrollPressed
|
||||
? GetThemeColor(ThemeStringNames.CompletionScrollHoveredColor)
|
||||
: GetThemeColor(ThemeStringNames.CompletionScrollColor);
|
||||
|
||||
float r = (float)MaxLines / availableCompletions;
|
||||
float o = (float)_codeCompletionLineOfs / availableCompletions;
|
||||
|
||||
RenderingServer.Singleton.CanvasItemAddRect(
|
||||
ci,
|
||||
new Rect2(
|
||||
new Vector2(
|
||||
_codeCompletionRect.Position.X + _codeCompletionRect.Size.X,
|
||||
_codeCompletionRect.Position.Y + o * _codeCompletionRect.Size.Y
|
||||
),
|
||||
new Vector2(scrollWidth, _codeCompletionRect.Size.Y * r)
|
||||
),
|
||||
scrollColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjxy1d78vrp6o
|
||||
@@ -0,0 +1,120 @@
|
||||
using Godot;
|
||||
using Microsoft.CodeAnalysis.Completion;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using SharpIDE.Godot.Features.Problems;
|
||||
|
||||
namespace SharpIDE.Godot.Features.CodeEditor;
|
||||
|
||||
public partial class SharpIdeCodeEdit
|
||||
{
|
||||
private bool CompletionsPopupTryConsumeGuiInput(InputEvent @event)
|
||||
{
|
||||
var isCodeCompletionPopupOpen = _codeCompletionOptions.Length is not 0;
|
||||
if (isCodeCompletionPopupOpen is false)
|
||||
{
|
||||
if (@event.IsActionPressed(InputStringNames.CodeEditorRequestCompletions))
|
||||
{
|
||||
completionTrigger = new CompletionTrigger(CompletionTriggerKind.InvokeAndCommitIfUnique);
|
||||
CustomCodeCompletionRequested.InvokeParallelFireAndForget(completionTrigger!.Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (isCodeCompletionPopupOpen)
|
||||
{
|
||||
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left or MouseButton.Right } mouseEvent)
|
||||
{
|
||||
var codeCompletionIndex = GetCompletionOptionAtPoint((Vector2I)mouseEvent.Position);
|
||||
if (codeCompletionIndex is null)
|
||||
{
|
||||
// if the index is null, it means we clicked outside the completion popup, so close it
|
||||
ResetCompletionPopupState();
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no item is currently centered
|
||||
if (_codeCompletionForceItemCenter is -1)
|
||||
{
|
||||
// center the current central item, as an anchor, before we update the selection
|
||||
_codeCompletionForceItemCenter = _codeCompletionCurrentSelected;
|
||||
}
|
||||
|
||||
_codeCompletionCurrentSelected = codeCompletionIndex.Value;
|
||||
if (mouseEvent is { DoubleClick: true })
|
||||
{
|
||||
ApplySelectedCodeCompletion();
|
||||
return true;
|
||||
}
|
||||
|
||||
GD.Print($"Code completion option clicked: {codeCompletionIndex.Value}");
|
||||
QueueRedraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.WheelDown or MouseButton.WheelUp } scrollEvent)
|
||||
{
|
||||
if (_codeCompletionRect.HasPoint((Vector2I)scrollEvent.Position))
|
||||
{
|
||||
int scrollAmount = scrollEvent.ButtonIndex is MouseButton.WheelDown ? 1 : -1;
|
||||
_codeCompletionCurrentSelected = Mathf.Clamp(
|
||||
_codeCompletionCurrentSelected + scrollAmount,
|
||||
0,
|
||||
_codeCompletionOptions.Length - 1
|
||||
);
|
||||
_codeCompletionForceItemCenter = -1;
|
||||
QueueRedraw();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (@event.IsActionPressed(InputStringNames.Backspace))
|
||||
{
|
||||
pendingCompletionFilterReason = CompletionFilterReason.Deletion;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (@event is InputEventKey { Pressed: true, Keycode: Key.Up or Key.Down } inputEventKey)
|
||||
{
|
||||
var delta = inputEventKey.Keycode is Key.Up ? -1 : 1;
|
||||
_codeCompletionCurrentSelected = Mathf.Clamp(
|
||||
_codeCompletionCurrentSelected + delta,
|
||||
0,
|
||||
_codeCompletionOptions.Length - 1
|
||||
);
|
||||
_codeCompletionForceItemCenter = -1;
|
||||
QueueRedraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (@event is InputEventKey { Pressed: true, Keycode: Key.Escape })
|
||||
{
|
||||
ResetCompletionPopupState();
|
||||
QueueRedraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (@event is InputEventKey { Pressed: true, Keycode: Key.Enter or Key.Tab })
|
||||
{
|
||||
ApplySelectedCodeCompletion();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (@event is InputEventKey { Pressed: true, Unicode: not 0 } keyEvent)
|
||||
{
|
||||
|
||||
var unicodeString = char.ConvertFromUtf32((int)keyEvent.Unicode);
|
||||
if (isCodeCompletionPopupOpen && keyEvent.Unicode >= 32)
|
||||
{
|
||||
pendingCompletionFilterReason = CompletionFilterReason.Insertion;
|
||||
return false; // Let the text update happen
|
||||
}
|
||||
|
||||
if (isCodeCompletionPopupOpen is false && _codeCompletionTriggers.Contains(unicodeString, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pendingCompletionTrigger = CompletionTrigger.CreateInsertionTrigger(unicodeString[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5pllbl8kxp0q
|
||||
@@ -4,6 +4,7 @@ namespace SharpIDE.Godot;
|
||||
|
||||
public static class InputStringNames
|
||||
{
|
||||
public static readonly StringName Backspace = "ui_text_backspace";
|
||||
public static readonly StringName RenameSymbol = nameof(RenameSymbol);
|
||||
public static readonly StringName CodeFixes = nameof(CodeFixes);
|
||||
public static readonly StringName StepOver = nameof(StepOver);
|
||||
@@ -16,6 +17,7 @@ public static class InputStringNames
|
||||
public static readonly StringName SaveAllFiles = nameof(SaveAllFiles);
|
||||
public static readonly StringName EditorFontSizeIncrease = nameof(EditorFontSizeIncrease);
|
||||
public static readonly StringName EditorFontSizeDecrease = nameof(EditorFontSizeDecrease);
|
||||
public static readonly StringName CodeEditorRequestCompletions = nameof(CodeEditorRequestCompletions);
|
||||
}
|
||||
|
||||
public static class ThemeStringNames
|
||||
@@ -29,4 +31,12 @@ public static class ThemeStringNames
|
||||
|
||||
public static readonly StringName Panel = "panel";
|
||||
public static readonly StringName Separation = "separation";
|
||||
|
||||
public static readonly StringName Completion = "completion";
|
||||
public static readonly StringName CompletionBackgroundColor = "completion_background_color";
|
||||
public static readonly StringName CompletionSelectedColor = "completion_selected_color";
|
||||
public static readonly StringName CompletionScrollHoveredColor = "completion_scroll_hovered_color";
|
||||
public static readonly StringName CompletionScrollColor = "completion_scroll_color";
|
||||
public static readonly StringName CompletionExistingColor = "completion_existing_color";
|
||||
public static readonly StringName CompletionColorBgIcon = "completion_color_bg";
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Publicize Include="Microsoft.CodeAnalysis.Workspaces" />
|
||||
<Publicize Include="Microsoft.CodeAnalysis.Features" />
|
||||
<DoNotPublicize Include="Microsoft.CodeAnalysis.Workspaces:System.Linq.RoslynEnumerableExtensions" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -98,6 +98,11 @@ EditorFontSizeDecrease={
|
||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"button_mask":16,"position":Vector2(120, 12),"global_position":Vector2(129, 60),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
CodeEditorRequestCompletions={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user