Skip to content

Commit be3686f

Browse files
authored
Including @using for Out-of-Scope Razor Component References (#10651)
### Summary of the changes Addition to feature wherein razor component dependencies are now identified, and their corresponding `@usings` are now also extracted into the new document. ## **_Important!_** Notes: Some changes in this PR are further rectified in #10760, such as: `AddComponentDependenciesInRange` does not take actionParams anymore. In general, most (if not all) of the internal methods that take `actionParams` as a parameter were moved to the resolver, where each method returns a collection of appropiate objects.
2 parents f1923ed + 851fb45 commit be3686f

21 files changed

+424
-50
lines changed

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Text.Json.Serialization;
67

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

9-
internal sealed class ExtractToNewComponentCodeActionParams
10+
// NOTE: As mentioned before, these have changed in future PRs, where much of the Provider logic was moved to the resolver.
11+
// The last three properties are not used in the current implementation.
12+
internal sealed class ExtractToComponentCodeActionParams
1013
{
1114
[JsonPropertyName("uri")]
1215
public required Uri Uri { get; set; }
16+
1317
[JsonPropertyName("extractStart")]
1418
public int ExtractStart { get; set; }
19+
1520
[JsonPropertyName("extractEnd")]
1621
public int ExtractEnd { get; set; }
22+
1723
[JsonPropertyName("namespace")]
1824
public required string Namespace { get; set; }
25+
26+
[JsonPropertyName("usingDirectives")]
27+
public required List<string> usingDirectives { get; set; }
1928
}

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

+88-19
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,21 @@
66
using System.Collections.Immutable;
77
using System.Diagnostics.CodeAnalysis;
88
using System.Linq;
9-
using System.Text;
109
using System.Threading;
1110
using System.Threading.Tasks;
1211
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;
1612
using Microsoft.AspNetCore.Razor.Language.Syntax;
1713
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
1814
using Microsoft.AspNetCore.Razor.Threading;
1915
using Microsoft.CodeAnalysis.Razor.Logging;
20-
using Microsoft.CodeAnalysis.Razor.Workspaces;
2116
using Microsoft.CodeAnalysis.Text;
2217
using Microsoft.VisualStudio.LanguageServer.Protocol;
23-
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
2418

2519
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;
2620

27-
internal sealed class ExtractToNewComponentCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider
21+
internal sealed class ExtractToComponentCodeActionProvider(ILoggerFactory loggerFactory) : IRazorCodeActionProvider
2822
{
29-
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToNewComponentCodeActionProvider>();
23+
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<ExtractToComponentCodeActionProvider>();
3024

3125
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
3226
{
@@ -51,14 +45,18 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
5145
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
5246
}
5347

54-
var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger);
55-
5648
// Make sure the selection starts on an element tag
49+
var (startElementNode, endElementNode) = GetStartAndEndElements(context, syntaxTree, _logger);
5750
if (startElementNode is null)
5851
{
5952
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
6053
}
6154

55+
if (endElementNode is null)
56+
{
57+
endElementNode = startElementNode;
58+
}
59+
6260
if (!TryGetNamespace(context.CodeDocument, out var @namespace))
6361
{
6462
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
@@ -68,14 +66,28 @@ public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeAct
6866

6967
ProcessSelection(startElementNode, endElementNode, actionParams);
7068

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+
7183
var resolutionParams = new RazorCodeActionResolutionParams()
7284
{
7385
Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction,
7486
Language = LanguageServerConstants.CodeActions.Languages.Razor,
7587
Data = actionParams,
7688
};
7789

78-
var codeAction = RazorCodeActionFactory.CreateExtractToNewComponent(resolutionParams);
90+
var codeAction = RazorCodeActionFactory.CreateExtractToComponent(resolutionParams);
7991
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([codeAction]);
8092
}
8193

@@ -95,6 +107,7 @@ private static (MarkupElementSyntax? Start, MarkupElementSyntax? End) GetStartAn
95107
}
96108

97109
var endElementNode = GetEndElementNode(context, syntaxTree);
110+
98111
return (startElementNode, endElementNode);
99112
}
100113

@@ -135,14 +148,15 @@ private static bool IsInsideProperHtmlContent(RazorCodeActionContext context, Ma
135148
return endOwner.FirstAncestorOrSelf<MarkupElementSyntax>();
136149
}
137150

138-
private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace)
151+
private static ExtractToComponentCodeActionParams CreateInitialActionParams(RazorCodeActionContext context, MarkupElementSyntax startElementNode, string @namespace)
139152
{
140-
return new ExtractToNewComponentCodeActionParams
153+
return new ExtractToComponentCodeActionParams
141154
{
142155
Uri = context.Request.TextDocument.Uri,
143156
ExtractStart = startElementNode.Span.Start,
144157
ExtractEnd = startElementNode.Span.End,
145-
Namespace = @namespace
158+
Namespace = @namespace,
159+
usingDirectives = []
146160
};
147161
}
148162

@@ -152,7 +166,7 @@ private static ExtractToNewComponentCodeActionParams CreateInitialActionParams(R
152166
/// <param name="startElementNode">The starting element of the selection.</param>
153167
/// <param name="endElementNode">The ending element of the selection, if it exists.</param>
154168
/// <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)
156170
{
157171
// If there's no end element, we can't process a multi-point selection
158172
if (endElementNode is null)
@@ -183,7 +197,7 @@ private static void ProcessSelection(MarkupElementSyntax startElementNode, Marku
183197
// </span>|}|}
184198
// </div>
185199
// In this case, we need to find the smallest set of complete elements that covers the entire selection.
186-
200+
187201
// Find the closest containing sibling pair that encompasses both the start and end elements
188202
var (extractStart, extractEnd) = FindContainingSiblingPair(startElementNode, endElementNode);
189203

@@ -207,7 +221,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
207221
{
208222
// Find the lowest common ancestor of both nodes
209223
var nearestCommonAncestor = FindNearestCommonAncestor(startNode, endNode);
210-
if (nearestCommonAncestor == null)
224+
if (nearestCommonAncestor is null)
211225
{
212226
return (null, null);
213227
}
@@ -223,7 +237,7 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
223237
{
224238
var childSpan = child.Span;
225239

226-
if (startContainingNode == null && childSpan.Contains(startSpan))
240+
if (startContainingNode is null && childSpan.Contains(startSpan))
227241
{
228242
startContainingNode = child;
229243
if (endContainingNode is not null)
@@ -245,7 +259,10 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
245259
{
246260
var current = node1;
247261

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)
249266
{
250267
if (current.Span.Contains(node2.Span))
251268
{
@@ -257,4 +274,56 @@ private static (SyntaxNode? Start, SyntaxNode? End) FindContainingSiblingPair(Sy
257274

258275
return null;
259276
}
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+
}
260329
}

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727

2828
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;
2929

30-
internal sealed class ExtractToNewComponentCodeActionResolver(
30+
internal sealed class ExtractToComponentCodeActionResolver
31+
(
3132
IDocumentContextFactory documentContextFactory,
3233
LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver
3334
{
@@ -44,7 +45,7 @@ internal sealed class ExtractToNewComponentCodeActionResolver(
4445
return null;
4546
}
4647

47-
var actionParams = JsonSerializer.Deserialize<ExtractToNewComponentCodeActionParams>(data.GetRawText());
48+
var actionParams = JsonSerializer.Deserialize<ExtractToComponentCodeActionParams>(data.GetRawText());
4849
if (actionParams is null)
4950
{
5051
return null;
@@ -90,7 +91,15 @@ internal sealed class ExtractToNewComponentCodeActionResolver(
9091
}
9192

9293
var componentName = Path.GetFileNameWithoutExtension(componentPath);
93-
var newComponentContent = text.GetSubTextString(new TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim();
94+
var newComponentContent = string.Empty;
95+
96+
newComponentContent += string.Join(Environment.NewLine, actionParams.usingDirectives);
97+
if (actionParams.usingDirectives.Count > 0)
98+
{
99+
newComponentContent += Environment.NewLine + Environment.NewLine; // Ensure there's a newline after the dependencies if any exist.
100+
}
101+
102+
newComponentContent += text.GetSubTextString(new CodeAnalysis.Text.TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim();
94103

95104
var start = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractStart);
96105
var end = componentDocument.Source.Text.Lines.GetLinePosition(actionParams.ExtractEnd);

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal static class RazorCodeActionFactory
1414
private readonly static Guid s_fullyQualifyComponentTelemetryId = new("3d9abe36-7d10-4e08-8c18-ad88baa9a923");
1515
private readonly static Guid s_createComponentFromTagTelemetryId = new("a28e0baa-a4d5-4953-a817-1db586035841");
1616
private readonly static Guid s_createExtractToCodeBehindTelemetryId = new("f63167f7-fdc6-450f-8b7b-b240892f4a27");
17-
private readonly static Guid s_createExtractToNewComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64");
17+
private readonly static Guid s_createExtractToComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64");
1818
private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef");
1919
private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939");
2020

@@ -68,15 +68,15 @@ public static RazorVSInternalCodeAction CreateExtractToCodeBehind(RazorCodeActio
6868
return codeAction;
6969
}
7070

71-
public static RazorVSInternalCodeAction CreateExtractToNewComponent(RazorCodeActionResolutionParams resolutionParams)
71+
public static RazorVSInternalCodeAction CreateExtractToComponent(RazorCodeActionResolutionParams resolutionParams)
7272
{
73-
var title = SR.ExtractTo_NewComponent_Title;
73+
var title = SR.ExtractTo_Component_Title;
7474
var data = JsonSerializer.SerializeToElement(resolutionParams);
7575
var codeAction = new RazorVSInternalCodeAction()
7676
{
7777
Title = title,
7878
Data = data,
79-
TelemetryId = s_createExtractToNewComponentTelemetryId,
79+
TelemetryId = s_createExtractToComponentTelemetryId,
8080
};
8181
return codeAction;
8282
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ public static void AddCodeActionsServices(this IServiceCollection services)
146146
// Razor Code actions
147147
services.AddSingleton<IRazorCodeActionProvider, ExtractToCodeBehindCodeActionProvider>();
148148
services.AddSingleton<IRazorCodeActionResolver, ExtractToCodeBehindCodeActionResolver>();
149-
services.AddSingleton<IRazorCodeActionProvider, ExtractToNewComponentCodeActionProvider>();
150-
services.AddSingleton<IRazorCodeActionResolver ,ExtractToNewComponentCodeActionResolver>();
149+
services.AddSingleton<IRazorCodeActionProvider, ExtractToComponentCodeActionProvider>();
150+
services.AddSingleton<IRazorCodeActionResolver ,ExtractToComponentCodeActionResolver>();
151151
services.AddSingleton<IRazorCodeActionProvider, ComponentAccessibilityCodeActionProvider>();
152152
services.AddSingleton<IRazorCodeActionResolver, CreateComponentCodeActionResolver>();
153153
services.AddSingleton<IRazorCodeActionResolver, AddUsingsCodeActionResolver>();

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx

+1-1
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@
183183
<data name="Statement" xml:space="preserve">
184184
<value>statement</value>
185185
</data>
186-
<data name="ExtractTo_NewComponent_Title" xml:space="preserve">
186+
<data name="ExtractTo_Component_Title" xml:space="preserve">
187187
<value>Extract element to new component</value>
188188
</data>
189189
</root>

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)