Skip to content

Commit 26301c4

Browse files
committed
Adjust indentation for component extraction and some more tests
1 parent 959f78b commit 26301c4

File tree

2 files changed

+171
-1
lines changed

2 files changed

+171
-1
lines changed

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
4141
using System.Reflection.Metadata.Ecma335;
4242
using Microsoft.VisualStudio.Utilities;
43+
using System.Text.RegularExpressions;
4344

4445
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;
4546

@@ -92,6 +93,17 @@ internal sealed class ExtractToComponentCodeActionResolver(
9293
return null;
9394
}
9495

96+
// For the purposes of determining the indentation of the extracted code, get the whitespace before the start of the selection.
97+
var whitespaceReferenceOwner = codeDocument.GetSyntaxTree().Root.FindInnermostNode(selectionAnalysis.ExtractStart, includeWhitespace: true).AssumeNotNull();
98+
var whitespaceReferenceNode = whitespaceReferenceOwner.FirstAncestorOrSelf<MarkupSyntaxNode>(node => node is MarkupElementSyntax or MarkupTagHelperElementSyntax);
99+
var whitespace = string.Empty;
100+
if (whitespaceReferenceNode.TryGetPreviousSibling(out var startPreviousSibling) && startPreviousSibling.ContainsOnlyWhitespace())
101+
{
102+
// Get the whitespace substring so we know how much to dedent the extracted code. Remove any carriage return and newline escape characters.
103+
whitespace = startPreviousSibling.ToFullString();
104+
whitespace = whitespace.Replace("\r", string.Empty).Replace("\n", string.Empty);
105+
}
106+
95107
var start = codeDocument.Source.Text.Lines.GetLinePosition(selectionAnalysis.ExtractStart);
96108
var end = codeDocument.Source.Text.Lines.GetLinePosition(selectionAnalysis.ExtractEnd);
97109
var removeRange = new Range
@@ -118,7 +130,7 @@ internal sealed class ExtractToComponentCodeActionResolver(
118130
}.Uri;
119131

120132
var componentName = Path.GetFileNameWithoutExtension(componentPath);
121-
var newComponentResult = await GenerateNewComponentAsync(selectionAnalysis, codeDocument, actionParams.Uri, documentContext, removeRange, cancellationToken).ConfigureAwait(false);
133+
var newComponentResult = await GenerateNewComponentAsync(selectionAnalysis, codeDocument, actionParams.Uri, documentContext, removeRange, newComponentUri, whitespace, cancellationToken).ConfigureAwait(false);
122134

123135
if (newComponentResult is null)
124136
{
@@ -498,6 +510,8 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
498510
Uri componentUri,
499511
DocumentContext documentContext,
500512
Range relevantRange,
513+
Uri newComponentUri,
514+
string whitespace,
501515
CancellationToken cancellationToken)
502516
{
503517
var contents = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
@@ -526,6 +540,18 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
526540
selectionAnalysis.ExtractEnd - selectionAnalysis.ExtractStart))
527541
.Trim();
528542

543+
// Go through each line of the extractedContents and remove the whitespace from the beginning of each line.
544+
var extractedLines = extractedContents.Split('\n');
545+
for (var i = 1; i < extractedLines.Length; i++)
546+
{
547+
var line = extractedLines[i];
548+
if (line.StartsWith(whitespace, StringComparison.Ordinal))
549+
{
550+
extractedLines[i] = line.Substring(whitespace.Length);
551+
}
552+
}
553+
554+
extractedContents = string.Join("\n", extractedLines);
529555
newFileContentBuilder.Append(extractedContents);
530556

531557
// Get CSharpStatements within component
@@ -808,6 +834,8 @@ private static HashSet<FieldSymbolicInfo> GetFieldsInContext(FieldSymbolicInfo[]
808834
return fieldsInContext;
809835
}
810836

837+
// By forwarded fields, I mean fields that are present in the extraction, but get directly added/copied to the extracted component's code block, instead of being passed as an attribute.
838+
// If you have naming suggestions that make more sense, please let me know.
811839
private static string GenerateForwardedConstantFields(HashSet<FieldSymbolicInfo> relevantFields, string? sourceDocumentFileName)
812840
{
813841
var builder = new StringBuilder();

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,6 +1167,148 @@ await ValidateExtractComponentCodeActionAsync(
11671167
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
11681168
}
11691169

1170+
[Fact]
1171+
public async Task Handle_ExtractComponent_IndentedNode_ReturnsResult()
1172+
{
1173+
var input = """
1174+
<div id="parent">
1175+
<div>
1176+
<[||]div>
1177+
<div>
1178+
<p>Deeply nested par</p>
1179+
</div>
1180+
</div>
1181+
</div>
1182+
</div>
1183+
""";
1184+
1185+
var expectedRazorComponent = """
1186+
<div>
1187+
<div>
1188+
<p>Deeply nested par</p>
1189+
</div>
1190+
</div>
1191+
""";
1192+
1193+
await ValidateExtractComponentCodeActionAsync(
1194+
input,
1195+
expectedRazorComponent,
1196+
ExtractToComponentTitle,
1197+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1198+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1199+
}
1200+
1201+
[Fact]
1202+
public async Task Handle_ExtractComponent_IndentedSiblingNodes_ReturnsResult()
1203+
{
1204+
var input = """
1205+
<div id="parent">
1206+
<div>
1207+
<div>
1208+
<div>
1209+
<[|div>
1210+
<p>Deeply nested par</p>
1211+
</div>
1212+
<div>
1213+
<p>Deeply nested par</p>
1214+
</div>
1215+
<div>
1216+
<p>Deeply nested par</p>
1217+
</div>
1218+
<div>
1219+
<p>Deeply nested par</p>
1220+
</div|]>
1221+
</div>
1222+
</div>
1223+
</div>
1224+
</div>
1225+
""";
1226+
1227+
var expectedRazorComponent = """
1228+
<div>
1229+
<p>Deeply nested par</p>
1230+
</div>
1231+
<div>
1232+
<p>Deeply nested par</p>
1233+
</div>
1234+
<div>
1235+
<p>Deeply nested par</p>
1236+
</div>
1237+
<div>
1238+
<p>Deeply nested par</p>
1239+
</div>
1240+
""";
1241+
1242+
await ValidateExtractComponentCodeActionAsync(
1243+
input,
1244+
expectedRazorComponent,
1245+
ExtractToComponentTitle,
1246+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1247+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1248+
}
1249+
1250+
[Fact]
1251+
public async Task Handle_ExtractComponent_IndentedStartNodeContainsEndNode_ReturnsResult()
1252+
{
1253+
var input = """
1254+
<div id="parent">
1255+
<div>
1256+
<[|div>
1257+
<div>
1258+
<p>Deeply nested par</p|]>
1259+
</div>
1260+
</div>
1261+
</div>
1262+
</div>
1263+
""";
1264+
1265+
var expectedRazorComponent = """
1266+
<div>
1267+
<div>
1268+
<p>Deeply nested par</p>
1269+
</div>
1270+
</div>
1271+
""";
1272+
1273+
await ValidateExtractComponentCodeActionAsync(
1274+
input,
1275+
expectedRazorComponent,
1276+
ExtractToComponentTitle,
1277+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1278+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1279+
}
1280+
1281+
[Fact]
1282+
public async Task Handle_ExtractComponent_IndentedEndNodeContainsStartNode_ReturnsResult()
1283+
{
1284+
var input = """
1285+
<div id="parent">
1286+
<div>
1287+
<div>
1288+
<div>
1289+
<[|p>Deeply nested par</p>
1290+
</div>
1291+
</div|]>
1292+
</div>
1293+
</div>
1294+
""";
1295+
1296+
var expectedRazorComponent = """
1297+
<div>
1298+
<div>
1299+
<p>Deeply nested par</p>
1300+
</div>
1301+
</div>
1302+
""";
1303+
1304+
await ValidateExtractComponentCodeActionAsync(
1305+
input,
1306+
expectedRazorComponent,
1307+
ExtractToComponentTitle,
1308+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1309+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1310+
}
1311+
11701312
#endregion
11711313

11721314
private async Task ValidateCodeBehindFileAsync(

0 commit comments

Comments
 (0)