Files
SharpIDE/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions.cs
2026-01-23 22:10:50 +10:00

146 lines
7.3 KiB
C#

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;
public partial class SharpIdeCodeEdit
{
private readonly Texture2D _csharpMethodIcon = ResourceLoader.Load<Texture2D>("uid://b17p18ijhvsep");
private readonly Texture2D _csharpClassIcon = ResourceLoader.Load<Texture2D>("uid://b027uufaewitj");
private readonly Texture2D _csharpInterfaceIcon = ResourceLoader.Load<Texture2D>("uid://bdwmkdweqvowt");
private readonly Texture2D _localVariableIcon = ResourceLoader.Load<Texture2D>("uid://vwvkxlnvqqk3");
private readonly Texture2D _fieldIcon = ResourceLoader.Load<Texture2D>("uid://c4y7d5m4upfju");
private readonly Texture2D _parameterIcon = ResourceLoader.Load<Texture2D>("uid://b0bv71yfmd08f");
private readonly Texture2D _propertyIcon = ResourceLoader.Load<Texture2D>("uid://y5pwrwwrjqmc");
private readonly Texture2D _keywordIcon = ResourceLoader.Load<Texture2D>("uid://b0ujhoq2xg2v0");
private readonly Texture2D _namespaceIcon = ResourceLoader.Load<Texture2D>("uid://bob5blfjll4h3");
private readonly Texture2D _eventIcon = ResourceLoader.Load<Texture2D>("uid://c3upo3lxmgtls");
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;
var texture = (symbolKind, typeKind, accessibility) switch
{
(SymbolKind.Method, _, _) => _csharpMethodIcon,
(_, TypeKind.Interface, _) => _csharpInterfaceIcon,
(_, TypeKind.Enum, _) => _enumIcon,
(_, TypeKind.Delegate, _) => _delegateIcon,
(_, TypeKind.Class, _) => _csharpClassIcon,
(_, TypeKind.Struct, _) => _csharpClassIcon,
(SymbolKind.NamedType, _, _) => _csharpClassIcon,
(SymbolKind.Local, _, _) => _localVariableIcon,
(SymbolKind.Field, _, _) => _fieldIcon,
(SymbolKind.Parameter, _, _) => _parameterIcon,
(SymbolKind.Property, _, _) => _propertyIcon,
(SymbolKind.Namespace, _, _) => _namespaceIcon,
(SymbolKind.Event, _, _) => _eventIcon,
_ => null
};
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();
}
}