File: Classification\Worker.cs
Web Access
Project: src\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.Classification;
 
/// <summary>
/// Worker is an utility class that can classify a list of tokens or a tree within a
/// requested span The implementation is generic and can produce any kind of classification
/// artifacts T T is normally either ClassificationSpan or a Tuple (for testing purposes) 
/// and constructed via provided factory.
/// </summary>
internal readonly ref partial struct Worker
{
    private readonly TextSpan _textSpan;
    private readonly SegmentedList<ClassifiedSpan> _result;
    private readonly CancellationToken _cancellationToken;
 
    private Worker(TextSpan textSpan, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        _result = result;
        _textSpan = textSpan;
        _cancellationToken = cancellationToken;
    }
 
    internal static void CollectClassifiedSpans(
        IEnumerable<SyntaxToken> tokens, TextSpan textSpan, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        var worker = new Worker(textSpan, result, cancellationToken);
        foreach (var tk in tokens)
            worker.ClassifyToken(tk);
    }
 
    internal static void CollectClassifiedSpans(
        SyntaxNode node, TextSpan textSpan, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        var worker = new Worker(textSpan, result, cancellationToken);
        worker.ClassifyNode(node);
    }
 
    private void AddClassification(TextSpan span, string type)
    {
        if (ShouldAddSpan(span))
        {
            _result.Add(new ClassifiedSpan(type, span));
        }
    }
 
    private bool ShouldAddSpan(TextSpan span)
        => span.Length > 0 && _textSpan.OverlapsWith(span);
 
    private void AddClassification(SyntaxTrivia trivia, string type)
        => AddClassification(trivia.Span, type);
 
    private void AddClassification(SyntaxToken token, string type)
        => AddClassification(token.Span, type);
 
    private void ClassifyNodeOrToken(SyntaxNodeOrToken nodeOrToken)
    {
        Debug.Assert(nodeOrToken.IsNode || nodeOrToken.IsToken);
 
        if (nodeOrToken.IsToken)
        {
            ClassifyToken(nodeOrToken.AsToken());
            return;
        }
 
        ClassifyNode(nodeOrToken.AsNode()!);
    }
 
    private void ClassifyNode(SyntaxNode node)
    {
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var stack);
        stack.Push(node);
 
        var textSpanStart = _textSpan.Start;
        var textSpanEnd = _textSpan.End;
 
        while (stack.TryPop(out var current))
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            // It's ok that we're not pushing in reverse.  The caller (TotalClassificationTaggerProvider) will be
            // sorting the results before doing anything with them.
            foreach (var child in current.ChildNodesAndTokens())
            {
                if (child.AsNode(out var childNode))
                {
                    var childSpan = childNode.FullSpan;
 
                    // If we haven't reached the start of the span we care about, then we can skip this node, going to
                    // the next.  Once we go past that span, we can stop immediately.  Otherwise, we must be
                    // intersecting the span, and we should recurse into this child.
                    if (childSpan.End < textSpanStart)
                        continue;
                    else if (childSpan.Start > textSpanEnd)
                        break;
                    else
                        stack.Push(childNode);
                }
                else
                {
                    ClassifyToken(child.AsToken());
                }
            }
        }
    }
 
    private void ClassifyToken(SyntaxToken token)
    {
        var span = token.Span;
        if (ShouldAddSpan(span))
        {
            var type = ClassificationHelpers.GetClassification(token);
 
            if (type != null)
            {
                if (token.Kind() is
                        SyntaxKind.Utf8StringLiteralToken or
                        SyntaxKind.Utf8SingleLineRawStringLiteralToken or
                        SyntaxKind.Utf8MultiLineRawStringLiteralToken &&
                    token.Text.EndsWith("u8", StringComparison.OrdinalIgnoreCase))
                {
                    AddClassification(TextSpan.FromBounds(token.Span.Start, token.Span.End - "u8".Length), type);
                    AddClassification(TextSpan.FromBounds(token.Span.End - "u8".Length, token.Span.End), ClassificationTypeNames.Keyword);
                }
                else
                {
                    AddClassification(span, type);
                }
 
                // Additionally classify static symbols
                if (token.Kind() == SyntaxKind.IdentifierToken &&
                    ClassificationHelpers.IsStaticallyDeclared(token))
                {
                    AddClassification(span, ClassificationTypeNames.StaticSymbol);
                }
            }
        }
 
        ClassifyTriviaList(token.LeadingTrivia);
        ClassifyTriviaList(token.TrailingTrivia);
    }
 
    private void ClassifyTriviaList(SyntaxTriviaList list)
    {
        if (list.Count == 0)
        {
            return;
        }
 
        // We may have long lists of trivia (for example a huge list of // comments after someone
        // comments out a file).  Try to skip as many as possible if they're not actually in the span
        // we care about classifying.
        var classificationSpanStart = _textSpan.Start;
 
        // First, skip all the trivia before the span we care about.
        var enumerator = list.GetEnumerator();
        while (true)
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            if (!enumerator.MoveNext())
            {
                // Reached the end of the trivia.  It was all before the span we want to classify.
                // Stop immediately.
                return;
            }
 
            if (enumerator.Current.FullSpan.End > classificationSpanStart)
            {
                // Found trivia that is after the text span we're classifying.  
                break;
            }
        }
 
        // Continue processing trivia from this point on until we get past the span we want to classify.
        do
        {
            _cancellationToken.ThrowIfCancellationRequested();
 
            var trivia = enumerator.Current;
            if (trivia.SpanStart >= _textSpan.End)
            {
                // reached trivia that is past what we are classifying.  Stop immediately.
                return;
            }
 
            ClassifyTrivia(trivia, list);
        }
        while (enumerator.MoveNext());
    }
 
    private void ClassifyTrivia(SyntaxTrivia trivia, SyntaxTriviaList triviaList)
    {
        switch (trivia.Kind())
        {
            case SyntaxKind.SingleLineCommentTrivia:
            case SyntaxKind.MultiLineCommentTrivia:
            case SyntaxKind.ShebangDirectiveTrivia:
                AddClassification(trivia, ClassificationTypeNames.Comment);
                return;
 
            case SyntaxKind.DisabledTextTrivia:
                ClassifyDisabledText(trivia, triviaList);
                return;
 
            case SyntaxKind.SkippedTokensTrivia:
                ClassifySkippedTokens((SkippedTokensTriviaSyntax)trivia.GetStructure()!);
                return;
 
            case SyntaxKind.SingleLineDocumentationCommentTrivia:
            case SyntaxKind.MultiLineDocumentationCommentTrivia:
                ClassifyDocumentationComment((DocumentationCommentTriviaSyntax)trivia.GetStructure()!);
                return;
 
            case SyntaxKind.DocumentationCommentExteriorTrivia:
                AddClassification(trivia, ClassificationTypeNames.XmlDocCommentDelimiter);
                return;
 
            case SyntaxKind.ConflictMarkerTrivia:
                ClassifyConflictMarker(trivia);
                return;
 
            case SyntaxKind.IfDirectiveTrivia:
            case SyntaxKind.ElifDirectiveTrivia:
            case SyntaxKind.ElseDirectiveTrivia:
            case SyntaxKind.EndIfDirectiveTrivia:
            case SyntaxKind.RegionDirectiveTrivia:
            case SyntaxKind.EndRegionDirectiveTrivia:
            case SyntaxKind.DefineDirectiveTrivia:
            case SyntaxKind.UndefDirectiveTrivia:
            case SyntaxKind.ErrorDirectiveTrivia:
            case SyntaxKind.WarningDirectiveTrivia:
            case SyntaxKind.LineDirectiveTrivia:
            case SyntaxKind.LineSpanDirectiveTrivia:
            case SyntaxKind.PragmaWarningDirectiveTrivia:
            case SyntaxKind.PragmaChecksumDirectiveTrivia:
            case SyntaxKind.ReferenceDirectiveTrivia:
            case SyntaxKind.LoadDirectiveTrivia:
            case SyntaxKind.NullableDirectiveTrivia:
            case SyntaxKind.BadDirectiveTrivia:
                ClassifyPreprocessorDirective((DirectiveTriviaSyntax)trivia.GetStructure()!);
                return;
        }
    }
 
    private void ClassifySkippedTokens(SkippedTokensTriviaSyntax skippedTokens)
    {
        if (!_textSpan.OverlapsWith(skippedTokens.Span))
        {
            return;
        }
 
        var tokens = skippedTokens.Tokens;
        foreach (var tk in tokens)
        {
            ClassifyToken(tk);
        }
    }
 
    private void ClassifyConflictMarker(SyntaxTrivia trivia)
        => AddClassification(trivia, ClassificationTypeNames.Comment);
 
    private void ClassifyDisabledText(SyntaxTrivia trivia, SyntaxTriviaList triviaList)
    {
        var index = triviaList.IndexOf(trivia);
        if (index >= 2 &&
            triviaList[index - 1].Kind() == SyntaxKind.EndOfLineTrivia &&
            triviaList[index - 2].Kind() == SyntaxKind.ConflictMarkerTrivia)
        {
            // for the ======== add a comment for the first line, and then lex all
            // subsequent lines up until the end of the conflict marker.
            foreach (var token in SyntaxFactory.ParseTokens(text: trivia.ToFullString(), initialTokenPosition: trivia.SpanStart))
            {
                ClassifyToken(token);
            }
        }
        else
        {
            AddClassification(trivia, ClassificationTypeNames.ExcludedCode);
        }
    }
}