42
42
using Microsoft . VisualStudio . Utilities ;
43
43
using Range = Microsoft . VisualStudio . LanguageServer . Protocol . Range ;
44
44
using Microsoft . AspNetCore . Razor . LanguageServer . Diagnostics ;
45
+ using Microsoft . AspNetCore . Razor . PooledObjects ;
45
46
46
47
namespace Microsoft . AspNetCore . Razor . LanguageServer . CodeActions ;
47
48
@@ -72,6 +73,8 @@ internal sealed class ExtractToComponentCodeActionResolver(
72
73
}
73
74
74
75
var codeDocument = await documentContext . GetCodeDocumentAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
76
+ var syntaxTree = codeDocument . GetSyntaxTree ( ) ;
77
+
75
78
if ( codeDocument . IsUnsupported ( ) )
76
79
{
77
80
return null ;
@@ -199,7 +202,10 @@ internal sealed record SelectionAnalysisResult
199
202
200
203
private static SelectionAnalysisResult TryAnalyzeSelection ( RazorCodeDocument codeDocument , ExtractToComponentCodeActionParams actionParams )
201
204
{
202
- var ( startElementNode , endElementNode ) = GetStartAndEndElements ( codeDocument , actionParams ) ;
205
+ var syntaxTree = codeDocument . GetSyntaxTree ( ) ;
206
+ var sourceText = codeDocument . Source . Text ;
207
+
208
+ var ( startElementNode , endElementNode ) = GetStartAndEndElements ( sourceText , syntaxTree , actionParams ) ;
203
209
if ( startElementNode is null )
204
210
{
205
211
return new SelectionAnalysisResult { Success = false } ;
@@ -221,7 +227,7 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
221
227
}
222
228
223
229
var dependencyScanRoot = FindNearestCommonAncestor ( startElementNode , endElementNode ) ?? startElementNode ;
224
- var usingDirectives = GetUsingDirectivesInRange ( dependencyScanRoot , extractStart , extractEnd ) ;
230
+ var usingDirectives = GetUsingDirectivesInRange ( syntaxTree , dependencyScanRoot , extractStart , extractEnd ) ;
225
231
var hasOtherIdentifiers = CheckHasOtherIdentifiers ( dependencyScanRoot , extractStart , extractEnd ) ;
226
232
227
233
return new SelectionAnalysisResult
@@ -235,9 +241,8 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
235
241
} ;
236
242
}
237
243
238
- private static ( MarkupSyntaxNode ? Start , MarkupSyntaxNode ? End ) GetStartAndEndElements ( RazorCodeDocument codeDocument , ExtractToComponentCodeActionParams actionParams )
244
+ private static ( MarkupSyntaxNode ? Start , MarkupSyntaxNode ? End ) GetStartAndEndElements ( SourceText sourceText , RazorSyntaxTree syntaxTree , ExtractToComponentCodeActionParams actionParams )
239
245
{
240
- var syntaxTree = codeDocument . GetSyntaxTree ( ) ;
241
246
if ( syntaxTree is null )
242
247
{
243
248
return ( null , null ) ;
@@ -255,32 +260,23 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
255
260
return ( null , null ) ;
256
261
}
257
262
258
- var sourceText = codeDocument . GetSourceText ( ) ;
259
- if ( sourceText is null )
260
- {
261
- return ( null , null ) ;
262
- }
263
-
264
- var endElementNode = TryGetEndElementNode ( actionParams . SelectStart , actionParams . SelectEnd , syntaxTree , sourceText ) ;
263
+ var endElementNode = GetEndElementNode ( sourceText , syntaxTree , actionParams ) ;
265
264
266
265
return ( startElementNode , endElementNode ) ;
267
266
}
268
267
269
- private static MarkupSyntaxNode ? TryGetEndElementNode ( Position selectionStart , Position selectionEnd , RazorSyntaxTree syntaxTree , SourceText sourceText )
268
+ private static MarkupSyntaxNode ? GetEndElementNode ( SourceText sourceText , RazorSyntaxTree syntaxTree , ExtractToComponentCodeActionParams actionParams )
270
269
{
271
- if ( selectionStart == selectionEnd )
272
- {
273
- return null ;
274
- }
270
+ var selectionStart = actionParams . SelectStart ;
271
+ var selectionEnd = actionParams . SelectEnd ;
275
272
276
- var endLocation = GetEndLocation ( selectionEnd , sourceText ) ;
277
- if ( ! endLocation . HasValue )
273
+ if ( selectionStart == selectionEnd )
278
274
{
279
275
return null ;
280
276
}
281
277
282
- var endOwner = syntaxTree . Root . FindInnermostNode ( endLocation . Value . AbsoluteIndex , true ) ;
283
-
278
+ var endAbsoluteIndex = sourceText . GetRequiredAbsoluteIndex ( selectionEnd ) ;
279
+ var endOwner = syntaxTree . Root . FindInnermostNode ( endAbsoluteIndex , true ) ;
284
280
if ( endOwner is null )
285
281
{
286
282
return null ;
@@ -295,16 +291,6 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
295
291
return endOwner . FirstAncestorOrSelf < MarkupSyntaxNode > ( node => node is MarkupTagHelperElementSyntax or MarkupElementSyntax ) ;
296
292
}
297
293
298
- private static SourceLocation ? GetEndLocation ( Position selectionEnd , SourceText sourceText )
299
- {
300
- if ( ! selectionEnd . TryGetSourceLocation ( sourceText , logger : default , out var location ) )
301
- {
302
- return null ;
303
- }
304
-
305
- return location ;
306
- }
307
-
308
294
/// <summary>
309
295
/// Processes a selection, providing the start and end of the extraction range if successful.
310
296
/// </summary>
@@ -377,13 +363,9 @@ private static bool TryProcessSelection(
377
363
return true ;
378
364
}
379
365
380
- var endLocation = GetEndLocation ( actionParams . SelectEnd , codeDocument . GetSourceText ( ) ) ;
381
- if ( ! endLocation . HasValue )
382
- {
383
- return false ;
384
- }
366
+ var endLocation = codeDocument . Source . Text . GetRequiredAbsoluteIndex ( actionParams . SelectEnd ) ;
385
367
386
- var endOwner = codeDocument . GetSyntaxTree ( ) . Root . FindInnermostNode ( endLocation . Value . AbsoluteIndex , true ) ;
368
+ var endOwner = codeDocument . GetSyntaxTree ( ) . Root . FindInnermostNode ( endLocation , true ) ;
387
369
var endCodeBlock = endOwner ? . FirstAncestorOrSelf < CSharpCodeBlockSyntax > ( ) ;
388
370
if ( endOwner is not null && endOwner . TryGetPreviousSibling ( out var previousSibling ) )
389
371
{
@@ -475,8 +457,48 @@ private static bool IsValidNode(SyntaxNode node, bool isCodeBlock)
475
457
return node is MarkupElementSyntax or MarkupTagHelperElementSyntax || ( isCodeBlock && node is CSharpCodeBlockSyntax ) ;
476
458
}
477
459
478
- private static HashSet < string > GetUsingDirectivesInRange ( SyntaxNode root , int extractStart , int extractEnd )
460
+ private static HashSet < string > GetUsingDirectivesInRange ( RazorSyntaxTree syntaxTree , SyntaxNode root , int extractStart , int extractEnd )
479
461
{
462
+ // The new component usings are going to be a subset of the usings in the source razor file.
463
+ using var pooledStringArray = new PooledArrayBuilder < string > ( ) ;
464
+ foreach ( var node in syntaxTree . Root . DescendantNodes ( ) )
465
+ {
466
+ if ( node . IsUsingDirective ( out var children ) )
467
+ {
468
+ var sb = new StringBuilder ( ) ;
469
+ var identifierFound = false ;
470
+ var lastIdentifierIndex = - 1 ;
471
+
472
+ // First pass: find the last identifier
473
+ for ( var i = 0 ; i < children . Count ; i ++ )
474
+ {
475
+ if ( children [ i ] is Language . Syntax . SyntaxToken token && token . Kind == Language . SyntaxKind . Identifier )
476
+ {
477
+ lastIdentifierIndex = i ;
478
+ }
479
+ }
480
+
481
+ // Second pass: build the string
482
+ for ( var i = 0 ; i <= lastIdentifierIndex ; i ++ )
483
+ {
484
+ var child = children [ i ] ;
485
+ if ( child is Language . Syntax . SyntaxToken tkn && tkn . Kind == Language . SyntaxKind . Identifier )
486
+ {
487
+ identifierFound = true ;
488
+ }
489
+ if ( identifierFound )
490
+ {
491
+ var token = child as Language . Syntax . SyntaxToken ;
492
+ sb . Append ( token ? . Content ) ;
493
+ }
494
+ }
495
+
496
+ pooledStringArray . Add ( sb . ToString ( ) ) ;
497
+ }
498
+ }
499
+
500
+ var usingsInSourceRazor = pooledStringArray . ToArray ( ) ;
501
+
480
502
var usings = new HashSet < string > ( ) ;
481
503
var extractSpan = new TextSpan ( extractStart , extractEnd - extractStart ) ;
482
504
@@ -490,7 +512,7 @@ private static HashSet<string> GetUsingDirectivesInRange(SyntaxNode root, int ex
490
512
491
513
if ( node is MarkupTagHelperElementSyntax { TagHelperInfo : { } tagHelperInfo } )
492
514
{
493
- AddUsingFromTagHelperInfo ( tagHelperInfo , usings ) ;
515
+ AddUsingFromTagHelperInfo ( tagHelperInfo , usings , usingsInSourceRazor ) ;
494
516
}
495
517
}
496
518
@@ -550,7 +572,7 @@ private static bool CheckHasOtherIdentifiers(SyntaxNode root, int extractStart,
550
572
return false ;
551
573
}
552
574
553
- private static void AddUsingFromTagHelperInfo ( TagHelperInfo tagHelperInfo , HashSet < string > dependencies )
575
+ private static void AddUsingFromTagHelperInfo ( TagHelperInfo tagHelperInfo , HashSet < string > usings , string [ ] usingsInSourceRazor )
554
576
{
555
577
foreach ( var descriptor in tagHelperInfo . BindingResult . Descriptors )
556
578
{
@@ -560,7 +582,31 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
560
582
}
561
583
562
584
var typeNamespace = descriptor . GetTypeNamespace ( ) ;
563
- dependencies . Add ( typeNamespace ) ;
585
+
586
+ // Since the using directive at the top of the file may be relative and not absolute,
587
+ // we need to generate all possible partial namespaces from `typeNamespace`.
588
+
589
+ // Potentially, the alternative could be to ask if the using namespace at the top is a substring of `typeNamespace`.
590
+ // The only potential edge case is if there are very similar namespaces where one
591
+ // is a substring of the other, but they're actually different (e.g., "My.App" and "My.Apple").
592
+
593
+ // Generate all possible partial namespaces from `typeNamespace`, from least to most specific
594
+ // (assuming that the user writes absolute `using` namespaces most of the time)
595
+
596
+ // This is a bit inefficient because at most 'k' string operations are performed (k = parts in the namespace),
597
+ // for each potential using directive.
598
+
599
+ var parts = typeNamespace . Split ( '.' ) ;
600
+ for ( var i = 0 ; i < parts . Length ; i ++ )
601
+ {
602
+ var partialNamespace = string . Join ( "." , parts . Skip ( i ) ) ;
603
+
604
+ if ( usingsInSourceRazor . Contains ( partialNamespace ) )
605
+ {
606
+ usings . Add ( partialNamespace ) ;
607
+ break ;
608
+ }
609
+ }
564
610
}
565
611
}
566
612
0 commit comments