Skip to content

Commit

Permalink
Merge pull request #75711 from davidwengier/CohostCodeActions
Browse files Browse the repository at this point in the history
Expose code actions to Razor cohosting
  • Loading branch information
davidwengier authored Nov 4, 2024
2 parents f2eab90 + 18779c1 commit 8fc87e0
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public FormatNewFileHandler(IGlobalOptionService globalOptions)

var document = solution.GetRequiredDocument(documentId);

return await GetFormattedNewFileContentAsync(document, cancellationToken).ConfigureAwait(false);
}

internal static async Task<string> 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<INewDocumentFormattingService>();
if (formattingService is not null)
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextEdit[]> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<CodeActionResolveData>((JsonElement)request.Data!, ProtocolConversions.LspJsonSerializerOptions);
Contract.ThrowIfNull(resolveData, "Missing data for code action resolve request");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions
{
internal class CodeActionResolveHelper
{
public static async Task<LSP.WorkspaceEdit> GetCodeActionResolveEditsAsync(RequestContext context, CodeActionResolveData data, ImmutableArray<CodeActionOperation> operations, CancellationToken cancellationToken)
public static Task<LSP.WorkspaceEdit> GetCodeActionResolveEditsAsync(RequestContext context, CodeActionResolveData data, ImmutableArray<CodeActionOperation> 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<LSP.WorkspaceEdit> GetCodeActionResolveEditsAsync(Solution solution, CodeActionResolveData data, ImmutableArray<CodeActionOperation> operations, ResourceOperationKind[] resourceOperations, Action<string> 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.
Expand All @@ -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;
}

Expand Down Expand Up @@ -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<TextDocumentEdit>() };
Expand All @@ -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<TextDocumentEdit>() };
Expand All @@ -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<TextDocumentEdit>() };
Expand Down
81 changes: 81 additions & 0 deletions src/Tools/ExternalAccess/Razor/Cohost/Handlers/CodeActions.cs
Original file line number Diff line number Diff line change
@@ -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<CodeAction[]> GetCodeActionsAsync(
Document document,
CodeActionParams request,
bool supportsVSExtensions,
CancellationToken cancellationToken)
{
var solution = document.Project.Solution;

var codeFixService = solution.Services.ExportProvider.GetService<ICodeFixService>();
var codeRefactoringService = solution.Services.ExportProvider.GetService<ICodeRefactoringService>();

return CodeActionHelpers.GetVSCodeActionsAsync(request, document, codeFixService, codeRefactoringService, supportsVSExtensions, cancellationToken);
}

public static async Task<CodeAction> 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<ICodeFixService>();
var codeRefactoringService = solution.Services.ExportProvider.GetService<ICodeRefactoringService>();

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<string> GetFormattedNewFileContentAsync(Document document, CancellationToken cancellationToken)
=> FormatNewFileHandler.GetFormattedNewFileContentAsync(document, cancellationToken);

public static Task<TextEdit[]> GetSimplifiedEditsAsync(Document document, TextEdit textEdit, CancellationToken cancellationToken)
=> SimplifyMethodHandler.GetSimplifiedEditsAsync(document, textEdit, cancellationToken);
}

0 comments on commit 8fc87e0

Please sign in to comment.