Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose code actions to Razor cohosting #75711

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading