rework completions v1

This commit is contained in:
Matt Parker
2026-01-23 22:09:48 +10:00
parent 19f9d07bf6
commit e766197ef8
12 changed files with 659 additions and 148 deletions

View File

@@ -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();
}
}