File: ClassifiedSpansAndHighlightSpanFactory.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Classification;
 
internal static class ClassifiedSpansAndHighlightSpanFactory
{
    public static async Task<ClassifiedSpansAndHighlightSpan> ClassifyAsync(
        DocumentSpan documentSpan, ClassifiedSpansAndHighlightSpan? classifiedSpans, ClassificationOptions options, CancellationToken cancellationToken)
    {
        // If the document span is providing us with the classified spans up front, then we
        // can just use that.  Otherwise, go back and actually classify the text for the line
        // the document span is on.
        if (classifiedSpans != null)
            return classifiedSpans.Value;
 
        return await ClassifyAsync(
            documentSpan.Document, documentSpan.SourceSpan, options, cancellationToken).ConfigureAwait(false);
    }
 
    private static async Task<ClassifiedSpansAndHighlightSpan> ClassifyAsync(
        Document document, TextSpan sourceSpan, ClassificationOptions options, CancellationToken cancellationToken)
    {
        var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        var narrowSpan = sourceSpan;
        var lineSpan = GetLineSpanForReference(sourceText, narrowSpan);
 
        var taggedLineParts = await GetTaggedTextForDocumentRegionAsync(
            document, narrowSpan, lineSpan, options, cancellationToken).ConfigureAwait(false);
        return taggedLineParts;
    }
 
    private static TextSpan GetLineSpanForReference(SourceText sourceText, TextSpan referenceSpan)
    {
        var sourceLine = sourceText.Lines.GetLineFromPosition(referenceSpan.Start);
        var firstNonWhitespacePosition = sourceLine.GetFirstNonWhitespacePosition().Value;
 
        // Get the span of the line from the first non-whitespace character to the end of it. Note: the reference
        // span might actually start in the leading whitespace of the line (nothing prevents any of our
        // languages/providers from doing that), so ensure that the line snap we clip out at least starts at that
        // position so that our span math will be correct.
        return TextSpan.FromBounds(Math.Min(firstNonWhitespacePosition, referenceSpan.Start), sourceLine.End);
    }
 
    private static async Task<ClassifiedSpansAndHighlightSpan> GetTaggedTextForDocumentRegionAsync(
        Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken)
    {
        var highlightSpan = new TextSpan(
            start: narrowSpan.Start - widenedSpan.Start,
            length: narrowSpan.Length);
 
        var classifiedSpans = await GetClassifiedSpansAsync(
            document, narrowSpan, widenedSpan, options, cancellationToken).ConfigureAwait(false);
        return new ClassifiedSpansAndHighlightSpan(classifiedSpans, highlightSpan);
    }
 
    private static async Task<ImmutableArray<ClassifiedSpan>> GetClassifiedSpansAsync(
        Document document, TextSpan narrowSpan, TextSpan widenedSpan, ClassificationOptions options, CancellationToken cancellationToken)
    {
        // We don't present things like static/assigned variables differently.  So pass `includeAdditiveSpans:
        // false` as we don't need that data.
        var result = await ClassifierHelper.GetClassifiedSpansAsync(
            document, widenedSpan, options, includeAdditiveSpans: false, cancellationToken).ConfigureAwait(false);
        if (!result.IsDefault)
            return result;
 
        // For languages that don't expose a classification service, we show the entire
        // item as plain text. Break the text into three spans so that we can properly
        // highlight the 'narrow-span' later on when we display the item.
        return
        [
            new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(widenedSpan.Start, narrowSpan.Start)),
            new ClassifiedSpan(ClassificationTypeNames.Text, narrowSpan),
            new ClassifiedSpan(ClassificationTypeNames.Text, TextSpan.FromBounds(narrowSpan.End, widenedSpan.End)),
        ];
    }
}