Move code actions to LSP#45572
Conversation
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
dibarbet
left a comment
There was a problem hiding this comment.
Have some high level comments I want to get out of the way before diving too deep.
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveData.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
| var runAsCommand = false; | ||
|
|
||
| var applyChangesOperations = operations.Where(operation => operation is ApplyChangesOperation); | ||
| if (applyChangesOperations.Any()) |
There was a problem hiding this comment.
I wonder if this should be all, or more specific filtering.
If we have an ApplyChangesOperation + DocumentNavigation operation, then it should be safe to do this (we can't tell the client to navigate anywhere anyway, so we can't do anything but drop the document navigation operation). But if there is an ApplyChangesOperation + some other actual modification operation, then we might get into a corrupt state b/c we applied half of the code action.
Not sure the best way to do this, thoughts @CyrusNajmabadi ?
| } | ||
|
|
||
| // Set up to run as command on the server instead of using WorkspaceEdits. | ||
| var commandOperations = operations.All(operation => !(operation is ApplyChangesOperation)); |
There was a problem hiding this comment.
Ah I see, we apply any non ApplyChangesOperation as a command. Then I think I'm only worried about the order. The LSP spec states that commands are applied after workspace edits. But if the code action specifies the non-ApplyChangesOperation to run before the ApplyChangesOperation, the code action could get corrupted if order matters.
There was a problem hiding this comment.
This also could be problematic - I see in the execute code actions request, we just find the one with the right title. It will end up executing all of the operations, so if there are workspace edits + a command, it will execute the edits twice (once on client workspace edits, once on server during RunCodeActions ApplyChangesOperation)
There was a problem hiding this comment.
Do you think it would be best if we were to just file a Roslyn bug on this?
I think ideally, the solution would be to include a check in RunCodeActionsHandler to see whether we are executing an ApplyChangesOperation so that we don't execute those kinds of operations twice. But we can't currently do that since some ApplyChangesOperations are applied as commands due to this bug on the LSP side. Once the LSP bug is fixed, I think we can address the concerns you mentioned.
The current approach is: if we have a mix of ApplyChangesOperations and other operations, only the ApplyChangesOperations are executed. But if we have only non-ApplyChangesOperations, then we can execute them (as a command). This isn't ideal, but it prevents workspace edits from potentially executing twice.
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
| return codeAction; | ||
|
|
||
| // Local functions | ||
| static async Task<TextDocumentEdit[]> ComputeWorkspaceEdits( |
There was a problem hiding this comment.
I think it would be cleaner for this to be something like TryComputeWorkspaceEdits that returns false when unable to compute them and a command has to be used instead.
There was a problem hiding this comment.
Can that be done when the function is async? If we return a bool, then we would need to have an out parameter for the workspace edits, but I don't think we can use out parameters in async methods. I guess we could also return a tuple of (bool, WorkspaceEdits) but that seems a bit messy.
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
| Kind = codeActionKind, | ||
| Diagnostics = request.Context.Diagnostics, | ||
| Children = nestedActions.ToArray(), | ||
| Data = new CodeActionResolveData { CodeActionParams = request, DistinctTitle = parentTitle + codeAction.Title } |
There was a problem hiding this comment.
are there cases where the nested code action titles conflict between two different nested groups? If so, I wonder if we could use the codeactionprovider type name + title instead of having to specify the entire tree (unless the title can be the same within a nested group)
we should also doc on the CodeActionResolveData what goes inside the DistinctTitle
There was a problem hiding this comment.
Hmm, I think this would fail with 'Suppress or Configure issues' if we had two diagnostics that we could suppress/configure, e.g. we would have "ConfigureSeverityCodeFixProvider" + "None" twice in this case.
|
@dibarbet I think I've addressed all your concerns via code changes or by responding to the initial feedback. |
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveData.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveData.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveData.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveData.cs
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| public override async Task<LSP.VSCodeAction> HandleRequestAsync( | ||
| LSP.VSCodeAction codeAction, |
There was a problem hiding this comment.
i'm a little confused as to the semantics here. it takes in a code action... and returns a code action? can you clarify the concept here?
There was a problem hiding this comment.
Per your suggestion I've added some details in the original post of this PR, as well as added more comments in the code.
The CodeActionsResolveHandler fills out the VSCodeAction's Edit and/or Command properties. The handler is only invoked when the user requests the code action (e.g. by hovering), so it saves unnecessary computation.
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
| return codeAction; | ||
| } | ||
|
|
||
| var codeActionToResolve = GetCodeActionToResolve(data.DistinctTitle, codeActions); |
There was a problem hiding this comment.
so to resolve a code action, we have to compute all the code actions again? seems costly. can/should we store/save things locally across calls to speed this up?
There was a problem hiding this comment.
One suggestion I would have is to do the caching in a follow-up pr. This matches the current behavior in nexus (always recompute) so we're not regressing anything.
This pr is already fairly big, and caching I think will be a little complex and would prefer to review separately.
There was a problem hiding this comment.
i am ok with that decision for this PR. :)
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs
Outdated
Show resolved
Hide resolved
| codeRefactorings.SelectMany(r => r.CodeActions.Select(ca => ca.action))); | ||
| // Special case (also dealt with specially in local Roslyn): | ||
| // If we have configure/suppress code actions, combine them under one top-level code action. | ||
| var configureSuppressActions = codeFixes.Where(a => a.Action is AbstractConfigurationActionWithNestedActions); |
There was a problem hiding this comment.
| var configureSuppressActions = codeFixes.Where(a => a.Action is AbstractConfigurationActionWithNestedActions); | |
| var configureSuppressActions = codeFixes.OfType<AbstractConfigurationActionWithNestedActions>(); |
There was a problem hiding this comment.
Will this work since we wanted to check the type of a.Action and not just a?
| // Flatten out the nested codeactions. | ||
| var nestedCodeActions = codeActions.Where(c => c is CodeAction.CodeActionWithNestedActions nc && nc.IsInlinable).SelectMany(nc => nc.NestedCodeActions); | ||
| codeActions = codeActions.Where(c => !(c is CodeAction.CodeActionWithNestedActions)).Concat(nestedCodeActions); | ||
| return results.ToArray(); |
There was a problem hiding this comment.
this also feels like it' reimplementing logic that must exist elsewhere. that doesn't seem desirable. i would far prefer to have a single location where we sort/order these things, and then use it from VS and LSP.
src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
|
Done with pass. I have some moderate concerns, but nothing critically blocking. We could create work items to track some things if they are too large for this PR. i have a general deep concern that we're being too loosy goosy with just papering over things that seem like they would be bugs, and that that could lead to a very frustrating user experience where things don't work and users don't know why. |
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs
Outdated
Show resolved
Hide resolved
| () => codeRefactoringService.GetRefactoringsAsync(document, textSpan, cancellationToken)); | ||
|
|
||
| await Task.WhenAll(codeFixCollectionsTask, codeRefactoringsTask).ConfigureAwait(false); | ||
| return (await codeFixCollectionsTask.ConfigureAwait(false), await codeRefactoringsTask.ConfigureAwait(false)); |
There was a problem hiding this comment.
I think you don't need WhenAll here since you await the completion of the tasks before returning.
There was a problem hiding this comment.
I discussed this with Cyrus recently and was told that it would probably be good to keep the WhenAll in here. I think the logic was that if both codeFixCollectionsTask and codeRefactoringsTask error or cancel, then using WhenAll allows us to retrieve both messages, while if we awaited them separately then we might only get one.
There was a problem hiding this comment.
Allison is correct. It's an extremely subtle thing and easy to make a mistake around. However, we can lose exceptions if we kick off tasks and then await each one serially. then WhenAll ensures we properly see all of this, and doesn't hurt anything in terms of perf. i.e. the later awaits will be no-ops because they will precheck if the tasks are complete and will grab the values if so.
There was a problem hiding this comment.
Ah interesting, that behavior makes sense now that I think about it. Thanks for the explanation!
src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs
Outdated
Show resolved
Hide resolved
|
|
||
| var changedDocuments = projectChanges.SelectMany(pc => pc.GetChangedDocuments()); | ||
| var changedAnalyzerConfigDocuments = projectChanges.SelectMany(pc => pc.GetChangedAnalyzerConfigDocuments()); | ||
| var changedAdditionalDocuments = projectChanges.SelectMany(pc => pc.GetChangedAdditionalDocuments()); |
There was a problem hiding this comment.
I think adding / removing documents via LSP workspace edits should be supported, but there are bugs in the LSP client preventing it from working
@allisonchou I think right now if we have to execute these actions on the server, let's not even implement the bit turning them into LSP edits. Just do a simple check to see if there are any changes at all outside the current document and return a command to execute on the server.
Once they fix the bugs we can followup and do the actual implementation to ensure add / remove documents works correctly.
| // Changed analyzer config documents | ||
| // We won't get any results until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ | ||
| // is fixed. | ||
| await AddTextDocumentEdits( |
There was a problem hiding this comment.
See above, lets not do this in this PR. Can followup once the LSP client bug is fixed.
There was a problem hiding this comment.
The issue with changed documents should now be fixed due to a change on the Liveshare side (hooray).
There's a bug on the Roslyn side when modifying editorconfig documents (#44331 is the PR fix), but it should hopefully be merged in soon.
Still pending is the work to support adding documents via code actions in LSP.
src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionResolveTests.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs
Outdated
Show resolved
Hide resolved
src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs
Outdated
Show resolved
Hide resolved
src/VisualStudio/LiveShare/Impl/Microsoft.VisualStudio.LanguageServices.LiveShare.csproj
Outdated
Show resolved
Hide resolved
|
@CyrusNajmabadi @dibarbet Would it be possible to get another look at this PR? I believe all feedback has been addressed. Outstanding bugs:
Currently, the future work items on the Roslyn side are:
|
Moves code actions over to LSP.
How it works:
There are three main handlers involved in processing LSP code actions:
CodeActionsHandler,CodeActionResolveHandler, andRunCodeActionsHandler.CodeActionsHandler: Deals with the initial code actions request from LSP. Returns an array ofVSCodeActionsgenerated from Roslyn code actions. TheEditandCommandproperties in theVSCodeActionare left blank, since those are populated in theCodeActionResolveHandler. We avoid filling out those two properties so we don't need to a bunch of work unless the user actually requests it.CodeActionResolveHandler: Invoked when a user hovers over a code action. We leave the bulk of the processing to this handler. Fills out either theEditorCommandproperties (can be both as well, but due to the LSP bug below this isn't currently possible). If we send back anEdit, the edit is processed and executed on the client/LSP side. If we send back aCommand, the command will eventually make its way to theRunCodeActionsHandlerand run from there.RunCodeActionsHandler: Invoked whenCommandproperty is filled out. We run actions as commands if they can't be executed as WorkspaceEdits for some reason (i.e. if not an ApplyChangesOperation, or due to the LSP bug below). These commands are executed on the local Roslyn side. The generalExecuteWorkspaceCommandHandleris called first. The general handler then calls into our code-action specificRunCodeActionsHandler.Code actions that add a document must currently be executed on the server side due to this LSP bug: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
eta: thanks to David for all the help on this PR!