Skip to content

Commit

Permalink
LSP: update OmniSharpCodeHandler so we support codeAction/resolve
Browse files Browse the repository at this point in the history
This feature was introduced in LSP
3.16.0 https://microsoft.github.io/language-server-protocol/specification#codeAction_resolve
and allows us to drop the hack where we were introducing a special
"omnisharp/executeCodeAction" command to delay the resolution of code
action changesets until user actually selets that code
action: see OmniSharp#1814.

Related to:
- OmniSharp#2068
- OmniSharp#1814
  • Loading branch information
Saulius Menkevicius authored and razzmatazz committed May 12, 2021
1 parent aa1018c commit 102a0de
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,71 +1,59 @@
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.JsonRpc;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
using OmniSharp.Models.V2.CodeActions;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
using OmniSharp.Models;
using Diagnostic = OmniSharp.Extensions.LanguageServer.Protocol.Models.Diagnostic;
using OmniSharp.Models.V2.CodeActions;

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)
{
var codeActionCaps = _server.ClientSettings.Capabilities.TextDocument.CodeAction.Value;
bool clientCanResolveEditProp = codeActionCaps?.ResolveSupport?.Properties.Contains("edit") ?? false;

var omnisharpRequest = new GetCodeActionsRequest
{
FileName = Helpers.FromUri(request.TextDocument.Uri),
Expand All @@ -87,40 +75,44 @@ public override async Task<CommandOrCodeActionContainer> Handle(CodeActionParams
else if (ca.Identifier.StartsWith("Change ")) { kind = CodeActionKind.QuickFix; }
else { kind = CodeActionKind.Refactor; }

codeActions.Add(
new CodeAction
{
Title = ca.Name,
Kind = kind,
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 }
});
var codeAction = new CodeAction {
Title = ca.Name,
Kind = kind,
Diagnostics = new Container<Diagnostic>(),
Edit = null,
Data = JObject.FromObject(
new CommandData()
{
Uri = request.TextDocument.Uri,
Identifier = ca.Identifier,
Name = ca.Name,
Range = request.Range,
})
};

if (!clientCanResolveEditProp)
{
var codeActionResolution = await this.Handle(codeAction, cancellationToken);

codeAction = codeAction with {
Edit = codeActionResolution.Edit,
Data = null,
};
}

codeActions.Add(codeAction);
}

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

public override Task<CodeAction> Handle(CodeAction request, CancellationToken cancellationToken)
public override async Task<CodeAction> Handle(CodeAction request, CancellationToken cancellationToken)
{
return Task.FromResult(request);
}
var data = request.Data.ToObject<CommandData>();

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

var omnisharpCaRequest = new RunCodeActionRequest {
var omnisharpCaRequest = new RunCodeActionRequest
{
Identifier = data.Identifier,
FileName = data.Uri.GetFileSystemPath(),
Column = data.Range.Start.Character,
Expand All @@ -139,19 +131,16 @@ public async Task<Unit> Handle(ExecuteCommandParams request, CancellationToken c
_server.ClientSettings.Capabilities.Workspace!.WorkspaceEdit.Value,
_documentVersions
);
;

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

// Do something with response?
//if (response.Applied)
Edit = edit,
};
}
else
{
return new CodeAction();
}

return Unit.Value;
}

class CommandData
Expand All @@ -162,12 +151,6 @@ class CommandData
public Range Range { get; set;}
}

ExecuteCommandRegistrationOptions IRegistration<ExecuteCommandRegistrationOptions, ExecuteCommandCapability>.GetRegistrationOptions(ExecuteCommandCapability capability, ClientCapabilities clientCapabilities)
{
_executeCommandCapability = capability;
return _executeCommandRegistrationOptions;
}

protected override CodeActionRegistrationOptions CreateRegistrationOptions(CodeActionCapability capability, ClientCapabilities clientCapabilities)
{
return new CodeActionRegistrationOptions()
Expand All @@ -177,6 +160,7 @@ protected override CodeActionRegistrationOptions CreateRegistrationOptions(CodeA
CodeActionKind.SourceOrganizeImports,
CodeActionKind.Refactor,
CodeActionKind.RefactorExtract),
ResolveProvider = true,
};
}
}
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 @@ -381,7 +381,7 @@ internal static void RegisterHandlers(ILanguageServer server, CompositionHost co
.Concat(OmniSharpReferencesHandler.Enumerate(handlers))
.Concat(OmniSharpImplementationHandler.Enumerate(handlers))
.Concat(OmniSharpCodeLensHandler.Enumerate(handlers))
.Concat(OmniSharpCodeActionHandler.Enumerate(handlers, serializer, server, documentVersions))
.Concat(OmniSharpCodeActionHandler.Enumerate(handlers, server, documentVersions))
.Concat(OmniSharpDocumentFormattingHandler.Enumerate(handlers))
.Concat(OmniSharpDocumentFormatRangeHandler.Enumerate(handlers))
.Concat(OmniSharpDocumentOnTypeFormattingHandler.Enumerate(handlers)))
Expand Down
55 changes: 31 additions & 24 deletions tests/OmniSharp.Lsp.Tests/OmniSharpCodeActionHandlerFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
Expand Down Expand Up @@ -228,14 +229,19 @@ await Configuration.Update("omnisharp",
var project = await AddProjectToWorkspace(testProject);
var document = project.Documents.First();

await Client.ExecuteCommand(Command.Create("omnisharp/executeCodeAction")
.WithArguments(new
{
var resolved = await Client.ResolveCodeAction(new CodeAction
{
Title = "N/A",
Data = JObject.FromObject(new {
Uri = DocumentUri.FromFileSystemPath(document.FilePath),
Identifier = "Generate class 'Z' in new file",
Name = "N/A",
Range = new Range((8, 12), (8, 12)),
}), CancellationToken);
})
});

await Server.Workspace.ApplyWorkspaceEdit(
new ApplyWorkspaceEditParams { Edit = resolved.Edit });

var updatedDocument = OmniSharpTestHost.Workspace.GetDocument(Path.Combine(Path.GetDirectoryName(document.FilePath), "Z.cs"));
var updateDocumentText = await updatedDocument.GetTextAsync(CancellationToken);
Expand All @@ -258,14 +264,19 @@ 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
{
var resolved = await Client.ResolveCodeAction(new CodeAction
{
Title = "N/A",
Data = JObject.FromObject(new {
Uri = DocumentUri.FromFileSystemPath(document.FilePath),
Identifier = "Rename file to Class1.cs",
Name = "N/A",
Range = new Range((4, 10), (4, 10)),
}), CancellationToken);
})
});

await Server.Workspace.ApplyWorkspaceEdit(
new ApplyWorkspaceEditParams { Edit = resolved.Edit });

Assert.Empty(OmniSharpTestHost.Workspace.GetDocuments(document.FilePath));

Expand All @@ -281,9 +292,18 @@ private async Task<IEnumerable<TestFile>> RunRefactoringAsync(string code, strin
configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled));
Assert.Contains(refactoringName, refactorings.Select(x => x.Title), StringComparer.OrdinalIgnoreCase);

var command = refactorings
.First(action => action.Title.Equals(refactoringName, StringComparison.OrdinalIgnoreCase)).Command;
return await RunRefactoringsAsync(code, command);
var codeAction = refactorings.First(action => action.Title.Equals(refactoringName, StringComparison.OrdinalIgnoreCase));

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

OmniSharpTestHost.AddFilesToWorkspace(testFile);

await Server.Workspace.ApplyWorkspaceEdit(
new ApplyWorkspaceEditParams { Edit = codeAction.Edit });

return new[] {testFile};
}

private async Task<IEnumerable<string>> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true)
Expand Down Expand Up @@ -315,19 +335,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

0 comments on commit 102a0de

Please sign in to comment.