6
6
using System . Collections . Immutable ;
7
7
using System . Diagnostics . CodeAnalysis ;
8
8
using System . Linq ;
9
- using System . Text ;
10
9
using System . Threading ;
11
10
using System . Threading . Tasks ;
12
11
using Microsoft . AspNetCore . Razor . Language ;
13
- using Microsoft . AspNetCore . Razor . Language . Components ;
14
- using Microsoft . AspNetCore . Razor . Language . Extensions ;
15
- using Microsoft . AspNetCore . Razor . Language . Intermediate ;
16
12
using Microsoft . AspNetCore . Razor . Language . Syntax ;
17
13
using Microsoft . AspNetCore . Razor . LanguageServer . CodeActions . Models ;
18
14
using Microsoft . AspNetCore . Razor . Threading ;
19
15
using Microsoft . CodeAnalysis . Razor . Logging ;
20
- using Microsoft . CodeAnalysis . Razor . Workspaces ;
21
16
using Microsoft . CodeAnalysis . Text ;
22
17
using Microsoft . VisualStudio . LanguageServer . Protocol ;
23
- using Range = Microsoft . VisualStudio . LanguageServer . Protocol . Range ;
24
18
25
19
namespace Microsoft . AspNetCore . Razor . LanguageServer . CodeActions . Razor ;
26
20
27
- internal sealed class ExtractToNewComponentCodeActionProvider ( ILoggerFactory loggerFactory ) : IRazorCodeActionProvider
21
+ internal sealed class ExtractToComponentCodeActionProvider ( ILoggerFactory loggerFactory ) : IRazorCodeActionProvider
28
22
{
29
- private readonly ILogger _logger = loggerFactory . GetOrCreateLogger < ExtractToNewComponentCodeActionProvider > ( ) ;
23
+ private readonly ILogger _logger = loggerFactory . GetOrCreateLogger < ExtractToComponentCodeActionProvider > ( ) ;
30
24
31
25
public Task < ImmutableArray < RazorVSInternalCodeAction > > ProvideAsync ( RazorCodeActionContext context , CancellationToken cancellationToken )
32
26
{
@@ -51,14 +45,18 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
51
45
return SpecializedTasks . EmptyImmutableArray < RazorVSInternalCodeAction > ( ) ;
52
46
}
53
47
54
- var ( startElementNode , endElementNode ) = GetStartAndEndElements ( context , syntaxTree , _logger ) ;
55
-
56
48
// Make sure the selection starts on an element tag
49
+ var ( startElementNode , endElementNode ) = GetStartAndEndElements ( context , syntaxTree , _logger ) ;
57
50
if ( startElementNode is null )
58
51
{
59
52
return SpecializedTasks . EmptyImmutableArray < RazorVSInternalCodeAction > ( ) ;
60
53
}
61
54
55
+ if ( endElementNode is null )
56
+ {
57
+ endElementNode = startElementNode ;
58
+ }
59
+
62
60
if ( ! TryGetNamespace ( context . CodeDocument , out var @namespace ) )
63
61
{
64
62
return SpecializedTasks . EmptyImmutableArray < RazorVSInternalCodeAction > ( ) ;
@@ -68,14 +66,28 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
68
66
69
67
ProcessSelection ( startElementNode , endElementNode , actionParams ) ;
70
68
69
+ var utilityScanRoot = FindNearestCommonAncestor ( startElementNode , endElementNode ) ?? startElementNode ;
70
+
71
+ // The new component usings are going to be a subset of the usings in the source razor file.
72
+ var usingStrings = syntaxTree . Root . DescendantNodes ( ) . Where ( node => node . IsUsingDirective ( out var _ ) ) . Select ( node => node . ToFullString ( ) . TrimEnd ( ) ) ;
73
+
74
+ // Get only the namespace after the "using" keyword.
75
+ var usingNamespaceStrings = usingStrings . Select ( usingString => usingString . Substring ( "using " . Length ) ) ;
76
+
77
+ AddUsingDirectivesInRange ( utilityScanRoot ,
78
+ usingNamespaceStrings ,
79
+ actionParams . ExtractStart ,
80
+ actionParams . ExtractEnd ,
81
+ actionParams ) ;
82
+
71
83
var resolutionParams = new RazorCodeActionResolutionParams ( )
72
84
{
73
85
Action = LanguageServerConstants . CodeActions . ExtractToNewComponentAction ,
74
86
Language = LanguageServerConstants . CodeActions . Languages . Razor ,
75
87
Data = actionParams ,
76
88
} ;
77
89
78
- var codeAction = RazorCodeActionFactory . CreateExtractToNewComponent ( resolutionParams ) ;
90
+ var codeAction = RazorCodeActionFactory . CreateExtractToComponent ( resolutionParams ) ;
79
91
return Task . FromResult < ImmutableArray < RazorVSInternalCodeAction > > ( [ codeAction ] ) ;
80
92
}
81
93
@@ -95,6 +107,7 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn
95
107
}
96
108
97
109
var endElementNode = GetEndElementNode ( context , syntaxTree ) ;
110
+
98
111
return ( startElementNode , endElementNode ) ;
99
112
}
100
113
@@ -135,14 +148,15 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma
135
148
return endOwner . FirstAncestorOrSelf < MarkupElementSyntax > ( ) ;
136
149
}
137
150
138
- private static ExtractToNewComponentCodeActionParams CreateInitialActionParams ( RazorCodeActionContext context , MarkupElementSyntax startElementNode , string @namespace )
151
+ private static ExtractToComponentCodeActionParams CreateInitialActionParams ( RazorCodeActionContext context , MarkupElementSyntax startElementNode , string @namespace )
139
152
{
140
- return new ExtractToNewComponentCodeActionParams
153
+ return new ExtractToComponentCodeActionParams
141
154
{
142
155
Uri = context . Request . TextDocument . Uri ,
143
156
ExtractStart = startElementNode . Span . Start ,
144
157
ExtractEnd = startElementNode . Span . End ,
145
- Namespace = @namespace
158
+ Namespace = @namespace ,
159
+ usingDirectives = [ ]
146
160
} ;
147
161
}
148
162
@@ -152,7 +166,7 @@ private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(R
152
166
/// <param name="startElementNode">The starting element of the selection.</param>
153
167
/// <param name="endElementNode">The ending element of the selection, if it exists.</param>
154
168
/// <param name="actionParams">The parameters for the extraction action, which will be updated.</param>
155
- private static void ProcessSelection ( MarkupElementSyntax startElementNode , MarkupElementSyntax ? endElementNode , ExtractToNewComponentCodeActionParams actionParams )
169
+ private static void ProcessSelection ( MarkupElementSyntax startElementNode , MarkupElementSyntax ? endElementNode , ExtractToComponentCodeActionParams actionParams )
156
170
{
157
171
// If there's no end element, we can't process a multi-point selection
158
172
if ( endElementNode is null )
@@ -183,7 +197,7 @@ private static void ProcessSelection(MarkupElementSyntax startElementNode, Marku
183
197
// </span>|}|}
184
198
// </div>
185
199
// In this case, we need to find the smallest set of complete elements that covers the entire selection.
186
-
200
+
187
201
// Find the closest containing sibling pair that encompasses both the start and end elements
188
202
var ( extractStart , extractEnd ) = FindContainingSiblingPair ( startElementNode , endElementNode ) ;
189
203
@@ -207,7 +221,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
207
221
{
208
222
// Find the lowest common ancestor of both nodes
209
223
var nearestCommonAncestor = FindNearestCommonAncestor ( startNode , endNode ) ;
210
- if ( nearestCommonAncestor == null )
224
+ if ( nearestCommonAncestor is null )
211
225
{
212
226
return ( null , null ) ;
213
227
}
@@ -223,7 +237,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
223
237
{
224
238
var childSpan = child . Span ;
225
239
226
- if ( startContainingNode == null && childSpan . Contains ( startSpan ) )
240
+ if ( startContainingNode is null && childSpan . Contains ( startSpan ) )
227
241
{
228
242
startContainingNode = child ;
229
243
if ( endContainingNode is not null )
@@ -245,7 +259,10 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
245
259
{
246
260
var current = node1 ;
247
261
248
- while ( current . Kind == SyntaxKind . MarkupElement && current is not null )
262
+ while ( current is MarkupElementSyntax or
263
+ MarkupTagHelperAttributeSyntax or
264
+ MarkupBlockSyntax &&
265
+ current is not null )
249
266
{
250
267
if ( current . Span . Contains ( node2 . Span ) )
251
268
{
@@ -257,4 +274,56 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
257
274
258
275
return null ;
259
276
}
277
+
278
+ private static void AddUsingDirectivesInRange ( SyntaxNode root , IEnumerable < string > usingsInSourceRazor , int extractStart , int extractEnd , ExtractToComponentCodeActionParams actionParams )
279
+ {
280
+ var components = new HashSet < string > ( ) ;
281
+ var extractSpan = new TextSpan ( extractStart , extractEnd - extractStart ) ;
282
+
283
+ foreach ( var node in root . DescendantNodes ( ) . Where ( node => extractSpan . Contains ( node . Span ) ) )
284
+ {
285
+ if ( node is MarkupTagHelperElementSyntax { TagHelperInfo : { } tagHelperInfo } )
286
+ {
287
+ AddUsingFromTagHelperInfo ( tagHelperInfo , components , usingsInSourceRazor , actionParams ) ;
288
+ }
289
+ }
290
+ }
291
+
292
+ private static void AddUsingFromTagHelperInfo ( TagHelperInfo tagHelperInfo , HashSet < string > components , IEnumerable < string > usingsInSourceRazor , ExtractToComponentCodeActionParams actionParams )
293
+ {
294
+ foreach ( var descriptor in tagHelperInfo . BindingResult . Descriptors )
295
+ {
296
+ if ( descriptor is null )
297
+ {
298
+ continue ;
299
+ }
300
+
301
+ var typeNamespace = descriptor . GetTypeNamespace ( ) ;
302
+
303
+ // Since the using directive at the top of the file may be relative and not absolute,
304
+ // we need to generate all possible partial namespaces from `typeNamespace`.
305
+
306
+ // Potentially, the alternative could be to ask if the using namespace at the top is a substring of `typeNamespace`.
307
+ // The only potential edge case is if there are very similar namespaces where one
308
+ // is a substring of the other, but they're actually different (e.g., "My.App" and "My.Apple").
309
+
310
+ // Generate all possible partial namespaces from `typeNamespace`, from least to most specific
311
+ // (assuming that the user writes absolute `using` namespaces most of the time)
312
+
313
+ // This is a bit inefficient because at most 'k' string operations are performed (k = parts in the namespace),
314
+ // for each potential using directive.
315
+
316
+ var parts = typeNamespace . Split ( '.' ) ;
317
+ for ( var i = 0 ; i < parts . Length ; i ++ )
318
+ {
319
+ var partialNamespace = string . Join ( "." , parts . Skip ( i ) ) ;
320
+
321
+ if ( components . Add ( partialNamespace ) && usingsInSourceRazor . Contains ( partialNamespace ) )
322
+ {
323
+ actionParams . usingDirectives . Add ( $ "@using { partialNamespace } ") ;
324
+ break ;
325
+ }
326
+ }
327
+ }
328
+ }
260
329
}
0 commit comments