Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LSP CodeAction resolve #2467

Merged
merged 7 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,43 @@
using OmniSharpCodeActionKind = OmniSharp.Models.V2.CodeActions.CodeActionKind;
using System;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
using NuGet.Protocol;

namespace OmniSharp.LanguageServerProtocol.Handlers
{
internal sealed class OmniSharpCodeActionHandler : CodeActionHandlerBase, IExecuteCommandHandler
internal sealed class OmniSharpCodeActionHandler : CodeActionHandlerBase
{
public static IEnumerable<IJsonRpcHandler> Enumerate(
RequestHandlers handlers,
ISerializer serializer,
ILanguageServer mediator,
DocumentVersions versions)
{
foreach (var (selector, getActionsHandler, runActionHandler) in handlers
.OfType<Mef.IRequestHandler<GetCodeActionsRequest, GetCodeActionsResponse>,
Mef.IRequestHandler<RunCodeActionRequest, RunCodeActionResponse>>())
{
yield return new OmniSharpCodeActionHandler(getActionsHandler, runActionHandler, selector, serializer, mediator, versions);
yield return new OmniSharpCodeActionHandler(getActionsHandler, runActionHandler, selector, mediator, versions);
}
}

private readonly Mef.IRequestHandler<GetCodeActionsRequest, GetCodeActionsResponse> _getActionsHandler;
private readonly ExecuteCommandRegistrationOptions _executeCommandRegistrationOptions;
private ExecuteCommandCapability _executeCommandCapability;
private Mef.IRequestHandler<RunCodeActionRequest, RunCodeActionResponse> _runActionHandler;
private readonly Mef.IRequestHandler<RunCodeActionRequest, RunCodeActionResponse> _runActionHandler;
private readonly DocumentSelector _documentSelector;
private readonly ISerializer _serializer;
private readonly ILanguageServer _server;
private readonly DocumentVersions _documentVersions;

public OmniSharpCodeActionHandler(
Mef.IRequestHandler<GetCodeActionsRequest, GetCodeActionsResponse> getActionsHandler,
Mef.IRequestHandler<RunCodeActionRequest, RunCodeActionResponse> runActionHandler,
DocumentSelector documentSelector,
ISerializer serializer,
ILanguageServer server,
DocumentVersions documentVersions)
{
_getActionsHandler = getActionsHandler;
_runActionHandler = runActionHandler;
_documentSelector = documentSelector;
_serializer = serializer;
_server = server;
_documentVersions = documentVersions;
_executeCommandRegistrationOptions = new ExecuteCommandRegistrationOptions()
{
Commands = new Container<string>("omnisharp/executeCodeAction"),
};
}

public override async Task<CommandOrCodeActionContainer> Handle(CodeActionParams request, CancellationToken cancellationToken)
Expand All @@ -82,41 +73,32 @@ public override async Task<CommandOrCodeActionContainer> Handle(CodeActionParams

foreach (var ca in omnisharpResponse.CodeActions)
{

codeActions.Add(
new CodeAction
{
Title = ca.Name,
Kind = OmniSharpCodeActionHandler.FromOmniSharpCodeActionKind(ca.CodeActionKind),
Diagnostics = new Container<Diagnostic>(),
Edit = new WorkspaceEdit(),
Command = Command.Create("omnisharp/executeCodeAction")
.WithArguments(new CommandData()
{
Uri = request.TextDocument.Uri,
Identifier = ca.Identifier,
Name = ca.Name,
Range = request.Range,
})
with { Title = ca.Name }
Data = new CommandData()
{
Uri = request.TextDocument.Uri,
Identifier = ca.Identifier,
Name = ca.Name,
Range = request.Range,
}.ToJToken()
});
}

return new CommandOrCodeActionContainer(
codeActions.Select(ca => new CommandOrCodeAction(ca)));
}

public override Task<CodeAction> Handle(CodeAction request, CancellationToken cancellationToken)
{
return Task.FromResult(request);
}

public async Task<Unit> Handle(ExecuteCommandParams request, CancellationToken cancellationToken)
public override async Task<CodeAction> Handle(CodeAction request, CancellationToken cancellationToken)
{
Debug.Assert(request.Command == "omnisharp/executeCodeAction");
var data = request.ExtractArguments<CommandData>(_serializer);
var data = request.Data.FromJToken<CommandData>();

var omnisharpCaRequest = new RunCodeActionRequest {
var omnisharpCaRequest = new RunCodeActionRequest
{
Identifier = data.Identifier,
FileName = data.Uri.GetFileSystemPath(),
Column = data.Range.Start.Character,
Expand All @@ -128,40 +110,26 @@ public async Task<Unit> Handle(ExecuteCommandParams request, CancellationToken c
};

var omnisharpCaResponse = await _runActionHandler.Handle(omnisharpCaRequest);
if (omnisharpCaResponse.Changes != null)
if (omnisharpCaResponse.Changes == null)
{
var edit = Helpers.ToWorkspaceEdit(
omnisharpCaResponse.Changes,
_server.ClientSettings.Capabilities.Workspace!.WorkspaceEdit.Value,
_documentVersions
);
;

await _server.Workspace.ApplyWorkspaceEdit(new ApplyWorkspaceEditParams()
{
Label = data.Name,
Edit = edit
}, cancellationToken);

// Do something with response?
//if (response.Applied)
return request with { Edit = new WorkspaceEdit() };
}

return Unit.Value;
}
var edit = Helpers.ToWorkspaceEdit(
omnisharpCaResponse.Changes,
_server.ClientSettings.Capabilities.Workspace!.WorkspaceEdit.Value,
_documentVersions
);

class CommandData
{
public DocumentUri Uri { get; set;}
public string Identifier { get; set;}
public string Name { get; set;}
public Range Range { get; set;}
return request with { Edit = edit };
}

ExecuteCommandRegistrationOptions IRegistration<ExecuteCommandRegistrationOptions, ExecuteCommandCapability>.GetRegistrationOptions(ExecuteCommandCapability capability, ClientCapabilities clientCapabilities)
class CommandData
{
_executeCommandCapability = capability;
return _executeCommandRegistrationOptions;
public DocumentUri Uri { get; set; }
public string Identifier { get; set; }
public string Name { get; set; }
public Range Range { get; set; }
}

private static CodeActionKind FromOmniSharpCodeActionKind(string omnisharpCodeAction)
Expand All @@ -179,6 +147,7 @@ protected override CodeActionRegistrationOptions CreateRegistrationOptions(CodeA
return new CodeActionRegistrationOptions()
{
DocumentSelector = _documentSelector,
ResolveProvider = true,
CodeActionKinds = new Container<CodeActionKind>(
CodeActionKind.SourceOrganizeImports,
CodeActionKind.Refactor,
Expand Down
2 changes: 1 addition & 1 deletion src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ internal static void RegisterHandlers(ILanguageServer server, CompositionHost co
var serializer = server.Services.GetRequiredService<ISerializer>();
server.Register(s =>
{
foreach (var handler in OmniSharpCodeActionHandler.Enumerate(handlers, serializer, server, documentVersions)
foreach (var handler in OmniSharpCodeActionHandler.Enumerate(handlers, server, documentVersions)
.Concat(OmniSharpCodeLensHandler.Enumerate(handlers))
.Concat(OmniSharpCompletionHandler.Enumerate(handlers))
.Concat(OmniSharpDefinitionHandler.Enumerate(handlers))
Expand Down
130 changes: 82 additions & 48 deletions tests/OmniSharp.Lsp.Tests/OmniSharpCodeActionHandlerFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
using OmniSharp.Models.V2;
using TestUtility;
using Xunit;
Expand Down Expand Up @@ -80,12 +79,10 @@ public async Task Can_remove_unnecessary_usings(bool roslynAnalyzersEnabled)
using MyNamespace2;
using System;
u[||]sing MyNamespace1;

public class c {public c() {Guid.NewGuid();}}";

const string expected =
@"using System;

public class c {public c() {Guid.NewGuid();}}";

var response =
Expand Down Expand Up @@ -235,24 +232,35 @@ await Configuration.Update("omnisharp",
var project = await AddProjectToWorkspace(testProject);
var document = project.Documents.First();

await Client.ExecuteCommand(Command.Create("omnisharp/executeCodeAction")
.WithArguments(new
{
Uri = DocumentUri.FromFileSystemPath(document.FilePath),
Identifier = "Generate class 'Z' in new file",
Name = "N/A",
Range = new Range((8, 12), (8, 12)),
}), CancellationToken);
var codeActions = await Client.RequestCodeAction(new CodeActionParams()
{
Context = new CodeActionContext(),
TextDocument = new TextDocumentIdentifier(DocumentUri.FromFileSystemPath(document.FilePath)),
Range = new Range((8, 12), (8, 12)),
}, CancellationToken);

var codeActionOrCommand = codeActions
.SingleOrDefault(ca => ca.CodeAction.Title == "Generate type 'Z' -> Generate class 'Z' in new file");

Assert.NotNull(codeActionOrCommand);
Assert.True(codeActionOrCommand.IsCodeAction);

var updatedDocument = OmniSharpTestHost.Workspace.GetDocument(Path.Combine(Path.GetDirectoryName(document.FilePath), "Z.cs"));
var updateDocumentText = await updatedDocument.GetTextAsync(CancellationToken);
var codeAction = codeActionOrCommand.CodeAction;

Assert.Equal(@"namespace ConsoleApplication
var resolvedCodeAction = await Client.ResolveCodeAction(codeAction, CancellationToken);

var change = resolvedCodeAction.Edit.DocumentChanges.SingleOrDefault();
Assert.True(change.IsTextDocumentEdit);

var textEdit = change.TextDocumentEdit.Edits.SingleOrDefault();

const string expected = @"namespace ConsoleApplication
{
internal class Z
{
}
}".Replace("\r\n", "\n"), updateDocumentText.ToString());
}";
Assert.Equal(expected.Replace("\r\n", "\n"), textEdit.NewText);
}

[Theory]
Expand All @@ -265,32 +273,71 @@ public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames
var project = await AddProjectToWorkspace(testProject);
var document = project.Documents.First();

await Client.ExecuteCommand(Command.Create("omnisharp/executeCodeAction")
.WithArguments(new
{
Uri = DocumentUri.FromFileSystemPath(document.FilePath),
Identifier = "Rename file to Class1.cs",
Name = "N/A",
Range = new Range((4, 10), (4, 10)),
}), CancellationToken);
var codeActions = await Client.RequestCodeAction(new CodeActionParams()
{
Context = new CodeActionContext(),
TextDocument = new TextDocumentIdentifier(DocumentUri.FromFileSystemPath(document.FilePath)),
Range = new Range((4, 10), (4, 10)),
}, CancellationToken);

Assert.Empty(OmniSharpTestHost.Workspace.GetDocuments(document.FilePath));
var codeActionOrCommand = codeActions
.SingleOrDefault(ca => ca.CodeAction.Title == "Rename file to Class1.cs");

Assert.NotEmpty(OmniSharpTestHost.Workspace.GetDocuments(
Path.Combine(Path.GetDirectoryName(document.FilePath), "Class1.cs")
));
Assert.NotNull(codeActionOrCommand);
Assert.True(codeActionOrCommand.IsCodeAction);

var codeAction = codeActionOrCommand.CodeAction;

var resolvedCodeAction = await Client.ResolveCodeAction(codeAction, CancellationToken);

var change = resolvedCodeAction.Edit.DocumentChanges.SingleOrDefault();

Assert.True(change.IsRenameFile);

var expected = DocumentUri.FromFileSystemPath(Path.Combine(Path.GetDirectoryName(document.FilePath), "Class1.cs"));
Assert.Equal(expected.GetFileSystemPath(), change.RenameFile.NewUri.GetFileSystemPath());
}

private async Task<IEnumerable<TestFile>> RunRefactoringAsync(string code, string refactoringName,
bool isAnalyzersEnabled = true)
private async Task<IEnumerable<TestFile>> RunRefactoringAsync(string code, string refactoringName, bool isAnalyzersEnabled = true)
{
var refactorings = await FindRefactoringsAsync(code,
configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled));
Assert.Contains(refactoringName, refactorings.Select(x => x.Title), StringComparer.OrdinalIgnoreCase);
await Restart(TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled));

var bufferPath =
$"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer.cs";
var testFile = new TestFile(bufferPath, code);
OmniSharpTestHost.AddFilesToWorkspace(testFile);

var project = OmniSharpTestHost.Workspace.CurrentSolution.Projects.Single();
var document = project.Documents.First();

var span = testFile.Content.GetSpans().Single();
var range = GetSelection(testFile.Content.GetRangeFromSpan(span));

// Request CodeAction
var codeActions = await Client.RequestCodeAction(new CodeActionParams()
{
Context = new CodeActionContext(),
TextDocument = new TextDocumentIdentifier(DocumentUri.FromFileSystemPath(document.FilePath)),
Range = LanguageServerProtocol.Helpers.ToRange(range),
}, CancellationToken);

// Locate CodeAction
var codeAction = codeActions
.Where(ca => ca.IsCodeAction)
.Select(ca => ca.CodeAction)
.SingleOrDefault(ca => ca.Title.Equals(refactoringName, StringComparison.OrdinalIgnoreCase));

// Resolve CodeAction
var resolvedCodeAction = await Client.ResolveCodeAction(codeAction, CancellationToken);

// Apply CodeAction
await Server.SendRequest(new ApplyWorkspaceEditParams()
{
Label = codeAction.Title,
Edit = resolvedCodeAction.Edit
}, CancellationToken);

var command = refactorings
.First(action => action.Title.Equals(refactoringName, StringComparison.OrdinalIgnoreCase)).Command;
return await RunRefactoringsAsync(code, command);
return new[] { testFile };
}

private async Task<IEnumerable<string>> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true)
Expand Down Expand Up @@ -322,19 +369,6 @@ private async Task<IEnumerable<CodeAction>> FindRefactoringsAsync(string code,
return response.Where(z => z.IsCodeAction).Select(z => z.CodeAction);
}

private async Task<IEnumerable<TestFile>> RunRefactoringsAsync(string code, Command command)
{
var bufferPath =
$"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer.cs";
var testFile = new TestFile(bufferPath, code);

OmniSharpTestHost.AddFilesToWorkspace(testFile);

await Client.Workspace.ExecuteCommand(command, CancellationToken);

return new[] { testFile };
}

private static Models.V2.Range GetSelection(TextRange range)
{
if (range.IsEmpty)
Expand Down