better code completion apply

This commit is contained in:
Matt Parker
2025-10-22 18:33:51 +10:00
parent 23ded1e6dd
commit 37fdfa7df2
7 changed files with 97 additions and 13 deletions

View File

@@ -0,0 +1,17 @@
using Microsoft.CodeAnalysis.Completion;
using SharpIDE.Application.Features.FileWatching;
using SharpIDE.Application.Features.SolutionDiscovery;
namespace SharpIDE.Application.Features.Analysis;
public class IdeCompletionService(RoslynAnalysis roslynAnalysis, FileChangedService fileChangedService)
{
private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis;
private readonly FileChangedService _fileChangedService = fileChangedService;
public async Task ApplyCompletion(SharpIdeFile file, CompletionItem completionItem)
{
var (updatedDocumentText, newLinePosition) = await _roslynAnalysis.GetCompletionApplyChanges(file, completionItem);
await _fileChangedService.SharpIdeFileChanged(file, updatedDocumentText, FileChangeType.CompletionChange, newLinePosition);
}
}

View File

@@ -575,6 +575,37 @@ public class RoslynAnalysis
return completions; return completions;
} }
public async Task<(string updatedText, SharpIdeFileLinePosition sharpIdeFileLinePosition)> GetCompletionApplyChanges(SharpIdeFile file, CompletionItem completionItem, CancellationToken cancellationToken = default)
{
var documentId = _workspace!.CurrentSolution.GetDocumentIdsWithFilePath(file.Path).Single();
var document = _workspace.CurrentSolution.GetRequiredDocument(documentId);
var completionService = CompletionService.GetService(document) ?? throw new InvalidOperationException("Completion service is not available for the document.");
var completionChange = await completionService.GetChangeAsync(document, completionItem, commitCharacter: '.', cancellationToken: cancellationToken);
var sourceText = await document.GetTextAsync(cancellationToken);
var newText = sourceText.WithChanges(completionChange.TextChange);
var newCaretPosition = completionChange.NewPosition ?? NewCaretPosition();
var linePosition = newText.Lines.GetLinePosition(newCaretPosition);
var sharpIdeFileLinePosition = new SharpIdeFileLinePosition
{
Line = linePosition.Line,
Column = linePosition.Character
};
return (newText.ToString(), sharpIdeFileLinePosition);
int NewCaretPosition()
{
var caretPosition = completionChange.TextChange.Span.Start + completionChange.TextChange.NewText!.Length;
// if change ends with (), place caret between the parentheses
if (completionChange.TextChange.NewText!.EndsWith("()"))
{
caretPosition -= 1;
}
return caretPosition;
}
}
/// Returns the list of files that would be modified by applying the code action. Does not apply the changes to the workspace sln /// Returns the list of files that would be modified by applying the code action. Does not apply the changes to the workspace sln
public async Task<List<(SharpIdeFile File, string UpdatedText)>> GetCodeActionApplyChanges(CodeAction codeAction) public async Task<List<(SharpIdeFile File, string UpdatedText)>> GetCodeActionApplyChanges(CodeAction codeAction)
{ {

View File

@@ -13,7 +13,8 @@ public enum FileChangeType
IdeSaveToDisk, // Apply to disk IdeSaveToDisk, // Apply to disk
IdeUnsavedChange, // Apply only in memory IdeUnsavedChange, // Apply only in memory
ExternalChange, // Apply to disk, as well as in memory ExternalChange, // Apply to disk, as well as in memory
CodeActionChange // Apply to disk, as well as in memory CodeActionChange, // Apply to disk, as well as in memory
CompletionChange // Apply only in memory, as well as notify tabs of new content
} }
public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileManager openTabsFileManager) public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileManager openTabsFileManager)
@@ -59,7 +60,7 @@ public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileMa
} }
// All file changes should go via this service // All file changes should go via this service
public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType) public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType, SharpIdeFileLinePosition? linePosition = null)
{ {
if (changeType is FileChangeType.ExternalChange) if (changeType is FileChangeType.ExternalChange)
{ {
@@ -67,13 +68,19 @@ public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileMa
// Update any open tabs // Update any open tabs
// update in memory // update in memory
await _openTabsFileManager.UpdateFileTextInMemory(file, newContents); await _openTabsFileManager.UpdateFileTextInMemory(file, newContents);
file.FileContentsChangedExternally.InvokeParallelFireAndForget(); file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition);
} }
else if (changeType is FileChangeType.CodeActionChange) else if (changeType is FileChangeType.CodeActionChange)
{ {
// update in memory, tabs and save to disk // update in memory, tabs and save to disk
await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(file, newContents); await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(file, newContents);
file.FileContentsChangedExternally.InvokeParallelFireAndForget(); file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition);
}
else if (changeType is FileChangeType.CompletionChange)
{
// update in memory, tabs
await _openTabsFileManager.UpdateFileTextInMemory(file, newContents);
file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition);
} }
else if (changeType is FileChangeType.IdeSaveToDisk) else if (changeType is FileChangeType.IdeSaveToDisk)
{ {

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using R3; using R3;
using SharpIDE.Application.Features.Analysis;
using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.Events;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
@@ -19,7 +20,7 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode, IFileOrFolder
public required ReactiveProperty<bool> IsDirty { get; init; } public required ReactiveProperty<bool> IsDirty { get; init; }
public required bool SuppressDiskChangeEvents { get; set; } // probably has concurrency issues public required bool SuppressDiskChangeEvents { get; set; } // probably has concurrency issues
public required DateTimeOffset? LastIdeWriteTime { get; set; } public required DateTimeOffset? LastIdeWriteTime { get; set; }
public EventWrapper<Task> FileContentsChangedExternally { get; } = new(() => Task.CompletedTask); public EventWrapper<SharpIdeFileLinePosition?, Task> FileContentsChangedExternally { get; } = new((_) => Task.CompletedTask);
[SetsRequiredMembers] [SetsRequiredMembers]
internal SharpIdeFile(string fullPath, string name, IExpandableSharpIdeNode parent, ConcurrentBag<SharpIdeFile> allFiles) internal SharpIdeFile(string fullPath, string name, IExpandableSharpIdeNode parent, ConcurrentBag<SharpIdeFile> allFiles)

View File

@@ -26,6 +26,7 @@ public partial class DiAutoload : Node
services.AddScoped<RunService>(); services.AddScoped<RunService>();
services.AddScoped<IdeFileExternalChangeHandler>(); services.AddScoped<IdeFileExternalChangeHandler>();
services.AddScoped<CodeActionService>(); services.AddScoped<CodeActionService>();
services.AddScoped<IdeCompletionService>();
services.AddScoped<FileChangedService>(); services.AddScoped<FileChangedService>();
services.AddScoped<IdeFileWatcher>(); services.AddScoped<IdeFileWatcher>();
services.AddScoped<IdeOpenTabsFileManager>(); services.AddScoped<IdeOpenTabsFileManager>();

View File

@@ -1,8 +1,8 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Godot; using Godot;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities; using Roslyn.Utilities;
using SharpIDE.Application; using SharpIDE.Application;
@@ -14,6 +14,7 @@ using SharpIDE.Application.Features.FileWatching;
using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.Run;
using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery;
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
using SharpIDE.Godot.Features.Problems;
using SharpIDE.RazorAccess; using SharpIDE.RazorAccess;
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
using Timer = Godot.Timer; using Timer = Godot.Timer;
@@ -47,6 +48,7 @@ public partial class SharpIdeCodeEdit : CodeEdit
[Inject] private readonly RoslynAnalysis _roslynAnalysis = null!; [Inject] private readonly RoslynAnalysis _roslynAnalysis = null!;
[Inject] private readonly CodeActionService _codeActionService = null!; [Inject] private readonly CodeActionService _codeActionService = null!;
[Inject] private readonly FileChangedService _fileChangedService = null!; [Inject] private readonly FileChangedService _fileChangedService = null!;
[Inject] private readonly IdeCompletionService _ideCompletionService = null!;
public override void _Ready() public override void _Ready()
{ {
@@ -299,12 +301,12 @@ public partial class SharpIdeCodeEdit : CodeEdit
}); });
} }
private async Task OnFileChangedExternally() private async Task OnFileChangedExternally(SharpIdeFileLinePosition? linePosition)
{ {
var fileContents = await _openTabsFileManager.GetFileTextAsync(_currentFile); var fileContents = await _openTabsFileManager.GetFileTextAsync(_currentFile);
Callable.From(() => Callable.From(() =>
{ {
var currentCaretPosition = GetCaretPosition(); (int line, int col) currentCaretPosition = linePosition is null ? GetCaretPosition() : (linePosition.Value.Line, linePosition.Value.Column);
var vScroll = GetVScroll(); var vScroll = GetVScroll();
BeginComplexOperation(); BeginComplexOperation();
SetText(fileContents); SetText(fileContents);
@@ -317,8 +319,8 @@ public partial class SharpIdeCodeEdit : CodeEdit
public void SetFileLinePosition(SharpIdeFileLinePosition fileLinePosition) public void SetFileLinePosition(SharpIdeFileLinePosition fileLinePosition)
{ {
var line = fileLinePosition.Line - 1; var line = fileLinePosition.Line;
var column = fileLinePosition.Column - 1; var column = fileLinePosition.Column;
SetCaretLine(line); SetCaretLine(line);
SetCaretColumn(column); SetCaretColumn(column);
CenterViewportToCaret(); CenterViewportToCaret();
@@ -498,6 +500,20 @@ public partial class SharpIdeCodeEdit : CodeEdit
}); });
} }
public override void _ConfirmCodeCompletion(bool replace)
{
GD.Print("Code completion confirmed");
var selectedIndex = GetCodeCompletionSelectedIndex();
var selectedText = GetCodeCompletionOption(selectedIndex);
if (selectedText is null) return;
var completionItem = selectedText["default_value"].As<RefCountedContainer<CompletionItem>>().Item;
_ = Task.GodotRun(async () =>
{
await _ideCompletionService.ApplyCompletion(_currentFile, completionItem);
});
CancelCodeCompletion();
}
private void OnCodeCompletionRequested() private void OnCodeCompletionRequested()
{ {
var (caretLine, caretColumn) = GetCaretPosition(); var (caretLine, caretColumn) = GetCaretPosition();
@@ -510,9 +526,20 @@ public partial class SharpIdeCodeEdit : CodeEdit
var completions = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos); var completions = await _roslynAnalysis.GetCodeCompletionsForDocumentAtPosition(_currentFile, linePos);
await this.InvokeAsync(() => await this.InvokeAsync(() =>
{ {
foreach (var completionItem in completions.ItemsList) foreach (var (index, completionItem) in completions.ItemsList.Take(100).Index())
{ {
AddCodeCompletionOption(CodeCompletionKind.Class, completionItem.DisplayText, completionItem.DisplayText); var symbolKindString = CollectionExtensions.GetValueOrDefault(completionItem.Properties, "SymbolKind");
var symbolKind = symbolKindString is null ? null : (SymbolKind?)int.Parse(symbolKindString);
var godotCompletionType = symbolKind switch
{
SymbolKind.Method => CodeCompletionKind.Function,
SymbolKind.NamedType => CodeCompletionKind.Class,
SymbolKind.Local => CodeCompletionKind.Variable,
SymbolKind.Property => CodeCompletionKind.Member,
SymbolKind.Field => CodeCompletionKind.Member,
_ => CodeCompletionKind.PlainText
};
AddCodeCompletionOption(godotCompletionType, completionItem.DisplayText, completionItem.DisplayText, value: new RefCountedContainer<CompletionItem>(completionItem));
} }
// partially working - displays menu only when caret is what CodeEdit determines as valid // partially working - displays menu only when caret is what CodeEdit determines as valid
UpdateCodeCompletionOptions(true); UpdateCodeCompletionOptions(true);

View File

@@ -26,7 +26,7 @@ public partial class SearchResultComponent : MarginContainer
private void OnButtonPressed() private void OnButtonPressed()
{ {
var fileLinePosition = new SharpIdeFileLinePosition { Line = Result.Line, Column = Result.StartColumn }; var fileLinePosition = new SharpIdeFileLinePosition { Line = Result.Line - 1, Column = Result.StartColumn - 1 };
GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(Result.File, fileLinePosition); GodotGlobalEvents.Instance.FileExternallySelected.InvokeParallelFireAndForget(Result.File, fileLinePosition);
ParentSearchWindow.Hide(); ParentSearchWindow.Hide();
} }