192 lines
8.4 KiB
C#
192 lines
8.4 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Razor.Language;
|
|
using Microsoft.CodeAnalysis.Classification;
|
|
using Microsoft.CodeAnalysis.Host;
|
|
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
|
using Microsoft.CodeAnalysis.Text;
|
|
|
|
namespace Microsoft.CodeAnalysis.Razor
|
|
{
|
|
internal class RazorDocumentExcerptService : IDocumentExcerptService
|
|
{
|
|
private readonly DocumentSnapshot _document;
|
|
private readonly ISpanMappingService _mapper;
|
|
|
|
public RazorDocumentExcerptService(DocumentSnapshot document, ISpanMappingService mapper)
|
|
{
|
|
if (mapper == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(mapper));
|
|
}
|
|
|
|
_document = document;
|
|
_mapper = mapper;
|
|
}
|
|
|
|
public async Task<ExcerptResult?> TryExcerptAsync(
|
|
Document document,
|
|
TextSpan span,
|
|
ExcerptMode mode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (_document == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var mapped = await _mapper.MapSpansAsync(document, new[] { span }, cancellationToken).ConfigureAwait(false);
|
|
if (mapped.Length == 0 || mapped[0].Equals(default(MappedSpanResult)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var project = _document.Project;
|
|
var primaryDocument = project.GetDocument(mapped[0].FilePath);
|
|
if (primaryDocument == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var primaryText = await primaryDocument.GetTextAsync().ConfigureAwait(false);
|
|
var primarySpan = primaryText.Lines.GetTextSpan(mapped[0].LinePositionSpan);
|
|
|
|
var secondaryDocument = document;
|
|
var secondarySpan = span;
|
|
|
|
// First compute the range of text we want to we to display relative to the primary document.
|
|
var excerptSpan = ChooseExcerptSpan(primaryText, primarySpan, mode);
|
|
|
|
// Then we'll classify the spans based on the primary document, since that's the coordinate
|
|
// space that our output mappings use.
|
|
var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false);
|
|
var mappings = output.GetCSharpDocument().SourceMappings;
|
|
var classifiedSpans = await ClassifyPreviewAsync(
|
|
primaryText,
|
|
excerptSpan,
|
|
secondaryDocument,
|
|
mappings,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
// Now translate everything to be relative to the excerpt
|
|
var offset = 0 - excerptSpan.Start;
|
|
var excerptText = primaryText.GetSubText(excerptSpan);
|
|
excerptSpan = new TextSpan(excerptSpan.Start + offset, excerptSpan.Length);
|
|
|
|
for (var i = 0; i < classifiedSpans.Count; i++)
|
|
{
|
|
var classifiedSpan = classifiedSpans[i];
|
|
var updated = new TextSpan(classifiedSpan.TextSpan.Start + offset, classifiedSpan.TextSpan.Length);
|
|
Debug.Assert(excerptSpan.Contains(updated));
|
|
|
|
classifiedSpans[i] = new ClassifiedSpan(classifiedSpan.ClassificationType, updated);
|
|
}
|
|
|
|
return new ExcerptResult(excerptText, excerptSpan, classifiedSpans.ToImmutable(), document, span);
|
|
}
|
|
|
|
private TextSpan ChooseExcerptSpan(SourceText primaryText, TextSpan primarySpan, ExcerptMode mode)
|
|
{
|
|
var startLine = primaryText.Lines.GetLineFromPosition(primarySpan.Start);
|
|
var endLine = primaryText.Lines.GetLineFromPosition(primarySpan.End);
|
|
|
|
// If we're showing a single line then this will do. Otherwise expand the range by 1 in
|
|
// each direction (if possible).
|
|
if (mode == ExcerptMode.Tooltip && startLine.LineNumber > 0)
|
|
{
|
|
startLine = primaryText.Lines[startLine.LineNumber - 1];
|
|
}
|
|
|
|
if (mode == ExcerptMode.Tooltip && endLine.LineNumber < primaryText.Lines.Count - 1)
|
|
{
|
|
endLine = primaryText.Lines[endLine.LineNumber + 1];
|
|
}
|
|
|
|
return new TextSpan(startLine.Start, endLine.End - startLine.Start);
|
|
}
|
|
|
|
private async Task<ImmutableArray<ClassifiedSpan>.Builder> ClassifyPreviewAsync(
|
|
SourceText primaryText,
|
|
TextSpan excerptSpan,
|
|
Document secondaryDocument,
|
|
IReadOnlyList<SourceMapping> mappings,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var builder = ImmutableArray.CreateBuilder<ClassifiedSpan>();
|
|
|
|
var sorted = new List<SourceMapping>(mappings);
|
|
sorted.Sort((x, y) => x.OriginalSpan.AbsoluteIndex.CompareTo(y.OriginalSpan.AbsoluteIndex));
|
|
|
|
// The algorithm here is to iterate through the source mappings (sorted) and use the C# classifier
|
|
// on the spans that are known to the C#. For the spans that are not known to be C# then
|
|
// we just treat them as text since we'd don't currently have our own classifications.
|
|
|
|
var remainingSpan = excerptSpan;
|
|
for (var i = 0; i < sorted.Count && excerptSpan.Length > 0; i++)
|
|
{
|
|
var primarySpan = sorted[i].OriginalSpan.AsTextSpan();
|
|
var intersection = primarySpan.Intersection(remainingSpan);
|
|
if (intersection == null)
|
|
{
|
|
// This span is outside the area we're interested in.
|
|
continue;
|
|
}
|
|
|
|
// OK this span intersects with the excerpt span, so we will process it. Let's compute
|
|
// the secondary span that matches the intersection.
|
|
var secondarySpan = sorted[i].GeneratedSpan.AsTextSpan();
|
|
secondarySpan = new TextSpan(secondarySpan.Start + intersection.Value.Start - primarySpan.Start, intersection.Value.Length);
|
|
primarySpan = intersection.Value;
|
|
|
|
if (remainingSpan.Start < primarySpan.Start)
|
|
{
|
|
// The position is before the next C# span. Classify everything up to the C# start
|
|
// as text.
|
|
builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, new TextSpan(remainingSpan.Start, primarySpan.Start - remainingSpan.Start)));
|
|
|
|
// Advance to the start of the C# span.
|
|
remainingSpan = new TextSpan(primarySpan.Start, remainingSpan.Length - (primarySpan.Start - remainingSpan.Start));
|
|
}
|
|
|
|
// We should be able to process this whole span as C#, so classify it.
|
|
//
|
|
// However, we'll have to translate it to the the secondary document's coordinates to do that.
|
|
Debug.Assert(remainingSpan.Contains(primarySpan) && remainingSpan.Start == primarySpan.Start);
|
|
var classifiedSecondarySpans = await Classifier.GetClassifiedSpansAsync(
|
|
secondaryDocument,
|
|
secondarySpan,
|
|
cancellationToken);
|
|
|
|
// Now we have to translate back to the primary document's coordinates.
|
|
var offset = primarySpan.Start - secondarySpan.Start;
|
|
foreach (var classifiedSecondarySpan in classifiedSecondarySpans)
|
|
{
|
|
Debug.Assert(secondarySpan.Contains(classifiedSecondarySpan.TextSpan));
|
|
|
|
var updated = new TextSpan(classifiedSecondarySpan.TextSpan.Start + offset, classifiedSecondarySpan.TextSpan.Length);
|
|
Debug.Assert(primarySpan.Contains(updated));
|
|
|
|
builder.Add(new ClassifiedSpan(classifiedSecondarySpan.ClassificationType, updated));
|
|
}
|
|
|
|
remainingSpan = new TextSpan(primarySpan.End, remainingSpan.Length - primarySpan.Length);
|
|
}
|
|
|
|
// Deal with residue
|
|
if (remainingSpan.Length > 0)
|
|
{
|
|
// Trailing Razor/markup text.
|
|
builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, remainingSpan));
|
|
}
|
|
|
|
return builder;
|
|
}
|
|
}
|
|
} |