Skip to content

Commit 5b76f23

Browse files
committed
Rebase. Made some mistakes while rebasing, I'm sorry.
Integrated previous PR feedback into this branch.
1 parent b37e6d8 commit 5b76f23

File tree

5 files changed

+109
-78
lines changed

5 files changed

+109
-78
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToCodeBehindCodeActionParams.cs

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ internal sealed class ExtractToCodeBehindCodeActionParams
1818
[JsonPropertyName("extractEnd")]
1919
public int ExtractEnd { get; set; }
2020

21+
[JsonPropertyName("removeStart")]
22+
public int RemoveStart { get; set; }
23+
24+
[JsonPropertyName("removeEnd")]
25+
public int RemoveEnd { get; set; }
26+
2127
[JsonPropertyName("namespace")]
2228
public required string Namespace { get; set; }
2329
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Text.Json.Serialization;
7+
using Microsoft.VisualStudio.LanguageServer.Protocol;
78

89
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
910

@@ -14,15 +15,15 @@ internal sealed class ExtractToComponentCodeActionParams
1415
[JsonPropertyName("uri")]
1516
public required Uri Uri { get; set; }
1617

17-
[JsonPropertyName("extractStart")]
18-
public int ExtractStart { get; set; }
18+
[JsonPropertyName("selectStart")]
19+
public required Position SelectStart { get; set; }
1920

20-
[JsonPropertyName("extractEnd")]
21-
public int ExtractEnd { get; set; }
21+
[JsonPropertyName("selectEnd")]
22+
public required Position SelectEnd { get; set; }
23+
24+
[JsonPropertyName("absoluteIndex")]
25+
public required int AbsoluteIndex { get; set; }
2226

2327
[JsonPropertyName("namespace")]
2428
public required string Namespace { get; set; }
25-
26-
[JsonPropertyName("usingDirectives")]
27-
public required List<string> usingDirectives { get; set; }
2829
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs

+8-16
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,15 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
4343
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
4444
}
4545

46-
var actionParams = CreateInitialActionParams(context, startElementNode, @namespace);
47-
48-
ProcessSelection(startElementNode, endElementNode, actionParams);
49-
50-
var utilityScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode;
51-
52-
// The new component usings are going to be a subset of the usings in the source razor file.
53-
var usingStrings = syntaxTree.Root.DescendantNodes().Where(node => node.IsUsingDirective(out var _)).Select(node => node.ToFullString().TrimEnd());
54-
55-
// Get only the namespace after the "using" keyword.
56-
var usingNamespaceStrings = usingStrings.Select(usingString => usingString.Substring("using ".Length));
46+
var actionParams = new ExtractToComponentCodeActionParams
47+
{
48+
Uri = context.Request.TextDocument.Uri,
49+
SelectStart = context.Request.Range.Start,
50+
SelectEnd = context.Request.Range.End,
51+
AbsoluteIndex = context.Location.AbsoluteIndex,
52+
Namespace = @namespace,
53+
};
5754

58-
AddUsingDirectivesInRange(utilityScanRoot,
59-
usingNamespaceStrings,
60-
actionParams.ExtractStart,
61-
actionParams.ExtractEnd,
62-
actionParams);
6355

6456
var resolutionParams = new RazorCodeActionResolutionParams()
6557
{

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs

+86-40
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
using Microsoft.VisualStudio.Utilities;
4343
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
4444
using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
45+
using Microsoft.AspNetCore.Razor.PooledObjects;
4546

4647
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;
4748

@@ -72,6 +73,8 @@ internal sealed class ExtractToComponentCodeActionResolver(
7273
}
7374

7475
var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
76+
var syntaxTree = codeDocument.GetSyntaxTree();
77+
7578
if (codeDocument.IsUnsupported())
7679
{
7780
return null;
@@ -199,7 +202,10 @@ internal sealed record SelectionAnalysisResult
199202

200203
private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument codeDocument, ExtractToComponentCodeActionParams actionParams)
201204
{
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);
203209
if (startElementNode is null)
204210
{
205211
return new SelectionAnalysisResult { Success = false };
@@ -221,7 +227,7 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
221227
}
222228

223229
var dependencyScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode;
224-
var usingDirectives = GetUsingDirectivesInRange(dependencyScanRoot, extractStart, extractEnd);
230+
var usingDirectives = GetUsingDirectivesInRange(syntaxTree, dependencyScanRoot, extractStart, extractEnd);
225231
var hasOtherIdentifiers = CheckHasOtherIdentifiers(dependencyScanRoot, extractStart, extractEnd);
226232

227233
return new SelectionAnalysisResult
@@ -235,9 +241,8 @@ private static SelectionAnalysisResult TryAnalyzeSelection(RazorCodeDocument cod
235241
};
236242
}
237243

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)
239245
{
240-
var syntaxTree = codeDocument.GetSyntaxTree();
241246
if (syntaxTree is null)
242247
{
243248
return (null, null);
@@ -255,32 +260,23 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
255260
return (null, null);
256261
}
257262

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);
265264

266265
return (startElementNode, endElementNode);
267266
}
268267

269-
private static MarkupSyntaxNode? TryGetEndElementNode(Position selectionStart, Position selectionEnd, RazorSyntaxTree syntaxTree, SourceText sourceText)
268+
private static MarkupSyntaxNode? GetEndElementNode(SourceText sourceText, RazorSyntaxTree syntaxTree, ExtractToComponentCodeActionParams actionParams)
270269
{
271-
if (selectionStart == selectionEnd)
272-
{
273-
return null;
274-
}
270+
var selectionStart = actionParams.SelectStart;
271+
var selectionEnd = actionParams.SelectEnd;
275272

276-
var endLocation = GetEndLocation(selectionEnd, sourceText);
277-
if (!endLocation.HasValue)
273+
if (selectionStart == selectionEnd)
278274
{
279275
return null;
280276
}
281277

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);
284280
if (endOwner is null)
285281
{
286282
return null;
@@ -295,16 +291,6 @@ private static (MarkupSyntaxNode? Start, MarkupSyntaxNode? End) GetStartAndEndEl
295291
return endOwner.FirstAncestorOrSelf<MarkupSyntaxNode>(node => node is MarkupTagHelperElementSyntax or MarkupElementSyntax);
296292
}
297293

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-
308294
/// <summary>
309295
/// Processes a selection, providing the start and end of the extraction range if successful.
310296
/// </summary>
@@ -377,13 +363,9 @@ private static bool TryProcessSelection(
377363
return true;
378364
}
379365

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);
385367

386-
var endOwner = codeDocument.GetSyntaxTree().Root.FindInnermostNode(endLocation.Value.AbsoluteIndex, true);
368+
var endOwner = codeDocument.GetSyntaxTree().Root.FindInnermostNode(endLocation, true);
387369
var endCodeBlock = endOwner?.FirstAncestorOrSelf<CSharpCodeBlockSyntax>();
388370
if (endOwner is not null && endOwner.TryGetPreviousSibling(out var previousSibling))
389371
{
@@ -475,8 +457,48 @@ private static bool IsValidNode(SyntaxNode node, bool isCodeBlock)
475457
return node is MarkupElementSyntax or MarkupTagHelperElementSyntax || (isCodeBlock && node is CSharpCodeBlockSyntax);
476458
}
477459

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)
479461
{
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+
480502
var usings = new HashSet<string>();
481503
var extractSpan = new TextSpan(extractStart, extractEnd - extractStart);
482504

@@ -490,7 +512,7 @@ private static HashSet<string> GetUsingDirectivesInRange(SyntaxNode root, int ex
490512

491513
if (node is MarkupTagHelperElementSyntax { TagHelperInfo: { } tagHelperInfo })
492514
{
493-
AddUsingFromTagHelperInfo(tagHelperInfo, usings);
515+
AddUsingFromTagHelperInfo(tagHelperInfo, usings, usingsInSourceRazor);
494516
}
495517
}
496518

@@ -550,7 +572,7 @@ private static bool CheckHasOtherIdentifiers(SyntaxNode root, int extractStart,
550572
return false;
551573
}
552574

553-
private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashSet<string> dependencies)
575+
private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashSet<string> usings, string[] usingsInSourceRazor)
554576
{
555577
foreach (var descriptor in tagHelperInfo.BindingResult.Descriptors)
556578
{
@@ -560,7 +582,31 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
560582
}
561583

562584
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+
}
564610
}
565611
}
566612

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.ExtractToComponent.NetFx.cs

+1-15
Original file line numberDiff line numberDiff line change
@@ -331,24 +331,10 @@ private async Task ValidateExtractComponentCodeActionAsync(
331331
var razorFilePath = "C:/path/Test.razor";
332332
var componentFilePath = "C:/path/Component.razor";
333333
var codeDocument = CreateCodeDocument(input, filePath: razorFilePath);
334-
var sourceText = codeDocument.GetSourceText();
334+
var sourceText = codeDocument.Source.Text;
335335
var uri = new Uri(razorFilePath);
336336
var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath, additionalRazorDocuments);
337337

338-
//var projectManager = CreateProjectSnapshotManager();
339-
340-
//await projectManager.UpdateAsync(updater =>
341-
//{
342-
// updater.ProjectAdded(new(
343-
// projectFilePath: "C:/path/to/project.csproj",
344-
// intermediateOutputPath: "C:/path/to/obj",
345-
// razorConfiguration: RazorConfiguration.Default,
346-
// rootNamespace: "project"));
347-
//});
348-
349-
//var componentSearchEngine = new DefaultRazorComponentSearchEngine(projectManager, LoggerFactory);
350-
//var componentDefinitionService = new RazorComponentDe
351-
352338
var documentContext = CreateDocumentContext(uri, codeDocument);
353339
var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null);
354340

0 commit comments

Comments
 (0)