File: EmbeddedLanguages\CSharpTestEmbeddedLanguageClassifier.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Composition;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages;
using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.Features.EmbeddedLanguages;
 
using static VirtualCharUtilities;
 
[ExportEmbeddedLanguageClassifier(
    PredefinedEmbeddedLanguageNames.CSharpTest, [LanguageNames.CSharp], supportsUnannotatedAPIs: false,
    PredefinedEmbeddedLanguageNames.CSharpTest), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpTestEmbeddedLanguageClassifier() : IEmbeddedLanguageClassifier
{
    public void RegisterClassifications(EmbeddedLanguageClassificationContext context)
    {
        var cancellationToken = context.CancellationToken;
 
        var token = context.SyntaxToken;
        var semanticModel = context.SemanticModel;
 
        if (token.Kind() is not (SyntaxKind.StringLiteralToken or SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken))
            return;
 
        var virtualCharsWithMarkup = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token);
        if (virtualCharsWithMarkup.IsDefaultOrEmpty())
            return;
 
        // Note: if we get here, then we know that the token is well formed (TryConvertToVirtualChars will fail if
        // the token has diagnostics).
 
        cancellationToken.ThrowIfCancellationRequested();
        using var _ = ArrayBuilder<VirtualChar>.GetInstance(virtualCharsWithMarkup.Length, out var virtualCharsBuilder);
        foreach (var vc in virtualCharsWithMarkup)
            virtualCharsBuilder.Add(vc);
 
        // Break the full sequence of virtual chars into the actual C# code and the markup
        var (virtualCharsWithoutMarkup, markdownSpans) = StripMarkupCharacters(virtualCharsBuilder, cancellationToken);
 
        // First, add all the markdown components (`$$`, `[|`, etc.) into the result.
        foreach (var span in markdownSpans)
            context.AddClassification(ClassificationTypeNames.TestCodeMarkdown, span);
 
        // Next, fill in everything with the "TestCode" classification.  This will ensure it gets the right
        // background highlighting.  For raw strings we don't want to classify the full lines with this background
        // color.  Only the parts to the right of the "start column" designation for that raw string.
 
        if (token.Kind() is SyntaxKind.MultiLineRawStringLiteralToken)
        {
            var text = semanticModel.SyntaxTree.GetText(cancellationToken);
            var lines = text.Lines;
            var firstLine = lines.GetLineFromPosition(token.Span.Start);
            var lastLine = lines.GetLineFromPosition(token.Span.End);
            var whitespaceCount = 0;
            for (var i = lastLine.Start; i < lastLine.End && SyntaxFacts.IsWhitespace(text[i]); i++)
                whitespaceCount++;
 
            for (var i = firstLine.LineNumber + 1; i < lastLine.LineNumber; i++)
            {
                var currentLine = lines[i];
                if (currentLine.Start + whitespaceCount < currentLine.End)
                {
                    context.AddClassification(
                        ClassificationTypeNames.TestCode,
                        TextSpan.FromBounds(
                            currentLine.Start + whitespaceCount,
                            currentLine.End));
                }
            }
        }
        else if (virtualCharsWithoutMarkup.Count > 0)
        {
            context.AddClassification(
                ClassificationTypeNames.TestCode,
                TextSpan.FromBounds(
                    virtualCharsWithoutMarkup[0].Span.Start,
                    virtualCharsWithoutMarkup[^1].Span.End));
        }
 
        // Next, get all the embedded language classifications for the test file.  Combine these with the markdown
        // components. Note: markdown components may be in between individual language components.  For example
        // `ret$$urn`.  This will break the `return` classification into two individual classifications around the
        // `$$` classification.
        var testFileClassifiedSpans = CSharpTestEmbeddedLanguageUtilities.GetTestFileClassifiedSpans(
            context.SolutionServices, semanticModel, virtualCharsWithoutMarkup, cancellationToken);
 
        CSharpTestEmbeddedLanguageUtilities.AddClassifications(
            virtualCharsWithoutMarkup, testFileClassifiedSpans,
            static (context, classificationType, span) => context.AddClassification(classificationType, span),
            context);
    }
}