66using System . Text . Json ;
77using System . Text . Json . Nodes ;
88using Microsoft . AspNetCore . Razor ;
9+ using Microsoft . AspNetCore . Razor . PooledObjects ;
910using Microsoft . CodeAnalysis . Razor . Protocol ;
1011
1112namespace Microsoft . CodeAnalysis . Razor . CodeActions . Models ;
1213
1314internal 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 ,
0 commit comments