Skip to content

Commit 96221e5

Browse files
committed
Adjust indentation for component extraction and some more tests
1 parent 462062a commit 96221e5

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ internal sealed class ExtractToComponentCodeActionResolver(
9393
return null;
9494
}
9595

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+
96107
var start = codeDocument.Source.Text.Lines.GetLinePosition(selectionAnalysis.ExtractStart);
97108
var end = codeDocument.Source.Text.Lines.GetLinePosition(selectionAnalysis.ExtractEnd);
98109
var removeRange = new Range
@@ -119,7 +130,7 @@ internal sealed class ExtractToComponentCodeActionResolver(
119130
}.Uri;
120131

121132
var componentName = Path.GetFileNameWithoutExtension(componentPath);
122-
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);
123134

124135
if (newComponentResult is null)
125136
{
@@ -499,6 +510,8 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
499510
Uri componentUri,
500511
DocumentContext documentContext,
501512
Range relevantRange,
513+
Uri newComponentUri,
514+
string whitespace,
502515
CancellationToken cancellationToken)
503516
{
504517
var contents = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
@@ -527,6 +540,18 @@ private static void AddUsingFromTagHelperInfo(TagHelperInfo tagHelperInfo, HashS
527540
selectionAnalysis.ExtractEnd - selectionAnalysis.ExtractStart))
528541
.Trim();
529542

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);
530555
newFileContentBuilder.Append(extractedContents);
531556

532557
// Get CSharpStatements within component
@@ -809,6 +834,8 @@ private static HashSet<FieldSymbolicInfo> GetFieldsInContext(FieldSymbolicInfo[]
809834
return fieldsInContext;
810835
}
811836

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.
812839
private static string GenerateForwardedConstantFields(HashSet<FieldSymbolicInfo> relevantFields, string? sourceDocumentFileName)
813840
{
814841
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
@@ -1200,6 +1200,148 @@ await ValidateExtractComponentCodeActionAsync(
12001200
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
12011201
}
12021202

1203+
[Fact]
1204+
public async Task Handle_ExtractComponent_IndentedNode_ReturnsResult()
1205+
{
1206+
var input = """
1207+
<div id="parent">
1208+
<div>
1209+
<[||]div>
1210+
<div>
1211+
<p>Deeply nested par</p>
1212+
</div>
1213+
</div>
1214+
</div>
1215+
</div>
1216+
""";
1217+
1218+
var expectedRazorComponent = """
1219+
<div>
1220+
<div>
1221+
<p>Deeply nested par</p>
1222+
</div>
1223+
</div>
1224+
""";
1225+
1226+
await ValidateExtractComponentCodeActionAsync(
1227+
input,
1228+
expectedRazorComponent,
1229+
ExtractToComponentTitle,
1230+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1231+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1232+
}
1233+
1234+
[Fact]
1235+
public async Task Handle_ExtractComponent_IndentedSiblingNodes_ReturnsResult()
1236+
{
1237+
var input = """
1238+
<div id="parent">
1239+
<div>
1240+
<div>
1241+
<div>
1242+
<[|div>
1243+
<p>Deeply nested par</p>
1244+
</div>
1245+
<div>
1246+
<p>Deeply nested par</p>
1247+
</div>
1248+
<div>
1249+
<p>Deeply nested par</p>
1250+
</div>
1251+
<div>
1252+
<p>Deeply nested par</p>
1253+
</div|]>
1254+
</div>
1255+
</div>
1256+
</div>
1257+
</div>
1258+
""";
1259+
1260+
var expectedRazorComponent = """
1261+
<div>
1262+
<p>Deeply nested par</p>
1263+
</div>
1264+
<div>
1265+
<p>Deeply nested par</p>
1266+
</div>
1267+
<div>
1268+
<p>Deeply nested par</p>
1269+
</div>
1270+
<div>
1271+
<p>Deeply nested par</p>
1272+
</div>
1273+
""";
1274+
1275+
await ValidateExtractComponentCodeActionAsync(
1276+
input,
1277+
expectedRazorComponent,
1278+
ExtractToComponentTitle,
1279+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1280+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1281+
}
1282+
1283+
[Fact]
1284+
public async Task Handle_ExtractComponent_IndentedStartNodeContainsEndNode_ReturnsResult()
1285+
{
1286+
var input = """
1287+
<div id="parent">
1288+
<div>
1289+
<[|div>
1290+
<div>
1291+
<p>Deeply nested par</p|]>
1292+
</div>
1293+
</div>
1294+
</div>
1295+
</div>
1296+
""";
1297+
1298+
var expectedRazorComponent = """
1299+
<div>
1300+
<div>
1301+
<p>Deeply nested par</p>
1302+
</div>
1303+
</div>
1304+
""";
1305+
1306+
await ValidateExtractComponentCodeActionAsync(
1307+
input,
1308+
expectedRazorComponent,
1309+
ExtractToComponentTitle,
1310+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1311+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1312+
}
1313+
1314+
[Fact]
1315+
public async Task Handle_ExtractComponent_IndentedEndNodeContainsStartNode_ReturnsResult()
1316+
{
1317+
var input = """
1318+
<div id="parent">
1319+
<div>
1320+
<div>
1321+
<div>
1322+
<[|p>Deeply nested par</p>
1323+
</div>
1324+
</div|]>
1325+
</div>
1326+
</div>
1327+
""";
1328+
1329+
var expectedRazorComponent = """
1330+
<div>
1331+
<div>
1332+
<p>Deeply nested par</p>
1333+
</div>
1334+
</div>
1335+
""";
1336+
1337+
await ValidateExtractComponentCodeActionAsync(
1338+
input,
1339+
expectedRazorComponent,
1340+
ExtractToComponentTitle,
1341+
razorCodeActionProviders: [new ExtractToComponentCodeActionProvider(LoggerFactory)],
1342+
codeActionResolversCreator: CreateExtractComponentCodeActionResolver);
1343+
}
1344+
12031345
#endregion
12041346

12051347
private async Task ValidateCodeBehindFileAsync(

0 commit comments

Comments
 (0)