Skip to content

Commit b786066

Browse files
authored
Modify RazorCSharpDocument to keep SourceMappings sorted by both original and generated document positions (#12772)
* Modify RazorCSharpDocument to keep sourcemappings sorted by both Original and Generated document indices This class previously just kept an array that was sorted by generated positions. Many callers would benefit instead from an array sorted by original (html) posititions. This allows many loops to short-circuit once they've moved beyond any mappings that could be of interest to them. This also renamed the original array to indicate it's sorted by generated positions, and to validate that upon it's input, as some callers were assuming that. By renaming this variable, it will hopefully make callers 1) Aware that the input is sorted and may be short-circuited or binary searched 2) Aware what it is sorted by (eg: formatting had some code that was assuming the array was sorted by original position) * Be more resilient to bad input * Use Debug.Fail * Fix test to do different validation in debug/release
1 parent 4abe6ad commit b786066

File tree

20 files changed

+176
-62
lines changed

20 files changed

+176
-62
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCSharpDocument.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Immutable;
5+
using System.Diagnostics;
56
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
67
using Microsoft.CodeAnalysis.Text;
78

@@ -12,7 +13,8 @@ public sealed class RazorCSharpDocument
1213
public RazorCodeDocument CodeDocument { get; }
1314
public SourceText Text { get; }
1415
public ImmutableArray<RazorDiagnostic> Diagnostics { get; }
15-
public ImmutableArray<SourceMapping> SourceMappings { get; }
16+
public ImmutableArray<SourceMapping> SourceMappingsSortedByGenerated { get; }
17+
public ImmutableArray<SourceMapping> SourceMappingsSortedByOriginal { get; }
1618
public ImmutableArray<LinePragma> LinePragmas { get; }
1719

1820
public RazorCSharpDocument(
@@ -29,7 +31,21 @@ public RazorCSharpDocument(
2931
Text = text;
3032

3133
Diagnostics = diagnostics.NullToEmpty();
32-
SourceMappings = sourceMappings.NullToEmpty();
34+
SourceMappingsSortedByGenerated = sourceMappings.NullToEmpty();
35+
36+
// Verify given source mappings are ordered by their generated spans
37+
for (var i = 0; i < SourceMappingsSortedByGenerated.Length - 1; i++)
38+
{
39+
if (SourceMappingsSortedByGenerated[i].GeneratedSpan.CompareByStartThenLength(SourceMappingsSortedByGenerated[i + 1].GeneratedSpan) > 0)
40+
{
41+
Debug.Fail("input not sorted");
42+
43+
SourceMappingsSortedByGenerated = SourceMappingsSortedByGenerated.Sort(static (m1, m2) => m1.GeneratedSpan.CompareByStartThenLength(m2.GeneratedSpan));
44+
break;
45+
}
46+
}
47+
48+
SourceMappingsSortedByOriginal = SourceMappingsSortedByGenerated.Sort(static (m1, m2) => m1.OriginalSpan.CompareByStartThenLength(m2.OriginalSpan));
3349
LinePragmas = linePragmas.NullToEmpty();
3450
}
3551
}

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/SourceSpan.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,15 @@ internal readonly SourceSpan Slice(int startIndex, int length)
115115
{
116116
return !left.Equals(right);
117117
}
118+
119+
public readonly int CompareByStartThenLength(SourceSpan other)
120+
{
121+
var cmpByStart = AbsoluteIndex.CompareTo(other.AbsoluteIndex);
122+
if (cmpByStart != 0)
123+
{
124+
return cmpByStart;
125+
}
126+
127+
return Length.CompareTo(other.Length);
128+
}
118129
}

src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDocumentMappingBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private bool TryMapToHostDocumentPosition(RazorCSharpDocument csharpDocument, in
129129
throw new ArgumentNullException(nameof(csharpDocument));
130130
}
131131

132-
foreach (var mapping in csharpDocument.SourceMappings)
132+
foreach (var mapping in csharpDocument.SourceMappingsSortedByGenerated)
133133
{
134134
var generatedSpan = mapping.GeneratedSpan;
135135
var generatedAbsoluteIndex = generatedSpan.AbsoluteIndex;

src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.LegacyEditor/RazorWrapperFactory.CodeDocumentWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public ImmutableArray<TagHelperSpan> GetTagHelperSpans()
5555

5656
public ImmutableArray<RazorSourceMapping> GetSourceMappings()
5757
{
58-
var mappings = Object.GetRequiredCSharpDocument().SourceMappings;
58+
var mappings = Object.GetRequiredCSharpDocument().SourceMappingsSortedByGenerated;
5959

6060
return WrapAll<SourceMapping, RazorSourceMapping>(mappings, ConvertSourceMapping);
6161
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentExcerpt/DocumentExcerptHelper.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,34 @@ internal static class DocumentExcerptHelper
1818
public static async Task<ImmutableArray<ClassifiedSpan>.Builder> ClassifyPreviewAsync(
1919
TextSpan excerptSpan,
2020
Document generatedDocument,
21-
ImmutableArray<SourceMapping> mappings,
21+
ImmutableArray<SourceMapping> mappingsSortedByOriginal,
2222
RazorClassificationOptionsWrapper options,
2323
CancellationToken cancellationToken)
2424
{
2525
var builder = ImmutableArray.CreateBuilder<ClassifiedSpan>();
2626

27-
var sorted = mappings.Sort((x, y) => x.OriginalSpan.AbsoluteIndex.CompareTo(y.OriginalSpan.AbsoluteIndex));
28-
2927
// The algorithm here is to iterate through the source mappings (sorted) and use the C# classifier
3028
// on the spans that are known to be C#. For the spans that are not known to be C# then
3129
// we just treat them as text since we'd don't currently have our own classifications.
3230

33-
var remainingSpan = excerptSpan;
34-
foreach (var span in sorted)
31+
if (excerptSpan.Length == 0)
3532
{
36-
if (excerptSpan.Length == 0)
37-
{
38-
break;
39-
}
33+
return builder;
34+
}
4035

36+
var remainingSpan = excerptSpan;
37+
foreach (var span in mappingsSortedByOriginal)
38+
{
4139
var primarySpan = span.OriginalSpan.AsTextSpan();
4240
if (primarySpan.Intersection(remainingSpan) is not TextSpan intersection)
4341
{
44-
// This span is outside the area we're interested in.
42+
if (primarySpan.Start > remainingSpan.End)
43+
{
44+
// This span (and all following) are after the area we're interested in
45+
break;
46+
}
47+
48+
// This span precedes the area we're interested in
4549
continue;
4650
}
4751

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/AbstractDocumentMappingService.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public ImmutableArray<LinePositionSpan> GetCSharpSpansOverlappingRazorSpan(Razor
264264

265265
using var builder = new PooledArrayBuilder<LinePositionSpan>();
266266

267-
foreach (var mapping in csharpDocument.SourceMappings)
267+
foreach (var mapping in csharpDocument.SourceMappingsSortedByOriginal)
268268
{
269269
var originalSpan = mapping.OriginalSpan.ToLinePositionSpan();
270270

@@ -274,18 +274,19 @@ public ImmutableArray<LinePositionSpan> GetCSharpSpansOverlappingRazorSpan(Razor
274274

275275
builder.Add(generatedSpan);
276276
}
277+
else if (originalSpan.Start > razorSpan.End)
278+
{
279+
// This span (and all following) are after the area we're interested in
280+
break;
281+
}
277282
}
278283

279284
return builder.ToImmutableAndClear();
280285
}
281286

282287
public bool TryMapToRazorDocumentPosition(RazorCSharpDocument csharpDocument, int csharpIndex, out LinePosition razorPosition, out int razorIndex)
283288
{
284-
var sourceMappings = csharpDocument.SourceMappings;
285-
286-
// We expect source mappings to be ordered by their generated document absolute index, because that is how the compiler creates them: As it
287-
// outputs the generated file to the text write.
288-
Debug.Assert(sourceMappings.SequenceEqual(sourceMappings.OrderBy(s => s.GeneratedSpan.AbsoluteIndex)));
289+
var sourceMappings = csharpDocument.SourceMappingsSortedByGenerated;
289290

290291
var index = sourceMappings.BinarySearchBy(csharpIndex, static (mapping, generatedDocumentIndex) =>
291292
{
@@ -334,7 +335,7 @@ private static bool TryMapToCSharpDocumentPositionInternal(RazorCSharpDocument c
334335

335336
var hostDocumentLine = csharpDocument.CodeDocument.Source.Text.GetLinePosition(razorIndex).Line;
336337

337-
foreach (var mapping in csharpDocument.SourceMappings)
338+
foreach (var mapping in csharpDocument.SourceMappingsSortedByOriginal)
338339
{
339340
var originalSpan = mapping.OriginalSpan;
340341
var originalAbsoluteIndex = originalSpan.AbsoluteIndex;
@@ -361,6 +362,11 @@ private static bool TryMapToCSharpDocumentPositionInternal(RazorCSharpDocument c
361362
// not original span, its possible for things to be out of order.
362363
nextCSharpMapping = mapping;
363364
}
365+
else
366+
{
367+
// This span (and all following) are after the area we're interested in
368+
break;
369+
}
364370
}
365371

366372
if (nextCSharpPositionOnFailure && nextCSharpMapping is not null)
@@ -434,23 +440,24 @@ private bool TryMapToRazorDocumentRangeInclusive(RazorCSharpDocument csharpDocum
434440
}
435441

436442
using var _1 = ListPool<SourceMapping>.GetPooledObject(out var candidateMappings);
443+
var sourceMappings = csharpDocument.SourceMappingsSortedByGenerated;
437444
if (startMappedDirectly)
438445
{
439446
// Start of generated range intersects with a mapping
440447
candidateMappings.AddRange(
441-
csharpDocument.SourceMappings.Where(mapping => IntersectsWith(startIndex, mapping.GeneratedSpan)));
448+
sourceMappings.Where(mapping => IntersectsWith(startIndex, mapping.GeneratedSpan)));
442449
}
443450
else if (endMappedDirectly)
444451
{
445452
// End of generated range intersects with a mapping
446453
candidateMappings.AddRange(
447-
csharpDocument.SourceMappings.Where(mapping => IntersectsWith(endIndex, mapping.GeneratedSpan)));
454+
sourceMappings.Where(mapping => IntersectsWith(endIndex, mapping.GeneratedSpan)));
448455
}
449456
else
450457
{
451458
// Our range does not intersect with any mapping; we should see if it overlaps generated locations
452459
candidateMappings.AddRange(
453-
csharpDocument.SourceMappings
460+
sourceMappings
454461
.Where(mapping => Overlaps(csharpSourceText.GetTextSpan(csharpRange), mapping.GeneratedSpan)));
455462
}
456463

@@ -503,20 +510,21 @@ private bool TryMapToRazorDocumentRangeInferred(RazorCSharpDocument csharpDocume
503510
var generatedRangeAsSpan = csharpSourceText.GetTextSpan(csharpRange);
504511
SourceMapping? mappingBeforeGeneratedRange = null;
505512
SourceMapping? mappingAfterGeneratedRange = null;
513+
var sourceMappings = csharpDocument.SourceMappingsSortedByGenerated;
506514

507-
for (var i = csharpDocument.SourceMappings.Length - 1; i >= 0; i--)
515+
for (var i = sourceMappings.Length - 1; i >= 0; i--)
508516
{
509-
var sourceMapping = csharpDocument.SourceMappings[i];
517+
var sourceMapping = sourceMappings[i];
510518
var sourceMappingEnd = sourceMapping.GeneratedSpan.AbsoluteIndex + sourceMapping.GeneratedSpan.Length;
511519
if (generatedRangeAsSpan.Start >= sourceMappingEnd)
512520
{
513521
// This is the source mapping that's before us!
514522
mappingBeforeGeneratedRange = sourceMapping;
515523

516-
if (i + 1 < csharpDocument.SourceMappings.Length)
524+
if (i + 1 < sourceMappings.Length)
517525
{
518526
// We're not at the end of the document there's another source mapping after us
519-
mappingAfterGeneratedRange = csharpDocument.SourceMappings[i + 1];
527+
mappingAfterGeneratedRange = sourceMappings[i + 1];
520528
}
521529

522530
break;

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/RazorEditHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@ internal static partial class RazorEditHelper
2222
{
2323
internal static bool TryGetMappedSpan(TextSpan span, SourceText source, RazorCSharpDocument output, out LinePositionSpan linePositionSpan, out TextSpan mappedSpan)
2424
{
25-
foreach (var mapping in output.SourceMappings)
25+
foreach (var mapping in output.SourceMappingsSortedByGenerated)
2626
{
27-
var original = mapping.OriginalSpan.AsTextSpan();
2827
var generated = mapping.GeneratedSpan.AsTextSpan();
2928

3029
if (!generated.Contains(span))
3130
{
31+
if (generated.Start > span.End)
32+
{
33+
// This span (and all following) are after the area we're interested in
34+
break;
35+
}
36+
3237
// If the search span isn't contained within the generated span, it is not a match.
3338
// A C# identifier won't cover multiple generated spans.
3439
continue;
@@ -39,6 +44,7 @@ internal static bool TryGetMappedSpan(TextSpan span, SourceText source, RazorCSh
3944
if (leftOffset >= 0 && rightOffset <= 0)
4045
{
4146
// This span mapping contains the span.
47+
var original = mapping.OriginalSpan.AsTextSpan();
4248
mappedSpan = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset));
4349
linePositionSpan = source.GetLinePositionSpan(mappedSpan);
4450
return true;

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorCodeDocumentExtensions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument,
6161
var csharpDoc = codeDocument.GetRequiredCSharpDocument();
6262

6363
// We want to find the min and max C# source mapping that corresponds with our Razor range.
64-
foreach (var mapping in csharpDoc.SourceMappings)
64+
foreach (var mapping in csharpDoc.SourceMappingsSortedByOriginal)
6565
{
6666
var mappedTextSpan = mapping.OriginalSpan.AsTextSpan();
6767

@@ -78,6 +78,11 @@ public static bool TryGetMinimalCSharpRange(this RazorCodeDocument codeDocument,
7878
maxGeneratedSpan = mapping.GeneratedSpan;
7979
}
8080
}
81+
else if (mappedTextSpan.Start > textSpan.End)
82+
{
83+
// This span (and all following) are after the area we're interested in
84+
break;
85+
}
8186
}
8287

8388
// Create a new projected range based on our calculated min/max source spans.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.CSharpDocumentGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public void Generate()
195195
using var _ = StringBuilderPool.GetPooledObject(out var additionalLinesBuilder);
196196

197197
var root = _codeDocument.GetRequiredSyntaxRoot();
198-
var sourceMappings = _codeDocument.GetRequiredCSharpDocument().SourceMappings;
198+
var sourceMappings = _codeDocument.GetRequiredCSharpDocument().SourceMappingsSortedByOriginal;
199199
var iMapping = 0;
200200
foreach (var line in _sourceText.Lines)
201201
{

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public async Task<ImmutableArray<TextChange>> ExecuteAsync(FormattingContext con
3333
// Process changes from previous passes
3434
var changedText = context.SourceText.WithChanges(changes);
3535
var changedContext = await context.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false);
36-
context.Logger?.LogObject("SourceMappings", changedContext.CodeDocument.GetRequiredCSharpDocument().SourceMappings);
36+
context.Logger?.LogObject("SourceMappings", changedContext.CodeDocument.GetRequiredCSharpDocument().SourceMappingsSortedByGenerated);
3737

3838
var csharpSyntaxTrue = await changedContext.CurrentSnapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
3939
var csharpSyntaxRoot = await csharpSyntaxTrue.GetRootAsync(cancellationToken).ConfigureAwait(false);

0 commit comments

Comments
 (0)