File: EmbeddedLanguages\Classification\AbstractEmbeddedLanguageClassificationService.cs
Web Access
Project: ..\..\..\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.
 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EmbeddedLanguages;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Classification
{
    internal abstract class AbstractEmbeddedLanguageClassificationService :
        AbstractEmbeddedLanguageFeatureService<IEmbeddedLanguageClassifier>, IEmbeddedLanguageClassificationService
    {
        /// <summary>
        /// Finally classifier to run if there is no embedded language in a string.  It will just classify escape sequences.
        /// </summary>
        private readonly IEmbeddedLanguageClassifier _fallbackClassifier;
 
        protected AbstractEmbeddedLanguageClassificationService(
            string languageName,
            EmbeddedLanguageInfo info,
            ISyntaxKinds syntaxKinds,
            IEmbeddedLanguageClassifier fallbackClassifier,
            IEnumerable<Lazy<IEmbeddedLanguageClassifier, EmbeddedLanguageMetadata>> allClassifiers)
            : base(languageName, info, syntaxKinds, allClassifiers)
        {
            _fallbackClassifier = fallbackClassifier;
        }
 
        public async Task AddEmbeddedLanguageClassificationsAsync(
            Document document, TextSpan textSpan, ClassificationOptions options, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            AddEmbeddedLanguageClassifications(document.Project, semanticModel, textSpan, options, result, cancellationToken);
        }
 
        public void AddEmbeddedLanguageClassifications(
            Project? project, SemanticModel semanticModel, TextSpan textSpan, ClassificationOptions options, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            if (project is null)
                return;
 
            var root = semanticModel.SyntaxTree.GetRoot(cancellationToken);
            var worker = new Worker(this, project, semanticModel, textSpan, options, result, cancellationToken);
            worker.VisitTokens(root);
        }
 
        private readonly ref struct Worker
        {
            private readonly AbstractEmbeddedLanguageClassificationService _owner;
            private readonly Project _project;
            private readonly SemanticModel _semanticModel;
            private readonly TextSpan _textSpan;
            private readonly ClassificationOptions _options;
            private readonly ArrayBuilder<ClassifiedSpan> _result;
            private readonly CancellationToken _cancellationToken;
 
            public Worker(
                AbstractEmbeddedLanguageClassificationService service,
                Project project,
                SemanticModel semanticModel,
                TextSpan textSpan,
                ClassificationOptions options,
                ArrayBuilder<ClassifiedSpan> result,
                CancellationToken cancellationToken)
            {
                _owner = service;
                _project = project;
                _semanticModel = semanticModel;
                _textSpan = textSpan;
                _options = options;
                _result = result;
                _cancellationToken = cancellationToken;
            }
 
            public void VisitTokens(SyntaxNode node)
            {
                using var pooledStack = SharedPools.Default<Stack<SyntaxNodeOrToken>>().GetPooledObject();
                var stack = pooledStack.Object;
                stack.Push(node);
                while (!stack.IsEmpty())
                {
                    _cancellationToken.ThrowIfCancellationRequested();
                    var currentNodeOrToken = stack.Pop();
                    if (currentNodeOrToken.Span.IntersectsWith(_textSpan))
                    {
                        if (currentNodeOrToken.IsNode)
                        {
                            foreach (var child in currentNodeOrToken.ChildNodesAndTokens().Reverse())
                            {
                                stack.Push(child);
                            }
                        }
                        else
                        {
                            ProcessToken(currentNodeOrToken.AsToken());
                        }
                    }
                }
            }
 
            private void ProcessToken(SyntaxToken token)
            {
                _cancellationToken.ThrowIfCancellationRequested();
                ProcessTriviaList(token.LeadingTrivia);
                ClassifyToken(token);
                ProcessTriviaList(token.TrailingTrivia);
            }
 
            private void ClassifyToken(SyntaxToken token)
            {
                if (token.Span.IntersectsWith(_textSpan) && _owner.SyntaxTokenKinds.Contains(token.RawKind))
                {
                    var context = new EmbeddedLanguageClassificationContext(
                        _project, _semanticModel, token, _options, _owner.Info.VirtualCharService, _result, _cancellationToken);
 
                    var classifiers = _owner.GetServices(_semanticModel, token, _cancellationToken);
                    foreach (var classifier in classifiers)
                    {
                        // If this classifier added values then need to check the other ones.
                        if (TryClassify(classifier.Value, context))
                            return;
                    }
 
                    // If not other classifier classified this, then give the fallback classifier a chance to classify basic language escapes.
                    TryClassify(_owner._fallbackClassifier, context);
                }
            }
 
            private bool TryClassify(IEmbeddedLanguageClassifier classifier, EmbeddedLanguageClassificationContext context)
            {
                var count = _result.Count;
                classifier.RegisterClassifications(context);
                return _result.Count != count;
            }
 
            private void ProcessTriviaList(SyntaxTriviaList triviaList)
            {
                foreach (var trivia in triviaList)
                    ProcessTrivia(trivia);
            }
 
            private void ProcessTrivia(SyntaxTrivia trivia)
            {
                if (trivia.HasStructure && trivia.FullSpan.IntersectsWith(_textSpan))
                    VisitTokens(trivia.GetStructure()!);
            }
        }
    }
}