better code completion apply
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user