Skip to content

Commit eeace1e

Browse files
authored
Support Roslyn's custom nested code actions in VS Code (#12467)
Fixes #12463
2 parents e5017fa + 7125d36 commit eeace1e

File tree

3 files changed

+92
-17
lines changed

3 files changed

+92
-17
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Models/CodeActionExtensions.cs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@
66
using System.Text.Json;
77
using System.Text.Json.Nodes;
88
using Microsoft.AspNetCore.Razor;
9+
using Microsoft.AspNetCore.Razor.PooledObjects;
910
using Microsoft.CodeAnalysis.Razor.Protocol;
1011

1112
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models;
1213

1314
internal static class CodeActionExtensions
1415
{
16+
// TODO: Use Constants once https://github.com/dotnet/roslyn/pull/81094 is available
17+
private const string NestedCodeActionCommand = "roslyn.client.nestedCodeAction";
18+
private const string NestedCodeActionsProperty = "NestedCodeActions";
19+
private const string CodeActionPathProperty = "CodeActionPath";
20+
private const string FixAllFlavorsProperty = "FixAllFlavors";
21+
1522
public static SumType<Command, CodeAction> AsVSCodeCommandOrCodeAction(this VSInternalCodeAction razorCodeAction, VSTextDocumentIdentifier textDocument, Uri? delegatedDocumentUri)
1623
{
1724
if (razorCodeAction.Data is null)
@@ -53,15 +60,18 @@ public static RazorVSInternalCodeAction WrapResolvableCodeAction(
5360
RazorLanguageKind language = RazorLanguageKind.CSharp,
5461
bool isOnAllowList = true)
5562
{
56-
var resolutionParams = new RazorCodeActionResolutionParams()
63+
if (!TryHandleNestedCodeAction(razorCodeAction, context, action, language))
5764
{
58-
TextDocument = context.Request.TextDocument,
59-
Action = action,
60-
Language = language,
61-
DelegatedDocumentUri = context.DelegatedDocumentUri,
62-
Data = razorCodeAction.Data
63-
};
64-
razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams);
65+
var resolutionParams = new RazorCodeActionResolutionParams()
66+
{
67+
TextDocument = context.Request.TextDocument,
68+
Action = action,
69+
Language = language,
70+
DelegatedDocumentUri = context.DelegatedDocumentUri,
71+
Data = razorCodeAction.Data
72+
};
73+
razorCodeAction.Data = JsonSerializer.SerializeToElement(resolutionParams);
74+
}
6575

6676
if (!isOnAllowList)
6777
{
@@ -79,6 +89,60 @@ public static RazorVSInternalCodeAction WrapResolvableCodeAction(
7989
return razorCodeAction;
8090
}
8191

92+
private static bool TryHandleNestedCodeAction(RazorVSInternalCodeAction razorCodeAction, RazorCodeActionContext context, string action, RazorLanguageKind language)
93+
{
94+
if (language != RazorLanguageKind.CSharp ||
95+
razorCodeAction.Command is not { CommandIdentifier: NestedCodeActionCommand, Arguments: [JsonElement arg] })
96+
{
97+
return false;
98+
}
99+
100+
// For nested code actions in VS Code, we want to not wrap the data from this code action with our context,
101+
// but wrap all of the nested code actions in the first argument. That way, the custom command in the C#
102+
// Extension will work (it expects Data to be unwrapped), and when it tries to resolve the children, they
103+
// will come to us because they're wrapped, and we'll send them on to Roslyn.
104+
//
105+
// We extract each nested code action, wrap its data with our context, then copy across a couple of things
106+
// from its data to our new wrapped data, and we're done. We end up with data that is an odd hybrid of Razor
107+
// and Roslyn expectations, but thanks to the dynamic nature of JSON, it works out.
108+
using var mappedNestedActions = new PooledArrayBuilder<RazorVSInternalCodeAction>();
109+
var nestedCodeActions = arg.GetProperty(NestedCodeActionsProperty);
110+
foreach (var nestedAction in nestedCodeActions.EnumerateArray())
111+
{
112+
var nestedCodeAction = nestedAction.Deserialize<RazorVSInternalCodeAction>(JsonHelpers.JsonSerializerOptions).AssumeNotNull();
113+
var resolutionParams = new RazorCodeActionResolutionParams()
114+
{
115+
TextDocument = context.Request.TextDocument,
116+
Action = action,
117+
Language = language,
118+
DelegatedDocumentUri = context.DelegatedDocumentUri,
119+
Data = nestedCodeAction.Data
120+
};
121+
122+
// We have to set two extra properties that Roslyn requires for nested code actions, copied from it's data object
123+
var newActionData = JsonSerializer.SerializeToNode(resolutionParams).AssumeNotNull();
124+
var nestedData = nestedAction.GetProperty("data");
125+
if (nestedData.TryGetProperty(CodeActionPathProperty, out var codeActionPath))
126+
{
127+
newActionData[CodeActionPathProperty] = JsonSerializer.SerializeToNode(codeActionPath, JsonHelpers.JsonSerializerOptions);
128+
}
129+
130+
if (nestedData.TryGetProperty(FixAllFlavorsProperty, out var fixAllFlavors))
131+
{
132+
newActionData[FixAllFlavorsProperty] = JsonSerializer.SerializeToNode(fixAllFlavors, JsonHelpers.JsonSerializerOptions);
133+
}
134+
135+
nestedCodeAction.Data = newActionData;
136+
mappedNestedActions.Add(nestedCodeAction);
137+
}
138+
139+
// We can't update NestedCodeActions directly, because JsonElement is immutable, so we have to convert to a node
140+
var newArg = JsonSerializer.SerializeToNode(arg, JsonHelpers.JsonSerializerOptions).AssumeNotNull();
141+
newArg.AsObject()[NestedCodeActionsProperty] = JsonSerializer.SerializeToNode(mappedNestedActions.ToArray(), JsonHelpers.JsonSerializerOptions);
142+
razorCodeAction.Command.Arguments[0] = newArg;
143+
return true;
144+
}
145+
82146
private static VSInternalCodeAction WrapResolvableCodeAction(
83147
this VSInternalCodeAction razorCodeAction,
84148
RazorCodeActionContext context,

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CSharpCodeActionTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ @using System.Linq
7878
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.UseExpressionBody);
7979
}
8080

81-
[Fact(Skip = "Roslyn code refactoring provider is not finding the expression")]
81+
[Fact]
8282
public async Task IntroduceLocal()
8383
{
8484
var input = """
@@ -90,7 +90,7 @@ @using System.Linq
9090
{
9191
void M(string[] args)
9292
{
93-
if ([|args.First()|].Length > 0)
93+
if (args.First()[||].Length > 0)
9494
{
9595
}
9696
if (args.First().Length > 0)
@@ -110,8 +110,8 @@ @using System.Linq
110110
{
111111
void M(string[] args)
112112
{
113-
string v = args.First();
114-
if (v.Length > 0)
113+
int length = args.First().Length;
114+
if (length > 0)
115115
{
116116
}
117117
if (args.First().Length > 0)
@@ -125,7 +125,7 @@ void M(string[] args)
125125
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeRefactoringProviderNames.IntroduceVariable);
126126
}
127127

128-
[Fact(Skip = "Roslyn code refactoring provider is not finding the expression")]
128+
[Fact]
129129
public async Task IntroduceLocal_All()
130130
{
131131
var input = """
@@ -137,7 +137,7 @@ @using System.Linq
137137
{
138138
void M(string[] args)
139139
{
140-
if ([|args.First()|].Length > 0)
140+
if (args.First()[||].Length > 0)
141141
{
142142
}
143143
if (args.First().Length > 0)
@@ -157,11 +157,11 @@ @using System.Linq
157157
{
158158
void M(string[] args)
159159
{
160-
string v = args.First();
161-
if (v.Length > 0)
160+
int length = args.First().Length;
161+
if (length > 0)
162162
{
163163
}
164-
if (v.Length > 0)
164+
if (length > 0)
165165
{
166166
}
167167
}

src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CohostCodeActionsEndpointTestBase.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
using System.IO;
77
using System.Linq;
88
using System.Text;
9+
using System.Text.Json;
10+
using System.Text.Json.Nodes;
911
using System.Threading.Tasks;
1012
using Microsoft.AspNetCore.Razor;
1113
using Microsoft.AspNetCore.Razor.Language;
@@ -15,6 +17,7 @@
1517
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
1618
using Microsoft.CodeAnalysis.Razor;
1719
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
20+
using Microsoft.CodeAnalysis.Razor.Protocol;
1821
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
1922
using Microsoft.CodeAnalysis.Razor.Telemetry;
2023
using Microsoft.CodeAnalysis.Razor.Utilities;
@@ -100,11 +103,19 @@ Could not find code action with name '{codeActionName}'.
100103
{string.Join(Environment.NewLine + " ", result.Select(e => ((RazorVSInternalCodeAction)e.Value!).Name))}
101104
""");
102105

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

112+
// In VS Code, the C# extension has some custom code to handle child code actions, which we mimic here
113+
if (codeActionToRun.Command is { CommandIdentifier: "roslyn.client.nestedCodeAction", Arguments: [JsonObject data] })
114+
{
115+
var nestedCodeAction = data["NestedCodeActions"].AssumeNotNull().AsArray()[childActionIndex];
116+
codeActionToRun = JsonSerializer.Deserialize<VSInternalCodeAction>(nestedCodeAction, JsonHelpers.JsonSerializerOptions);
117+
}
118+
108119
Assert.NotNull(codeActionToRun);
109120
return codeActionToRun;
110121
}

0 commit comments

Comments
 (0)