Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,5 @@ internal interface IDocumentMappingService

bool TryMapToCSharpDocumentPosition(RazorCSharpDocument csharpDocument, int razorIndex, out LinePosition csharpPosition, out int csharpIndex);

bool TryMapToCSharpPositionOrNext(RazorCSharpDocument csharpDocument, int razorIndex, out LinePosition csharpPosition, out int csharpIndex);

ImmutableArray<LinePositionSpan> GetCSharpSpansOverlappingRazorSpan(RazorCSharpDocument csharpDocument, LinePositionSpan razorSpan);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,6 @@ public static bool TryMapToCSharpDocumentPosition(this IDocumentMappingService s
return result;
}

public static bool TryMapToCSharpPositionOrNext(this IDocumentMappingService service, RazorCSharpDocument csharpDocument, int razorIndex, [NotNullWhen(true)] out Position? csharpPosition, out int csharpIndex)
{
var result = service.TryMapToCSharpPositionOrNext(csharpDocument, razorIndex, out var csharpLinePosition, out csharpIndex);
csharpPosition = result ? csharpLinePosition.ToPosition() : null;
return result;
}

/// <summary>
/// Convenience method to map from Razor to C#, which checks both impl and decl documents
/// </summary>
Expand Down Expand Up @@ -126,4 +119,26 @@ public static bool TryMapToCSharpDocumentLinePosition(this IDocumentMappingServi

return false;
}

/// <summary>
/// Convenience method to map from Razor to C#, which checks both impl and decl documents
/// </summary>
public static bool TryMapToCSharpDocumentLinePositionSpan(this IDocumentMappingService service, RazorCodeDocument codeDocument, LinePositionSpan razorRange, out LinePositionSpan csharpRange, out bool inDeclDocument)
{
inDeclDocument = false;
if (service.TryMapToCSharpDocumentRange(codeDocument.GetRequiredCSharpDocument(declarationDocument: false), razorRange, out csharpRange))
{
return true;
}

if (codeDocument.GetCSharpDocument(declarationDocument: true) is { } declDocument &&
service.TryMapToCSharpDocumentRange(declDocument, razorRange, out csharpRange))
{
inDeclDocument = true;
return true;
}

csharpRange = default;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ protected override IRemoteDataTipRangeService CreateService(in ServiceArgs args)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var razorIndex = codeDocument.Source.Text.GetRequiredAbsoluteIndex(position);
var csharpDocument = codeDocument.GetRequiredImplCSharpDocument();

if (!_documentMappingService.TryMapToCSharpDocumentPosition(csharpDocument, razorIndex, out var csharpPosition, out _))
if (!_documentMappingService.TryMapToCSharpDocumentLinePosition(codeDocument, razorIndex, out var csharpPosition, out _, out var inDeclDocument))
{
return NoFurtherHandling;
}

var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false);
var csharpDocument = codeDocument.GetRequiredCSharpDocument(inDeclDocument);
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(inDeclDocument, cancellationToken).ConfigureAwait(false);

var csharpResult = await DataTipRangeHandler.GetDataTipRangeAsync(generatedDocument, csharpPosition, cancellationToken).ConfigureAwait(false);
if (csharpResult?.ExpressionRange is null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -37,18 +38,18 @@ protected override IRemoteDebugInfoService CreateService(in ServiceArgs args)
public async ValueTask<LinePositionSpan?> ValidateBreakableRangeAsync(RemoteDocumentContext context, LinePositionSpan span, CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
var csharpDocument = codeDocument.GetRequiredImplCSharpDocument();

if (!_documentMappingService.TryMapToCSharpDocumentRange(csharpDocument, span, out var mappedSpan))
if (!_documentMappingService.TryMapToCSharpDocumentLinePositionSpan(codeDocument, span, out var mappedSpan, out var inDeclDocument))
{
return null;
}

var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false);
var csharpDocument = codeDocument.GetRequiredCSharpDocument(inDeclDocument);
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync(inDeclDocument, cancellationToken).ConfigureAwait(false);

var result = await GetBreakableRangeAsync(generatedDocument, mappedSpan, cancellationToken).ConfigureAwait(false);
if (result is { } csharpSpan &&
_documentMappingService.TryMapToRazorDocumentRange(codeDocument.GetRequiredImplCSharpDocument(), csharpSpan, MappingBehavior.Inclusive, out var hostSpan))
_documentMappingService.TryMapToRazorDocumentRange(csharpDocument, csharpSpan, MappingBehavior.Inclusive, out var hostSpan))
{
return hostSpan;
}
Expand All @@ -66,23 +67,22 @@ protected override IRemoteDebugInfoService CreateService(in ServiceArgs args)
private async ValueTask<LinePositionSpan?> ResolveBreakpointRangeAsync(RemoteDocumentContext context, LinePosition position, CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
if (!TryGetUsableProjectedIndex(codeDocument, position, out var projectedIndex))
if (!TryGetUsableProjectedIndex(codeDocument, position, out var projectedIndex, out var csharpDocument))
{
return null;
}

// Now ask Roslyn to adjust the breakpoint to a valid location in the code
var syntaxTree = await context.Snapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = await context.Snapshot.GetCSharpSyntaxTreeAsync(csharpDocument.IsDeclarationDocument, cancellationToken).ConfigureAwait(false);
if (!BreakpointSpans.TryGetBreakpointSpan(syntaxTree, projectedIndex, cancellationToken, out var csharpBreakpointSpan))
{
return null;
}

var csharpText = codeDocument.GetCSharpSourceText();
var projectedRange = csharpText.GetLinePositionSpan(csharpBreakpointSpan);
var projectedRange = csharpDocument.Text.GetLinePositionSpan(csharpBreakpointSpan);

// Inclusive mapping means we are lenient to portions of the breakpoint that might be outside of use code in the Razor file
if (!_documentMappingService.TryMapToRazorDocumentRange(codeDocument.GetRequiredImplCSharpDocument(), projectedRange, MappingBehavior.Inclusive, out var hostDocumentRange))
if (!_documentMappingService.TryMapToRazorDocumentRange(csharpDocument, projectedRange, MappingBehavior.Inclusive, out var hostDocumentRange))
{
return null;
}
Expand All @@ -102,32 +102,33 @@ protected override IRemoteDebugInfoService CreateService(in ServiceArgs args)
private async ValueTask<string[]?> ResolveProximityExpressionsAsync(RemoteDocumentContext context, LinePosition position, CancellationToken cancellationToken)
{
var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
if (!TryGetUsableProjectedIndex(codeDocument, position, out var projectedIndex))
if (!TryGetUsableProjectedIndex(codeDocument, position, out var projectedIndex, out var csharpDocument))
{
return null;
}

// Now ask Roslyn to adjust the breakpoint to a valid location in the code
var syntaxTree = await context.Snapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = await context.Snapshot.GetCSharpSyntaxTreeAsync(csharpDocument.IsDeclarationDocument, cancellationToken).ConfigureAwait(false);
var result = CSharpProximityExpressionsService.GetProximityExpressions(syntaxTree, projectedIndex, cancellationToken);

return result?.ToArray();
}

private bool TryGetUsableProjectedIndex(RazorCodeDocument codeDocument, LinePosition hostDocumentPosition, out int projectedIndex)
private bool TryGetUsableProjectedIndex(RazorCodeDocument codeDocument, LinePosition hostDocumentPosition, out int projectedIndex, [NotNullWhen(true)] out RazorCSharpDocument? csharpDocument)
{
csharpDocument = null;
projectedIndex = 0;

var sourceText = codeDocument.Source.Text;
var hostDocumentIndex = sourceText.GetPosition(hostDocumentPosition);
var syntaxRoot = codeDocument.GetRequiredSyntaxRoot();
var csharpDocument = codeDocument.GetRequiredImplCSharpDocument();

// We want to find a position that maps to C# on the same line as the original request, but we might have to skip over
// some Razor/HTML nodes to find valid C#.
while (sourceText.GetLinePosition(hostDocumentIndex).Line == hostDocumentPosition.Line)
while (hostDocumentIndex <= sourceText.Length &&
sourceText.GetLinePosition(hostDocumentIndex).Line == hostDocumentPosition.Line)
{
if (_documentMappingService.TryMapToCSharpPositionOrNext(csharpDocument, hostDocumentIndex, out _, out projectedIndex))
if (TryMapToCSharpPositionOrNext(codeDocument, hostDocumentIndex, out _, out projectedIndex, out csharpDocument))
{
if (syntaxRoot.FindInnermostNode(hostDocumentIndex) is not { } node)
{
Expand All @@ -145,8 +146,13 @@ private bool TryGetUsableProjectedIndex(RazorCodeDocument codeDocument, LinePosi
hostDocumentIndex = node.Span.End + 1;
}

if (hostDocumentIndex > sourceText.Length)
{
return false;
}

// See if there is more C# on the line to map to, for example "$$<p>@DateTime.Now</p>"
if (!_documentMappingService.TryMapToCSharpPositionOrNext(csharpDocument, hostDocumentIndex, out _, out projectedIndex))
if (!TryMapToCSharpPositionOrNext(codeDocument, hostDocumentIndex, out _, out projectedIndex, out csharpDocument))
{
return false;
}
Expand All @@ -161,6 +167,69 @@ private bool TryGetUsableProjectedIndex(RazorCodeDocument codeDocument, LinePosi
return false;
}

private bool TryMapToCSharpPositionOrNext(RazorCodeDocument codeDocument, int razorIndex, out LinePosition csharpPosition, out int csharpIndex, [NotNullWhen(true)] out RazorCSharpDocument? csharpDocument)
{
// Fast path: Can we just directly map?
if (_documentMappingService.TryMapToCSharpDocumentLinePosition(codeDocument, razorIndex, out csharpPosition, out csharpIndex, out var inDeclDocument))
{
csharpDocument = codeDocument.GetRequiredCSharpDocument(inDeclDocument);
return true;
}

// If we can't map directly, then we need to find the next C# on the line, but each transition to C# could map to a different
// C# document, which makes this a little more complicated than you might think.

var hostDocumentLine = codeDocument.Source.Text.GetLinePosition(razorIndex).Line;

var selectedDocument = codeDocument.GetRequiredCSharpDocument(declarationDocument: false);
var selectedMapping = GetNextMapping(selectedDocument, razorIndex, hostDocumentLine);

if (codeDocument.GetCSharpDocument(declarationDocument: true) is { } declDocument)
{
var declMapping = GetNextMapping(declDocument, razorIndex, hostDocumentLine);
if (declMapping is not null &&
(selectedMapping is null ||
declMapping.OriginalSpan.AbsoluteIndex < selectedMapping.OriginalSpan.AbsoluteIndex))
{
selectedMapping = declMapping;
selectedDocument = declDocument;
}
}

if (selectedMapping is null)
{
csharpDocument = null;
csharpPosition = default;
csharpIndex = default;
return false;
}

csharpDocument = selectedDocument;
csharpIndex = selectedMapping.GeneratedSpan.AbsoluteIndex;
csharpPosition = selectedDocument.Text.GetLinePosition(csharpIndex);
return true;

static SourceMapping? GetNextMapping(RazorCSharpDocument document, int razorIndex, int hostDocumentLine)
{
foreach (var mapping in document.SourceMappingsSortedByOriginal)
{
if (mapping.OriginalSpan.AbsoluteIndex < razorIndex)
{
continue;
}

if (mapping.OriginalSpan.LineIndex != hostDocumentLine)
{
break;
}

return mapping;
}

return null;
}
}

private static async Task<LinePositionSpan?> GetBreakableRangeAsync(
Document document,
LinePositionSpan span,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,18 +167,11 @@ public bool TryMapToRazorDocumentPosition(RazorCSharpDocument csharpDocument, in
return false;
}

public bool TryMapToCSharpPositionOrNext(RazorCSharpDocument csharpDocument, int hostDocumentIndex, out LinePosition generatedPosition, out int generatedIndex)
=> TryMapToCSharpDocumentPositionInternal(csharpDocument, hostDocumentIndex, nextCSharpPositionOnFailure: true, out generatedPosition, out generatedIndex);

public bool TryMapToCSharpDocumentPosition(RazorCSharpDocument csharpDocument, int hostDocumentIndex, out LinePosition generatedPosition, out int generatedIndex)
=> TryMapToCSharpDocumentPositionInternal(csharpDocument, hostDocumentIndex, nextCSharpPositionOnFailure: false, out generatedPosition, out generatedIndex);
=> TryMapToCSharpDocumentPositionInternal(csharpDocument, hostDocumentIndex, out generatedPosition, out generatedIndex);

private static bool TryMapToCSharpDocumentPositionInternal(RazorCSharpDocument csharpDocument, int razorIndex, bool nextCSharpPositionOnFailure, out LinePosition csharpPosition, out int csharpIndex)
private static bool TryMapToCSharpDocumentPositionInternal(RazorCSharpDocument csharpDocument, int razorIndex, out LinePosition csharpPosition, out int csharpIndex)
{
SourceMapping? nextCSharpMapping = null;

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

foreach (var mapping in csharpDocument.SourceMappingsSortedByOriginal)
{
var originalSpan = mapping.OriginalSpan;
Expand All @@ -195,31 +188,13 @@ private static bool TryMapToCSharpDocumentPositionInternal(RazorCSharpDocument c
return true;
}
}
else if (nextCSharpPositionOnFailure &&
mapping.OriginalSpan.LineIndex == hostDocumentLine &&
mapping.OriginalSpan.AbsoluteIndex >= razorIndex &&
(nextCSharpMapping is null || mapping.OriginalSpan.AbsoluteIndex < nextCSharpMapping.OriginalSpan.AbsoluteIndex))
{
// The "next" C# location is only valid if it is on the same line in the source document
// as the requested position, and before than any previous "next" C# position we have found,
// comparing their original positions. Due to source mappings being ordered by generated span,
// not original span, its possible for things to be out of order.
nextCSharpMapping = mapping;
}
else
{
// This span (and all following) are after the area we're interested in
break;
}
}

if (nextCSharpPositionOnFailure && nextCSharpMapping is not null)
{
csharpIndex = nextCSharpMapping.GeneratedSpan.AbsoluteIndex;
csharpPosition = csharpDocument.Text.GetLinePosition(csharpIndex);
return true;
}

csharpPosition = default;
csharpIndex = default;
return false;
Expand Down
Loading
Loading