// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable enable using System; using System.Collections.Generic; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json.Linq; using Roslyn.Utilities; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler { /// /// Resolves a code action by filling out its Edit and/or Command property. /// The handler is triggered only when a user hovers over a code action. This /// system allows the basic code action data to be computed quickly, and the /// complex data, such as edits and commands, to be computed only when necessary /// (i.e. when hovering/previewing a code action). /// [ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName), Shared] internal class CodeActionResolveHandler : AbstractRequestHandler { private readonly ICodeFixService _codeFixService; private readonly ICodeRefactoringService _codeRefactoringService; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CodeActionResolveHandler( ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, ILspSolutionProvider solutionProvider) : base(solutionProvider) { _codeFixService = codeFixService; _codeRefactoringService = codeRefactoringService; } public override async Task HandleRequestAsync(LSP.VSCodeAction codeAction, RequestContext context, CancellationToken cancellationToken) { var data = ((JToken)codeAction.Data).ToObject(); var document = SolutionProvider.GetDocument(data.TextDocument, context.ClientName); Contract.ThrowIfNull(document); var codeActions = await CodeActionHelpers.GetCodeActionsAsync( document, _codeFixService, _codeRefactoringService, data.Range, cancellationToken).ConfigureAwait(false); var codeActionToResolve = CodeActionHelpers.GetCodeActionToResolve( data.UniqueIdentifier, codeActions); Contract.ThrowIfNull(codeActionToResolve); var operations = await codeActionToResolve.GetOperationsAsync(cancellationToken).ConfigureAwait(false); if (operations.IsEmpty) { return codeAction; } // If we have all non-ApplyChangesOperations, set up to run as command on the server // instead of using WorkspaceEdits. if (operations.All(operation => !(operation is ApplyChangesOperation))) { codeAction.Command = SetCommand(codeAction.Title, data); return codeAction; } // TO-DO: We currently must execute code actions which add new documents on the server as commands, // since there is no LSP support for adding documents yet. In the future, we should move these actions // to execute on the client. // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ // Add workspace edits var applyChangesOperations = operations.OfType(); if (applyChangesOperations.Any()) { using var _ = ArrayBuilder.GetInstance(out var textDocumentEdits); foreach (var applyChangesOperation in applyChangesOperations) { var solution = document.Project.Solution; var changes = applyChangesOperation.ChangedSolution.GetChanges(solution); var projectChanges = changes.GetProjectChanges(); // TO-DO: If the change involves adding or removing a document, execute via command instead of WorkspaceEdit // until adding/removing documents is supported in LSP: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ // After support is added, remove the below if-statement and add code to support adding/removing documents. var addedDocuments = projectChanges.SelectMany( pc => pc.GetAddedDocuments().Concat(pc.GetAddedAdditionalDocuments().Concat(pc.GetAddedAnalyzerConfigDocuments()))); var removedDocuments = projectChanges.SelectMany( pc => pc.GetRemovedDocuments().Concat(pc.GetRemovedAdditionalDocuments().Concat(pc.GetRemovedAnalyzerConfigDocuments()))); if (addedDocuments.Any() || removedDocuments.Any()) { codeAction.Command = SetCommand(codeAction.Title, data); return codeAction; } // TO-DO: If the change involves adding or removing a project reference, execute via command instead of // WorkspaceEdit until adding/removing project references is supported in LSP: // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1166040 var projectReferences = projectChanges.SelectMany( pc => pc.GetAddedProjectReferences().Concat(pc.GetRemovedProjectReferences())); if (projectReferences.Any()) { codeAction.Command = SetCommand(codeAction.Title, data); return codeAction; } var changedDocuments = projectChanges.SelectMany(pc => pc.GetChangedDocuments()); var changedAnalyzerConfigDocuments = projectChanges.SelectMany(pc => pc.GetChangedAnalyzerConfigDocuments()); var changedAdditionalDocuments = projectChanges.SelectMany(pc => pc.GetChangedAdditionalDocuments()); // Changed documents await AddTextDocumentEdits( textDocumentEdits, applyChangesOperation, solution, changedDocuments, applyChangesOperation.ChangedSolution.GetDocument, solution.GetDocument, cancellationToken).ConfigureAwait(false); // Changed analyzer config documents await AddTextDocumentEdits( textDocumentEdits, applyChangesOperation, solution, changedAnalyzerConfigDocuments, applyChangesOperation.ChangedSolution.GetAnalyzerConfigDocument, solution.GetAnalyzerConfigDocument, cancellationToken).ConfigureAwait(false); // Changed additional documents await AddTextDocumentEdits( textDocumentEdits, applyChangesOperation, solution, changedAdditionalDocuments, applyChangesOperation.ChangedSolution.GetAdditionalDocument, solution.GetAdditionalDocument, cancellationToken).ConfigureAwait(false); } codeAction.Edit = new LSP.WorkspaceEdit { DocumentChanges = textDocumentEdits.ToArray() }; } return codeAction; // Local functions static LSP.Command SetCommand(string title, CodeActionResolveData data) => new LSP.Command { CommandIdentifier = CodeActionsHandler.RunCodeActionCommandName, Title = title, Arguments = new object[] { data } }; static async Task AddTextDocumentEdits( ArrayBuilder textDocumentEdits, ApplyChangesOperation applyChangesOperation, Solution solution, IEnumerable changedDocuments, Func getNewDocumentFunc, Func getOldDocumentFunc, CancellationToken cancellationToken) where T : TextDocument { foreach (var docId in changedDocuments) { var newDoc = getNewDocumentFunc(docId); var oldDoc = getOldDocumentFunc(docId); Contract.ThrowIfNull(oldDoc); Contract.ThrowIfNull(newDoc); var oldText = await oldDoc.GetTextAsync(cancellationToken).ConfigureAwait(false); var newText = await newDoc.GetTextAsync(cancellationToken).ConfigureAwait(false); var textChanges = newText.GetTextChanges(oldText); var edits = textChanges.Select(tc => ProtocolConversions.TextChangeToTextEdit(tc, oldText)).ToArray(); var documentIdentifier = new VersionedTextDocumentIdentifier { Uri = newDoc.GetURI() }; textDocumentEdits.Add(new TextDocumentEdit { TextDocument = documentIdentifier, Edits = edits.ToArray() }); } } } } }