improve completion triggering, filtering, insertion

This commit is contained in:
Matt Parker
2026-01-31 15:41:51 +10:00
parent 934c75ece9
commit d90c3045c7
4 changed files with 33 additions and 34 deletions

View File

@@ -603,12 +603,14 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> 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 // 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 // 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 record IdeCompletionListResult(Document Document, CompletionList CompletionList, LinePosition LinePosition);
public async Task<IdeCompletionListResult> GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, LinePosition linePosition, CompletionTrigger completionTrigger) public async Task<IdeCompletionListResult> GetCodeCompletionsForDocumentAtPosition(SharpIdeFile fileModel, string documentText, LinePosition linePosition, CompletionTrigger completionTrigger)
{ {
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetCodeCompletionsForDocumentAtPosition)}"); using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetCodeCompletionsForDocumentAtPosition)}");
await _solutionLoadedTcs.Task; await _solutionLoadedTcs.Task;
var document = await GetDocumentForSharpIdeFile(fileModel); var document = await GetDocumentForSharpIdeFile(fileModel);
Guard.Against.Null(document, nameof(document)); 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); var (completions, triggerLinePosition) = await GetCompletionsAsync(document, linePosition, completionTrigger).ConfigureAwait(false);
return new IdeCompletionListResult(document, completions, triggerLinePosition); return new IdeCompletionListResult(document, completions, triggerLinePosition);
} }
@@ -738,7 +740,6 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService
return (completions, triggerLinePosition); return (completions, triggerLinePosition);
} }
// Currently unused
public async Task<bool> ShouldTriggerCompletionAsync(SharpIdeFile file, string documentText, 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; await _solutionLoadedTcs.Task;
@@ -746,7 +747,6 @@ public partial class RoslynAnalysis(ILogger<RoslynAnalysis> logger, BuildService
var completionService = CompletionService.GetService(document); var completionService = CompletionService.GetService(document);
if (completionService is null) throw new InvalidOperationException("Completion service is not available for the 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 sourceText = SourceText.From(documentText, Encoding.UTF8);
var position = sourceText.Lines.GetPosition(linePosition); 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); var shouldTrigger = completionService.ShouldTriggerCompletion(document.Project, document.Project.Services, sourceText, position, completionTrigger, CompletionOptions.Default, document.Project.Solution.Options ?? OptionSet.Empty);

View File

@@ -216,22 +216,24 @@ public partial class SharpIdeCodeEdit : CodeEdit
private void OnTextChanged() private void OnTextChanged()
{ {
var text = Text;
var pendingCompletionTrigger = _pendingCompletionTrigger;
_pendingCompletionTrigger = null;
var cursorPosition = GetCaretPosition();
_ = Task.GodotRun(async () => _ = Task.GodotRun(async () =>
{ {
var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}"); var __ = SharpIdeOtel.Source.StartActivity($"{nameof(SharpIdeCodeEdit)}.{nameof(OnTextChanged)}");
_currentFile.IsDirty.Value = true; _currentFile.IsDirty.Value = true;
await _fileChangedService.SharpIdeFileChanged(_currentFile, Text, FileChangeType.IdeUnsavedChange); await _fileChangedService.SharpIdeFileChanged(_currentFile, text, FileChangeType.IdeUnsavedChange);
if (pendingCompletionTrigger is not null) if (pendingCompletionTrigger is not null)
{ {
var cursorPosition = GetCaretPosition();
var linePosition = new LinePosition(cursorPosition.line, cursorPosition.col);
completionTrigger = pendingCompletionTrigger; completionTrigger = pendingCompletionTrigger;
pendingCompletionTrigger = null; var linePosition = new LinePosition(cursorPosition.line, cursorPosition.col);
var shouldTriggerCompletion = await _roslynAnalysis.ShouldTriggerCompletionAsync(_currentFile, Text, linePosition, completionTrigger!.Value); 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}"); GD.Print($"Code completion trigger typed: '{completionTrigger.Value.Character}' at {linePosition.Line}:{linePosition.Character} should trigger: {shouldTriggerCompletion}");
if (shouldTriggerCompletion) if (shouldTriggerCompletion)
{ {
await OnCodeCompletionRequested(completionTrigger.Value); await OnCodeCompletionRequested(completionTrigger.Value, text, cursorPosition);
} }
} }
else if (pendingCompletionFilterReason is not null) else if (pendingCompletionFilterReason is not null)

View File

@@ -67,11 +67,11 @@ public partial class SharpIdeCodeEdit
return texture; return texture;
} }
private EventWrapper<CompletionTrigger, Task> CustomCodeCompletionRequested { get; } = new(_ => Task.CompletedTask); private EventWrapper<CompletionTrigger, string, (int,int), Task> CustomCodeCompletionRequested { get; } = new((_, _, _) => Task.CompletedTask);
private CompletionList? completionList; private CompletionList? completionList;
private Document? completionResultDocument; private Document? completionResultDocument;
private CompletionTrigger? completionTrigger; private CompletionTrigger? completionTrigger;
private CompletionTrigger? pendingCompletionTrigger; private CompletionTrigger? _pendingCompletionTrigger;
private CompletionFilterReason? pendingCompletionFilterReason; private CompletionFilterReason? pendingCompletionFilterReason;
private readonly List<string> _codeCompletionTriggers = private readonly List<string> _codeCompletionTriggers =
@@ -94,39 +94,36 @@ public partial class SharpIdeCodeEdit
private async Task CustomFilterCodeCompletionCandidates(CompletionFilterReason filterReason) private async Task CustomFilterCodeCompletionCandidates(CompletionFilterReason filterReason)
{ {
if (completionList is null || completionList.ItemsList.Count is 0) return; 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 linePosition = new LinePosition(cursorPosition.line, cursorPosition.col);
var filteredCompletions = RoslynAnalysis.FilterCompletions(_currentFile, Text, linePosition, completionList, completionTrigger!.Value, filterReason); var filteredCompletions = RoslynAnalysis.FilterCompletions(_currentFile, Text, linePosition, completionList, completionTrigger!.Value, filterReason);
_codeCompletionOptions = filteredCompletions; _codeCompletionOptions = filteredCompletions;
await this.InvokeAsync(QueueRedraw); 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}"); 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 // We can't draw until we get this position
_completionTriggerPosition = await this.InvokeAsync(() => GetPosAtLineColumn(completionsResult.LinePosition.Line, completionsResult.LinePosition.Character)); _completionTriggerPosition = await this.InvokeAsync(() => GetPosAtLineColumn(completionsResult.LinePosition.Line, completionsResult.LinePosition.Character));
completionList = completionsResult.CompletionList; completionList = completionsResult.CompletionList;
completionResultDocument = completionsResult.Document; completionResultDocument = completionsResult.Document;
var filterReason = completionTrigger.Kind switch var filterReason = completionTrigger.Kind switch
{ {
CompletionTriggerKind.Insertion => CompletionFilterReason.Insertion, CompletionTriggerKind.Insertion => CompletionFilterReason.Insertion,
CompletionTriggerKind.Deletion => CompletionFilterReason.Deletion, CompletionTriggerKind.Deletion => CompletionFilterReason.Deletion,
CompletionTriggerKind.InvokeAndCommitIfUnique => CompletionFilterReason.Other, CompletionTriggerKind.InvokeAndCommitIfUnique => CompletionFilterReason.Other,
_ => throw new ArgumentOutOfRangeException(nameof(completionTrigger.Kind), completionTrigger.Kind, null), _ => throw new ArgumentOutOfRangeException(nameof(completionTrigger.Kind), completionTrigger.Kind, null),
}; };
await CustomFilterCodeCompletionCandidates(filterReason); await CustomFilterCodeCompletionCandidates(filterReason);
GD.Print($"Found {completionsResult.CompletionList.ItemsList.Count} completions, displaying menu"); GD.Print($"Found {completionsResult.CompletionList.ItemsList.Count} completions, displaying menu");
});
} }
public void ApplySelectedCodeCompletion() public void ApplySelectedCodeCompletion()

View File

@@ -15,7 +15,7 @@ public partial class SharpIdeCodeEdit
if (@event.IsActionPressed(InputStringNames.CodeEditorRequestCompletions)) if (@event.IsActionPressed(InputStringNames.CodeEditorRequestCompletions))
{ {
completionTrigger = new CompletionTrigger(CompletionTriggerKind.InvokeAndCommitIfUnique); completionTrigger = new CompletionTrigger(CompletionTriggerKind.InvokeAndCommitIfUnique);
CustomCodeCompletionRequested.InvokeParallelFireAndForget(completionTrigger!.Value); CustomCodeCompletionRequested.InvokeParallelFireAndForget(completionTrigger!.Value, Text, GetCaretPosition());
return true; return true;
} }
} }
@@ -110,7 +110,7 @@ public partial class SharpIdeCodeEdit
if (isCodeCompletionPopupOpen is false && _codeCompletionTriggers.Contains(unicodeString, StringComparer.OrdinalIgnoreCase)) if (isCodeCompletionPopupOpen is false && _codeCompletionTriggers.Contains(unicodeString, StringComparer.OrdinalIgnoreCase))
{ {
pendingCompletionTrigger = CompletionTrigger.CreateInsertionTrigger(unicodeString[0]); _pendingCompletionTrigger = CompletionTrigger.CreateInsertionTrigger(unicodeString[0]);
return false; return false;
} }
} }