diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index 5914c7b..7158f21 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -603,12 +603,14 @@ public partial class RoslynAnalysis(ILogger logger, BuildService // 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, LinePosition LinePosition); - public async Task GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, LinePosition linePosition, CompletionTrigger completionTrigger) + public async Task GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, string documentText, 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)); + // The document in the workspace may have been further updated since the completion request was made, so we need to fork a document with the text at the time of the completion request + document = document.WithText(SourceText.From(documentText, Encoding.UTF8)); var (completions, triggerLinePosition) = await GetCompletionsAsync(document, linePosition, completionTrigger).ConfigureAwait(false); return new IdeCompletionListResult(document, completions, triggerLinePosition); } @@ -738,7 +740,6 @@ public partial class RoslynAnalysis(ILogger logger, BuildService return (completions, triggerLinePosition); } - // Currently unused public async Task ShouldTriggerCompletionAsync(SharpIdeFile file, string documentText, LinePosition linePosition, CompletionTrigger completionTrigger, CancellationToken cancellationToken = default) { await _solutionLoadedTcs.Task; @@ -746,7 +747,6 @@ public partial class RoslynAnalysis(ILogger logger, BuildService 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 = SourceText.From(documentText, Encoding.UTF8); var position = sourceText.Lines.GetPosition(linePosition); var shouldTrigger = completionService.ShouldTriggerCompletion(document.Project, document.Project.Services, sourceText, position, completionTrigger, CompletionOptions.Default, document.Project.Solution.Options ?? OptionSet.Empty); diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index 96c74b4..bb9569b 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -216,22 +216,24 @@ public partial class SharpIdeCodeEdit : CodeEdit private void OnTextChanged() { + var text = Text; + var pendingCompletionTrigger = _pendingCompletionTrigger; + _pendingCompletionTrigger = null; + var cursorPosition = GetCaretPosition(); _ = Task.GodotRun(async () => { var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}"); _currentFile.IsDirty.Value = true; - await _fileChangedService.SharpIdeFileChanged(_currentFile, Text, FileChangeType.IdeUnsavedChange); + 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); + var linePosition = new LinePosition(cursorPosition.line, cursorPosition.col); + 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); + await OnCodeCompletionRequested(completionTrigger.Value, text, cursorPosition); } } else if (pendingCompletionFilterReason is not null) diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions.cs index 64d03a2..b1abb19 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions.cs @@ -67,11 +67,11 @@ public partial class SharpIdeCodeEdit return texture; } - private EventWrapper CustomCodeCompletionRequested { get; } = new(_ => Task.CompletedTask); + private EventWrapper CustomCodeCompletionRequested { get; } = new((_, _, _) => Task.CompletedTask); private CompletionList? completionList; private Document? completionResultDocument; private CompletionTrigger? completionTrigger; - private CompletionTrigger? pendingCompletionTrigger; + private CompletionTrigger? _pendingCompletionTrigger; private CompletionFilterReason? pendingCompletionFilterReason; private readonly List _codeCompletionTriggers = @@ -94,39 +94,36 @@ public partial class SharpIdeCodeEdit private async Task CustomFilterCodeCompletionCandidates(CompletionFilterReason filterReason) { if (completionList is null || completionList.ItemsList.Count is 0) return; - var cursorPosition = GetCaretPosition(); + var cursorPosition = await this.InvokeAsync(() => 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) + private async Task OnCodeCompletionRequested(CompletionTrigger completionTrigger, string documentTextAtTimeOfCompletionRequest, (int, int) completionCaretPosition) { - var (caretLine, caretColumn) = GetCaretPosition(); + var (caretLine, caretColumn) = completionCaretPosition; GD.Print($"Code completion requested at line {caretLine}, column {caretColumn}"); - _ = Task.GodotRun(async () => - { - var linePos = new LinePosition(caretLine, caretColumn); + var linePos = new LinePosition(caretLine, caretColumn); - var completionsResult = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos, completionTrigger); + var completionsResult = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, documentTextAtTimeOfCompletionRequest, linePos, completionTrigger); - // We can't draw until we get this position - _completionTriggerPosition = await this.InvokeAsync(() => GetPosAtLineColumn(completionsResult.LinePosition.Line, completionsResult.LinePosition.Character)); + // 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"); - }); + 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() diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions_InputHandling.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions_InputHandling.cs index 206e447..abde47f 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions_InputHandling.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit_Completions_InputHandling.cs @@ -15,7 +15,7 @@ public partial class SharpIdeCodeEdit if (@event.IsActionPressed(InputStringNames.CodeEditorRequestCompletions)) { completionTrigger = new CompletionTrigger(CompletionTriggerKind.InvokeAndCommitIfUnique); - CustomCodeCompletionRequested.InvokeParallelFireAndForget(completionTrigger!.Value); + CustomCodeCompletionRequested.InvokeParallelFireAndForget(completionTrigger!.Value, Text, GetCaretPosition()); return true; } } @@ -110,7 +110,7 @@ public partial class SharpIdeCodeEdit if (isCodeCompletionPopupOpen is false && _codeCompletionTriggers.Contains(unicodeString, StringComparer.OrdinalIgnoreCase)) { - pendingCompletionTrigger = CompletionTrigger.CreateInsertionTrigger(unicodeString[0]); + _pendingCompletionTrigger = CompletionTrigger.CreateInsertionTrigger(unicodeString[0]); return false; } }