From 18779c125323c8fe04ef8a3f2a1bae63c5b82b19 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 4 Nov 2024 15:59:37 +1100 Subject: [PATCH] Expose code actions to Razor cohosting --- .../Razor/FormatNewFileHandler.cs | 8 +- .../Razor/SimplifyMethodHandler.cs | 9 ++- .../CodeActions/CodeActionResolveHandler.cs | 3 +- .../CodeActions/CodeActionResolveHelper.cs | 24 ++++-- .../Razor/Cohost/Handlers/CodeActions.cs | 81 +++++++++++++++++++ 5 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 src/Tools/ExternalAccess/Razor/Cohost/Handlers/CodeActions.cs diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs index 1036b6cafa5dc..fb8bdf4533e68 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/FormatNewFileHandler.cs @@ -59,6 +59,12 @@ public FormatNewFileHandler(IGlobalOptionService globalOptions) var document = solution.GetRequiredDocument(documentId); + return await GetFormattedNewFileContentAsync(document, cancellationToken).ConfigureAwait(false); + } + + internal static async Task GetFormattedNewFileContentAsync(Document document, CancellationToken cancellationToken) + { + var project = document.Project; // Run the new document formatting service, to make sure the right namespace type is used, among other things var formattingService = document.GetLanguageService(); if (formattingService is not null) @@ -79,7 +85,7 @@ public FormatNewFileHandler(IGlobalOptionService globalOptions) // Now format the document so indentation etc. is correct var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); - root = Formatter.Format(root, solution.Services, syntaxFormattingOptions, cancellationToken); + root = Formatter.Format(root, project.Solution.Services, syntaxFormattingOptions, cancellationToken); return root.ToFullString(); } diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/SimplifyMethodHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/SimplifyMethodHandler.cs index 7d547f461e083..f9eae08e54024 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/SimplifyMethodHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/SimplifyMethodHandler.cs @@ -43,9 +43,16 @@ public SimplifyMethodHandler() if (originalDocument is null) return null; + var textEdit = request.TextEdit; + + return await GetSimplifiedEditsAsync(originalDocument, textEdit, cancellationToken).ConfigureAwait(false); + } + + internal static async Task GetSimplifiedEditsAsync(Document originalDocument, TextEdit textEdit, CancellationToken cancellationToken) + { // Create a temporary syntax tree that includes the text edit. var originalSourceText = await originalDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); - var pendingChange = ProtocolConversions.TextEditToTextChange(request.TextEdit, originalSourceText); + var pendingChange = ProtocolConversions.TextEditToTextChange(textEdit, originalSourceText); var newSourceText = originalSourceText.WithChanges(pendingChange); var originalTree = await originalDocument.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var newTree = originalTree.WithChangedText(newSourceText); diff --git a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs index be6077f5ed3b7..06a2c81bc987d 100644 --- a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs +++ b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHandler.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.Host.Mef; @@ -97,7 +96,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeAction request) return codeAction; } - private static CodeActionResolveData GetCodeActionResolveData(LSP.CodeAction request) + internal static CodeActionResolveData GetCodeActionResolveData(LSP.CodeAction request) { var resolveData = JsonSerializer.Deserialize((JsonElement)request.Data!, ProtocolConversions.LspJsonSerializerOptions); Contract.ThrowIfNull(resolveData, "Missing data for code action resolve request"); diff --git a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs index cec1f54f679ab..88b7247a1cd16 100644 --- a/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs +++ b/src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs @@ -20,11 +20,22 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions { internal class CodeActionResolveHelper { - public static async Task GetCodeActionResolveEditsAsync(RequestContext context, CodeActionResolveData data, ImmutableArray operations, CancellationToken cancellationToken) + public static Task GetCodeActionResolveEditsAsync(RequestContext context, CodeActionResolveData data, ImmutableArray operations, CancellationToken cancellationToken) { var solution = context.Solution; Contract.ThrowIfNull(solution); + return GetCodeActionResolveEditsAsync( + solution, + data, + operations, + context.GetRequiredClientCapabilities().Workspace?.WorkspaceEdit?.ResourceOperations ?? [], + context.TraceInformation, + cancellationToken); + } + + public static async Task GetCodeActionResolveEditsAsync(Solution solution, CodeActionResolveData data, ImmutableArray operations, ResourceOperationKind[] resourceOperations, Action logFunction, CancellationToken cancellationToken) + { // 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. @@ -45,7 +56,7 @@ internal class CodeActionResolveHelper // only apply the portions of their work that updates documents, and nothing else. if (option is not ApplyChangesOperation applyChangesOperation) { - context.TraceInformation($"Skipping code action operation for '{data.UniqueIdentifier}'. It was a '{option.GetType().FullName}'"); + logFunction($"Skipping code action operation for '{data.UniqueIdentifier}'. It was a '{option.GetType().FullName}'"); continue; } @@ -79,8 +90,7 @@ internal class CodeActionResolveHelper || projectChange.GetRemovedAdditionalDocuments().Any() || projectChange.GetRemovedAnalyzerConfigDocuments().Any()) { - if (context.GetRequiredClientCapabilities() is not { Workspace.WorkspaceEdit.ResourceOperations: { } resourceOperations } - || !resourceOperations.Contains(ResourceOperationKind.Delete)) + if (!resourceOperations.Contains(ResourceOperationKind.Delete)) { // Removing documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty() }; @@ -91,8 +101,7 @@ internal class CodeActionResolveHelper || projectChange.GetAddedAdditionalDocuments().Any() || projectChange.GetAddedAnalyzerConfigDocuments().Any()) { - if (context.GetRequiredClientCapabilities() is not { Workspace.WorkspaceEdit.ResourceOperations: { } resourceOperations } - || !resourceOperations.Contains(ResourceOperationKind.Create)) + if (!resourceOperations.Contains(ResourceOperationKind.Create)) { // Adding documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty() }; @@ -103,8 +112,7 @@ internal class CodeActionResolveHelper || projectChange.GetChangedAdditionalDocuments().Any(docId => HasDocumentNameChange(docId, newSolution, solution) || projectChange.GetChangedAnalyzerConfigDocuments().Any(docId => HasDocumentNameChange(docId, newSolution, solution)))) { - if (context.GetRequiredClientCapabilities() is not { Workspace.WorkspaceEdit.ResourceOperations: { } resourceOperations } - || !resourceOperations.Contains(ResourceOperationKind.Rename)) + if (!resourceOperations.Contains(ResourceOperationKind.Rename)) { // Rename documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty() }; diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Handlers/CodeActions.cs b/src/Tools/ExternalAccess/Razor/Cohost/Handlers/CodeActions.cs new file mode 100644 index 0000000000000..1fe5e62b48853 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Cohost/Handlers/CodeActions.cs @@ -0,0 +1,81 @@ +// 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. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; + +internal static class CodeActions +{ + public static Task GetCodeActionsAsync( + Document document, + CodeActionParams request, + bool supportsVSExtensions, + CancellationToken cancellationToken) + { + var solution = document.Project.Solution; + + var codeFixService = solution.Services.ExportProvider.GetService(); + var codeRefactoringService = solution.Services.ExportProvider.GetService(); + + return CodeActionHelpers.GetVSCodeActionsAsync(request, document, codeFixService, codeRefactoringService, supportsVSExtensions, cancellationToken); + } + + public static async Task ResolveCodeActionAsync(Document document, CodeAction codeAction, ResourceOperationKind[] resourceOperations, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(codeAction.Data); + var data = CodeActionResolveHandler.GetCodeActionResolveData(codeAction); + Assumes.Present(data); + + // We don't need to resolve a top level code action that has nested actions - it requires further action + // on the client to pick which of the nested actions to actually apply. + if (data.NestedCodeActions.HasValue && data.NestedCodeActions.Value.Length > 0) + { + return codeAction; + } + + var solution = document.Project.Solution; + + var codeFixService = solution.Services.ExportProvider.GetService(); + var codeRefactoringService = solution.Services.ExportProvider.GetService(); + + var codeActions = await CodeActionHelpers.GetCodeActionsAsync( + document, + data.Range, + codeFixService, + codeRefactoringService, + fixAllScope: null, + cancellationToken).ConfigureAwait(false); + + Contract.ThrowIfNull(data.CodeActionPath); + var codeActionToResolve = CodeActionHelpers.GetCodeActionToResolve(data.CodeActionPath, codeActions, isFixAllAction: false); + + var operations = await codeActionToResolve.GetOperationsAsync(solution, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false); + + var edit = await CodeActionResolveHelper.GetCodeActionResolveEditsAsync( + solution, + data, + operations, + resourceOperations, + logFunction: static s => { }, + cancellationToken).ConfigureAwait(false); + + codeAction.Edit = edit; + return codeAction; + } + + public static Task GetFormattedNewFileContentAsync(Document document, CancellationToken cancellationToken) + => FormatNewFileHandler.GetFormattedNewFileContentAsync(document, cancellationToken); + + public static Task GetSimplifiedEditsAsync(Document document, TextEdit textEdit, CancellationToken cancellationToken) + => SimplifyMethodHandler.GetSimplifiedEditsAsync(document, textEdit, cancellationToken); +}