File: DocumentExcerpt\DocumentExcerptHelper.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
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.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.DocumentExcerpt;
 
internal static class DocumentExcerptHelper
{
    public static async Task<ImmutableArray<ClassifiedSpan>.Builder> ClassifyPreviewAsync(
        TextSpan excerptSpan,
        Document generatedDocument,
        ImmutableArray<SourceMapping> mappingsSortedByOriginal,
        RazorClassificationOptionsWrapper options,
        CancellationToken cancellationToken)
    {
        var builder = ImmutableArray.CreateBuilder<ClassifiedSpan>();
 
        // The algorithm here is to iterate through the source mappings (sorted) and use the C# classifier
        // on the spans that are known to be 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.
 
        if (excerptSpan.Length == 0)
        {
            return builder;
        }
 
        var remainingSpan = excerptSpan;
        foreach (var span in mappingsSortedByOriginal)
        {
            var primarySpan = span.OriginalSpan.AsTextSpan();
            if (primarySpan.Intersection(remainingSpan) is not TextSpan intersection)
            {
                if (primarySpan.Start > remainingSpan.End)
                {
                    // This span (and all following) are after the area we're interested in
                    break;
                }
 
                // This span precedes 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 = span.GeneratedSpan.AsTextSpan();
            secondarySpan = new TextSpan(secondarySpan.Start + intersection.Start - primarySpan.Start, intersection.Length);
            primarySpan = intersection;
 
            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 generated document's coordinates to do that.
            Debug.Assert(remainingSpan.Contains(primarySpan) && remainingSpan.Start == primarySpan.Start);
            var classifiedSecondarySpans = await RazorClassifierAccessor.GetClassifiedSpansAsync(
                generatedDocument,
                secondarySpan,
                options,
                cancellationToken).ConfigureAwait(false);
 
            // NOTE: The Classifier will only returns spans for things that it understands. That means
            // that whitespace is not classified. The preview expects us to provide contiguous spans,
            // so we are going to have to fill in the gaps.
 
            // Now we have to translate back to the primary document's coordinates.
            var offset = primarySpan.Start - secondarySpan.Start;
            foreach (var classifiedSecondarySpan in classifiedSecondarySpans)
            {
                // It's possible for the classified span to extend past our secondary span, so we cap it
                var classifiedSpan = classifiedSecondarySpan.TextSpan.End > secondarySpan.End
                    ? TextSpan.FromBounds(classifiedSecondarySpan.TextSpan.Start, secondarySpan.End)
                    : classifiedSecondarySpan.TextSpan;
                Debug.Assert(secondarySpan.Contains(classifiedSpan));
 
                var updated = new TextSpan(classifiedSpan.Start + offset, classifiedSpan.Length);
                Debug.Assert(primarySpan.Contains(updated));
 
                // Make sure that we're not introducing a gap. Remember, we need to fill in the whitespace.
                if (remainingSpan.Start < updated.Start)
                {
                    builder.Add(new ClassifiedSpan(
                        ClassificationTypeNames.Text,
                        new TextSpan(remainingSpan.Start, updated.Start - remainingSpan.Start)));
                    remainingSpan = new TextSpan(updated.Start, remainingSpan.Length - (updated.Start - remainingSpan.Start));
                }
 
                builder.Add(new ClassifiedSpan(classifiedSecondarySpan.ClassificationType, updated));
                remainingSpan = new TextSpan(updated.End, remainingSpan.Length - (updated.End - remainingSpan.Start));
            }
 
            // Make sure that we're not introducing a gap. Remember, we need to fill in the whitespace.
            if (remainingSpan.Start < primarySpan.End)
            {
                builder.Add(new ClassifiedSpan(
                    ClassificationTypeNames.Text,
                    new TextSpan(remainingSpan.Start, primarySpan.End - remainingSpan.Start)));
                remainingSpan = new TextSpan(primarySpan.End, remainingSpan.Length - (primarySpan.End - remainingSpan.Start));
            }
        }
 
        // Deal with residue
        if (remainingSpan.Length > 0)
        {
            // Trailing Razor/markup text.
            builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, remainingSpan));
        }
 
        return builder;
    }
 
    public static TextSpan ChooseExcerptSpan(SourceText text, TextSpan span, RazorExcerptMode mode)
    {
        var startLine = text.Lines.GetLineFromPosition(span.Start);
        var endLine = text.Lines.GetLineFromPosition(span.End);
 
        if (mode == RazorExcerptMode.Tooltip)
        {
            // Expand the range by 3 in each direction (if possible).
            var startIndex = Math.Max(startLine.LineNumber - 3, 0);
            startLine = text.Lines[startIndex];
 
            var endIndex = Math.Min(endLine.LineNumber + 3, text.Lines.Count - 1);
            endLine = text.Lines[endIndex];
            return CreateTextSpan(startLine, endLine);
        }
        else
        {
            // Trim leading whitespace in a single line excerpt
            var excerptSpan = CreateTextSpan(startLine, endLine);
            var trimmedExcerptSpan = excerptSpan.TrimLeadingWhitespace(text);
            return trimmedExcerptSpan;
        }
 
        static TextSpan CreateTextSpan(TextLine startLine, TextLine endLine)
        {
            return new TextSpan(startLine.Start, endLine.End - startLine.Start);
        }
    }
 
    public static SourceText GetTranslatedExcerptText(
        SourceText razorDocumentText,
        ref TextSpan razorDocumentSpan,
        ref TextSpan excerptSpan,
        ImmutableArray<ClassifiedSpan>.Builder classifiedSpans)
    {
        // Now translate everything to be relative to the excerpt
        var offset = 0 - excerptSpan.Start;
        var excerptText = razorDocumentText.GetSubText(excerptSpan);
        excerptSpan = new TextSpan(0, excerptSpan.Length);
        razorDocumentSpan = new TextSpan(razorDocumentSpan.Start + offset, razorDocumentSpan.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 excerptText;
    }
}