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("uid://b17p18ijhvsep"); private readonly Texture2D _csharpClassIcon = ResourceLoader.Load("uid://b027uufaewitj"); private readonly Texture2D _csharpInterfaceIcon = ResourceLoader.Load("uid://bdwmkdweqvowt"); private readonly Texture2D _localVariableIcon = ResourceLoader.Load("uid://vwvkxlnvqqk3"); private readonly Texture2D _fieldIcon = ResourceLoader.Load("uid://c4y7d5m4upfju"); private readonly Texture2D _parameterIcon = ResourceLoader.Load("uid://b0bv71yfmd08f"); private readonly Texture2D _propertyIcon = ResourceLoader.Load("uid://y5pwrwwrjqmc"); private readonly Texture2D _keywordIcon = ResourceLoader.Load("uid://b0ujhoq2xg2v0"); private readonly Texture2D _namespaceIcon = ResourceLoader.Load("uid://bob5blfjll4h3"); private readonly Texture2D _eventIcon = ResourceLoader.Load("uid://c3upo3lxmgtls"); private readonly Texture2D _enumIcon = ResourceLoader.Load("uid://8mdxo65qepqv"); private readonly Texture2D _delegateIcon = ResourceLoader.Load("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(typeKindString, out var tk) ? tk : null; Accessibility? accessibilityModifier = Enum.TryParse(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 CustomCodeCompletionRequested { get; } = new(_ => Task.CompletedTask); private CompletionList? completionList; private Document? completionResultDocument; private CompletionTrigger? completionTrigger; private CompletionTrigger? pendingCompletionTrigger; private CompletionFilterReason? pendingCompletionFilterReason; private readonly List _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.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(); } }