Skip to content
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 @@ -6,12 +6,19 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Protocol;

namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models;

internal static class CodeActionExtensions
{
// TODO: Use Constants once https://github.com/dotnet/roslyn/pull/81094 is available
private const string NestedCodeActionCommand = "roslyn.client.nestedCodeAction";
private const string NestedCodeActionsProperty = "NestedCodeActions";
private const string CodeActionPathProperty = "CodeActionPath";
private const string FixAllFlavorsProperty = "FixAllFlavors";

public static SumType<Command, CodeAction> AsVSCodeCommandOrCodeAction(this VSInternalCodeAction razorCodeAction, VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri)
{
if (razorCodeAction.Data is null)
Expand Down Expand Up @@ -53,15 +60,18 @@ public static RazorVSInternalCodeAction WrapResolvableCodeAction(
RazorLanguageKind language = RazorLanguageKind.CSharp,
bool isOnAllowList = true)
{
var resolutionParams = new RazorCodeActionResolutionParams()
if (!TryHandleNestedCodeAction(razorCodeAction, context, action, language))
{
TextDocument = context.Request.TextDocument,
Action = action,
Language = language,
DelegatedDocumentUri = context.DelegatedDocumentUri,
Data = razorCodeAction.Data
};
razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams);
var resolutionParams = new RazorCodeActionResolutionParams()
{
TextDocument = context.Request.TextDocument,
Action = action,
Language = language,
DelegatedDocumentUri = context.DelegatedDocumentUri,
Data = razorCodeAction.Data
};
razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams);
}

if (!isOnAllowList)
{
Expand All @@ -79,6 +89,60 @@ public static RazorVSInternalCodeAction WrapResolvableCodeAction(
return razorCodeAction;
}

private static bool TryHandleNestedCodeAction(RazorVSInternalCodeAction razorCodeAction, RazorCodeActionContext context, string action, RazorLanguageKind language)
{
if (language != RazorLanguageKind.CSharp ||
razorCodeAction.Command is not { CommandIdentifier: NestedCodeActionCommand, Arguments: [JsonElement arg] })
{
return false;
}

// For nested code actions in VS Code, we want to not wrap the data from this code action with our context,
// but wrap all of the nested code actions in the first argument. That way, the custom command in the C#
// Extension will work (it expects Data to be unwrapped), and when it tries to resolve the children, they
// will come to us because they're wrapped, and we'll send them on to Roslyn.
//
// We extract each nested code action, wrap its data with our context, then copy across a couple of things
// from its data to our new wrapped data, and we're done. We end up with data that is an odd hybrid of Razor
// and Roslyn expectations, but thanks to the dynamic nature of JSON, it works out.
using var mappedNestedActions = new PooledArrayBuilder<RazorVSInternalCodeAction>();
var nestedCodeActions = arg.GetProperty(NestedCodeActionsProperty);
foreach (var nestedAction in nestedCodeActions.EnumerateArray())
{
var nestedCodeAction = nestedAction.Deserialize<RazorVSInternalCodeAction>(JsonHelpers.JsonSerializerOptions).AssumeNotNull();
var resolutionParams = new RazorCodeActionResolutionParams()
{
TextDocument = context.Request.TextDocument,
Action = action,
Language = language,
DelegatedDocumentUri = context.DelegatedDocumentUri,
Data = nestedCodeAction.Data
};

// We have to set two extra properties that Roslyn requires for nested code actions, copied from it's data object
var newActionData = JsonSerializer.SerializeToNode(resolutionParams).AssumeNotNull();
var nestedData = nestedAction.GetProperty("data");
if (nestedData.TryGetProperty(CodeActionPathProperty, out var codeActionPath))
{
newActionData[CodeActionPathProperty] = JsonSerializer.SerializeToNode(codeActionPath, JsonHelpers.JsonSerializerOptions);
}

if (nestedData.TryGetProperty(FixAllFlavorsProperty, out var fixAllFlavors))
{
newActionData[FixAllFlavorsProperty] = JsonSerializer.SerializeToNode(fixAllFlavors, JsonHelpers.JsonSerializerOptions);
}

nestedCodeAction.Data = newActionData;
mappedNestedActions.Add(nestedCodeAction);
}

// We can't update NestedCodeActions directly, because JsonElement is immutable, so we have to convert to a node
var newArg = JsonSerializer.SerializeToNode(arg, JsonHelpers.JsonSerializerOptions).AssumeNotNull();
newArg.AsObject()[NestedCodeActionsProperty] = JsonSerializer.SerializeToNode(mappedNestedActions.ToArray(), JsonHelpers.JsonSerializerOptions);
razorCodeAction.Command.Arguments[0] = newArg;
return true;
}

private static VSInternalCodeAction WrapResolvableCodeAction(
this VSInternalCodeAction razorCodeAction,
RazorCodeActionContext context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ @using System.Linq
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.UseExpressionBody);
}

[Fact(Skip = "Roslyn code refactoring provider is not finding the expression")]
[Fact]
public async Task IntroduceLocal()
{
var input = """
Expand All @@ -90,7 +90,7 @@ @using System.Linq
{
void M(string[] args)
{
if ([|args.First()|].Length > 0)
if (args.First()[||].Length > 0)
{
}
if (args.First().Length > 0)
Expand All @@ -110,8 +110,8 @@ @using System.Linq
{
void M(string[] args)
{
string v = args.First();
if (v.Length > 0)
int length = args.First().Length;
if (length > 0)
{
}
if (args.First().Length > 0)
Expand All @@ -125,7 +125,7 @@ void M(string[] args)
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.IntroduceVariable);
}

[Fact(Skip = "Roslyn code refactoring provider is not finding the expression")]
[Fact]
public async Task IntroduceLocal_All()
{
var input = """
Expand All @@ -137,7 +137,7 @@ @using System.Linq
{
void M(string[] args)
{
if ([|args.First()|].Length > 0)
if (args.First()[||].Length > 0)
{
}
if (args.First().Length > 0)
Expand All @@ -157,11 +157,11 @@ @using System.Linq
{
void M(string[] args)
{
string v = args.First();
if (v.Length > 0)
int length = args.First().Length;
if (length > 0)
{
}
if (v.Length > 0)
if (length > 0)
{
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Language;
Expand All @@ -15,6 +17,7 @@
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
using Microsoft.CodeAnalysis.Razor.Telemetry;
using Microsoft.CodeAnalysis.Razor.Utilities;
Expand Down Expand Up @@ -100,11 +103,19 @@ Could not find code action with name '{codeActionName}'.
{string.Join(Environment.NewLine + " ", result.Select(e => ((RazorVSInternalCodeAction)e.Value!).Name))}
""");

// In VS, child code actions use the children property, and are easy
if (codeActionToRun.Children?.Length > 0)
{
codeActionToRun = codeActionToRun.Children[childActionIndex];
}

// In VS Code, the C# extension has some custom code to handle child code actions, which we mimic here
if (codeActionToRun.Command is { CommandIdentifier: "roslyn.client.nestedCodeAction", Arguments: [JsonObject data] })
{
var nestedCodeAction = data["NestedCodeActions"].AssumeNotNull().AsArray()[childActionIndex];
codeActionToRun = JsonSerializer.Deserialize<VSInternalCodeAction>(nestedCodeAction, JsonHelpers.JsonSerializerOptions);
}

Assert.NotNull(codeActionToRun);
return codeActionToRun;
}
Expand Down