Skip to content

Commit 02e4e99

Browse files
committed
Added method to scan identifiers
1 parent fb61c9c commit 02e4e99

File tree

3 files changed

+116
-20
lines changed

3 files changed

+116
-20
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ internal sealed class ExtractToNewComponentCodeActionParams
2323

2424
[JsonPropertyName("dependencies")]
2525
public required List<string> Dependencies { get; set; }
26+
27+
[JsonPropertyName("usedIdentifiers")]
28+
public required HashSet<string> UsedIdentifiers { get; set; }
29+
30+
[JsonPropertyName("usedMembers")]
31+
public required HashSet<string> UsedMembers { get; set; }
2632
}

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
2-
// Licensed under the MIT license. See License.txt in the project root for license information.
2+
// Licensed under the MIT license. See License.txt in the project divNode for license information.
33

44
using System;
55
using System.Collections.Generic;
@@ -17,6 +17,7 @@
1717
using Microsoft.AspNetCore.Razor.Language.Syntax;
1818
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
1919
using Microsoft.AspNetCore.Razor.Threading;
20+
using Microsoft.CodeAnalysis.CSharp.Syntax;
2021
using Microsoft.CodeAnalysis.Razor.Logging;
2122
using Microsoft.CodeAnalysis.Razor.Workspaces;
2223
using Microsoft.CodeAnalysis.Text;
@@ -58,25 +59,30 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
5859
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
5960
}
6061

62+
if (endElementNode is null)
63+
{
64+
endElementNode = startElementNode;
65+
}
66+
6167
if (!TryGetNamespace(context.CodeDocument, out var @namespace))
6268
{
6369
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
6470
}
6571

66-
var actionParams = CreateInitialActionParams(context, startElementNode, @namespace);
67-
68-
var dependencyScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode);
69-
70-
AddComponentDependenciesInRange(dependencyScanRoot,
71-
actionParams.ExtractStart,
72-
actionParams.ExtractEnd,
73-
actionParams);
72+
var actionParams = CreateInitialActionParams(context, startElementNode, @namespace);
7473

7574
if (IsMultiPointSelection(context.Request.Range))
7675
{
7776
ProcessMultiPointSelection(startElementNode, endElementNode, actionParams);
7877
}
7978

79+
var utilityScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode;
80+
AddComponentDependenciesInRange(utilityScanRoot,
81+
actionParams.ExtractStart,
82+
actionParams.ExtractEnd,
83+
actionParams);
84+
GetUsedIdentifiers(utilityScanRoot, syntaxTree.Root, actionParams);
85+
8086
var resolutionParams = new RazorCodeActionResolutionParams()
8187
{
8288
Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction,
@@ -105,8 +111,6 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn
105111

106112
var endElementNode = GetEndElementNode(context, syntaxTree, logger);
107113

108-
endElementNode ??= startElementNode;
109-
110114
return (startElementNode, endElementNode);
111115
}
112116

@@ -144,7 +148,7 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma
144148
return null;
145149
}
146150

147-
// Correct selection to include the current node if the selection ends immediately after a closing tag.
151+
// Correct selection to include the current node if the selection ends at the "edge" (i.e. immediately after the ">") of a tag.
148152
if (string.IsNullOrWhiteSpace(endOwner.ToFullString()) && endOwner.TryGetPreviousSibling(out var previousSibling))
149153
{
150154
endOwner = previousSibling;
@@ -161,7 +165,9 @@ private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(R
161165
ExtractStart = startElementNode.Span.Start,
162166
ExtractEnd = startElementNode.Span.End,
163167
Namespace = @namespace,
164-
Dependencies = []
168+
Dependencies = [],
169+
UsedIdentifiers = [],
170+
UsedMembers = [],
165171
};
166172
}
167173

@@ -236,7 +242,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
236242
{
237243
// Find the lowest common ancestor of both nodes
238244
var nearestCommonAncestor = FindNearestCommonAncestor(startNode, endNode);
239-
if (nearestCommonAncestor == null)
245+
if (nearestCommonAncestor is null)
240246
{
241247
return (null, null);
242248
}
@@ -248,11 +254,11 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
248254
var startSpan = startNode.Span;
249255
var endSpan = endNode.Span;
250256

251-
foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node.Kind == SyntaxKind.MarkupElement))
257+
foreach (var child in nearestCommonAncestor.ChildNodes().Where(node => node is MarkupElementSyntax))
252258
{
253259
var childSpan = child.Span;
254260

255-
if (startContainingNode == null && childSpan.Contains(startSpan))
261+
if (startContainingNode is null && childSpan.Contains(startSpan))
256262
{
257263
startContainingNode = child;
258264
if (endContainingNode is not null)
@@ -274,7 +280,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
274280
{
275281
var current = node1;
276282

277-
while (current.Kind == SyntaxKind.MarkupElement && current is not null)
283+
while (current is MarkupElementSyntax && current is not null)
278284
{
279285
if (current.Span.Contains(node2.Span))
280286
{
@@ -297,7 +303,7 @@ private static void AddComponentDependenciesInRange(SyntaxNode root, int extract
297303
if (IsMarkupTagHelperElement(node, extractSpan))
298304
{
299305
var tagHelperInfo = GetTagHelperInfo(node);
300-
if (tagHelperInfo != null)
306+
if (tagHelperInfo is not null)
301307
{
302308
AddDependenciesFromTagHelperInfo(tagHelperInfo, components, actionParams);
303309
}
@@ -325,7 +331,7 @@ private static void AddDependenciesFromTagHelperInfo(TagHelperInfo tagHelperInfo
325331
{
326332
foreach (var descriptor in tagHelperInfo.BindingResult.Descriptors)
327333
{
328-
if (descriptor != null)
334+
if (descriptor is not null)
329335
{
330336
foreach (var metadata in descriptor.Metadata)
331337
{
@@ -340,4 +346,27 @@ metadata.Value is not null &&
340346
}
341347
}
342348
}
349+
350+
private static void GetUsedIdentifiers(SyntaxNode divNode, SyntaxNode documentRoot, ExtractToNewComponentCodeActionParams actionParams)
351+
{
352+
HashSet<string> identifiersInScope = [];
353+
HashSet<string> identifiersInBlock = [];
354+
355+
356+
foreach (var node in divNode.DescendantNodes().Where(static node => node.Kind is SyntaxKind.Identifier))
357+
{
358+
identifiersInScope.Add(node.GetContent());
359+
}
360+
361+
foreach (var codeBlock in documentRoot.DescendantNodes().Where(static node => node.Kind is SyntaxKind.RazorDirective))
362+
{
363+
foreach (var node in codeBlock.DescendantNodes().Where(static node => node.Kind is SyntaxKind.Identifier))
364+
{
365+
identifiersInBlock.Add(node.GetContent());
366+
}
367+
}
368+
369+
identifiersInBlock.IntersectWith(identifiersInScope);
370+
actionParams.UsedIdentifiers = identifiersInBlock;
371+
}
343372
}

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToNewComponentCodeActionProviderTest.cs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,67 @@ public async Task Handle_SinglePointSelection_ReturnsNotEmpty()
118118
Assert.NotEmpty(commandOrCodeActionContainer);
119119
}
120120

121+
[Fact]
122+
public async Task Handle_CodeInsideDiv_ScansCorrect()
123+
{
124+
// Arrange
125+
var documentPath = "c:/Test.cs";
126+
var contents = """
127+
@page "/"
128+
129+
<PageTitle>Home</PageTitle>
130+
131+
<$$div id="codeInside">
132+
@for(int idx = 0; idx < 10; idx++) {
133+
string s = someFunc(idx * myField);
134+
}
135+
</div>
136+
137+
<div id="parent">
138+
<div>
139+
<h1>Div a title</h1>
140+
<p>Div a par</p>
141+
</div>
142+
<div>
143+
<h1>Div b title</h1>
144+
<p>Div b par</p>
145+
</div>
146+
</div>
147+
148+
@code {
149+
public int myField = 7;
150+
151+
public string someFunc(int num) {
152+
return "Hello for number" + num;
153+
}
154+
}
155+
""";
156+
TestFileMarkupParser.GetPosition(contents, out contents, out var cursorPosition);
157+
158+
var request = new VSCodeActionParams()
159+
{
160+
TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) },
161+
Range = new Range(),
162+
Context = new VSInternalCodeActionContext()
163+
};
164+
165+
var location = new SourceLocation(cursorPosition, -1, -1);
166+
var context = CreateRazorCodeActionContext(request, location, documentPath, contents);
167+
168+
var provider = new ExtractToNewComponentCodeActionProvider(LoggerFactory);
169+
170+
// Act
171+
var commandOrCodeActionContainer = await provider.ProvideAsync(context, default);
172+
173+
// Assert
174+
Assert.NotEmpty(commandOrCodeActionContainer);
175+
var codeAction = Assert.Single(commandOrCodeActionContainer);
176+
var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize<RazorCodeActionResolutionParams>();
177+
Assert.NotNull(razorCodeActionResolutionParams);
178+
var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize<ExtractToNewComponentCodeActionParams>();
179+
Assert.NotNull(actionParams);
180+
}
181+
121182
[Fact]
122183
public async Task Handle_MultiPointSelection_ReturnsNotEmpty()
123184
{
@@ -176,8 +237,8 @@ public async Task Handle_MultiPointSelectionWithEndAtEdge_ReturnsCurrentElement(
176237
// Arrange
177238
var documentPath = "c:/Test.cs";
178239
var contents = """
179-
@namespace MarketApp.Pages.Product.Home
180240
@page "/"
241+
@namespace MarketApp.Pages.Product.Home
181242
182243
namespace MarketApp.Pages.Product.Home
183244

0 commit comments

Comments
 (0)