File: Classification\AbstractClassificationService.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification.Classifiers;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.ObsoleteSymbol;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ReassignedVariable;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Classification;
 
internal abstract class AbstractClassificationService(ISyntaxClassificationService syntaxClassificationService) : IClassificationService
{
    private readonly ISyntaxClassificationService _syntaxClassificationService = syntaxClassificationService;
 
    private Func<SyntaxNode, ImmutableArray<ISyntaxClassifier>>? _getNodeClassifiers;
    private Func<SyntaxToken, ImmutableArray<ISyntaxClassifier>>? _getTokenClassifiers;
 
    public abstract void AddLexicalClassifications(SourceText text, TextSpan textSpan, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken);
    public abstract ClassifiedSpan AdjustStaleClassification(SourceText text, ClassifiedSpan classifiedSpan);
 
    public Task AddSemanticClassificationsAsync(
        Document document, ImmutableArray<TextSpan> textSpans, ClassificationOptions options, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        return AddClassificationsAsync(document, textSpans, options, ClassificationType.Semantic, result, cancellationToken);
    }
 
    public Task AddEmbeddedLanguageClassificationsAsync(
        Document document, ImmutableArray<TextSpan> textSpans, ClassificationOptions options, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        return AddClassificationsAsync(document, textSpans, options, ClassificationType.EmbeddedLanguage, result, cancellationToken);
    }
 
    public async Task AddClassificationsAsync(
        Document document,
        ImmutableArray<TextSpan> textSpans,
        ClassificationOptions options,
        ClassificationType type,
        SegmentedList<ClassifiedSpan> result,
        CancellationToken cancellationToken)
    {
        var classificationService = document.GetLanguageService<ISyntaxClassificationService>();
        if (classificationService == null)
        {
            // When renaming a file's extension through VS when it's opened in editor, 
            // the content type might change and the content type changed event can be 
            // raised before the renaming propagate through VS workspace. As a result, 
            // the document we got (based on the buffer) could still be the one in the workspace
            // before rename happened. This would cause us problem if the document is supported 
            // by workspace but not a roslyn language (e.g. xaml, F#, etc.), since none of the roslyn 
            // language services would be available.
            //
            // If this is the case, we will simply bail out. It's OK to ignore the request
            // because when the buffer eventually get associated with the correct document in roslyn
            // workspace, we will be invoked again.
            //
            // For example, if you open a xaml from from a WPF project in designer view,
            // and then rename file extension from .xaml to .cs, then the document we received
            // here would still belong to the special "-xaml" project.
            return;
        }
 
        var project = document.Project;
        var client = await RemoteHostClient.TryGetClientAsync(project, cancellationToken).ConfigureAwait(false);
        if (client != null)
        {
            // We have an oop connection.  If we're not fully loaded, see if we can retrieve a previously cached set
            // of classifications from the server.  Note: this must be a separate call (instead of being part of
            // service.GetSemanticClassificationsAsync below) as we want to try to read in the cached
            // classifications without doing any syncing to the OOP process.
            var workspaceStatusService = document.Project.Solution.Services.GetRequiredService<IWorkspaceStatusService>();
            var isFullyLoaded = await workspaceStatusService.IsFullyLoadedAsync(cancellationToken).ConfigureAwait(false);
            if (await TryGetCachedClassificationsAsync(document, textSpans, type, client, isFullyLoaded, result, cancellationToken).ConfigureAwait(false))
                return;
 
            // Call the project overload.  Semantic classification only needs the current project's information
            // to classify properly.
            var classifiedSpans = await client.TryInvokeAsync<IRemoteSemanticClassificationService, SerializableClassifiedSpans>(
               project,
               (service, solutionInfo, cancellationToken) => service.GetClassificationsAsync(
                   solutionInfo, document.Id, textSpans, type, options, isFullyLoaded, cancellationToken),
               cancellationToken).ConfigureAwait(false);
 
            // if the remote call fails do nothing (error has already been reported)
            if (classifiedSpans.HasValue)
                classifiedSpans.Value.Rehydrate(result);
        }
        else
        {
            await AddClassificationsInCurrentProcessAsync(
                document, textSpans, type, options, result, cancellationToken).ConfigureAwait(false);
        }
    }
 
    private static async Task<bool> TryGetCachedClassificationsAsync(
        Document document,
        ImmutableArray<TextSpan> textSpans,
        ClassificationType type,
        RemoteHostClient client,
        bool isFullyLoaded,
        SegmentedList<ClassifiedSpan> result,
        CancellationToken cancellationToken)
    {
        // Only try to get cached classifications if we're not fully loaded yet.
        if (isFullyLoaded)
            return false;
 
        var (documentKey, checksum) = await SemanticClassificationCacheUtilities.GetDocumentKeyAndChecksumAsync(
            document, cancellationToken).ConfigureAwait(false);
 
        var cachedSpans = await client.TryInvokeAsync<IRemoteSemanticClassificationService, SerializableClassifiedSpans?>(
           document.Project,
           (service, solutionInfo, cancellationToken) => service.GetCachedClassificationsAsync(
               documentKey, textSpans, type, checksum, cancellationToken),
           cancellationToken).ConfigureAwait(false);
 
        // if the remote call fails do nothing (error has already been reported)
        if (!cachedSpans.HasValue || cachedSpans.Value == null)
            return false;
 
        cachedSpans.Value.Rehydrate(result);
        return true;
    }
 
    private async Task AddClassificationsInCurrentProcessAsync(
        Document document,
        ImmutableArray<TextSpan> textSpans,
        ClassificationType type,
        ClassificationOptions options,
        SegmentedList<ClassifiedSpan> result,
        CancellationToken cancellationToken)
    {
        if (type == ClassificationType.Semantic)
        {
            var classificationService = document.GetRequiredLanguageService<ISyntaxClassificationService>();
 
            var (getNodeClassifiers, getTokenClassifiers) = GetExtensionClassifiers(document, classificationService);
 
            await classificationService.AddSemanticClassificationsAsync(
                document, textSpans, options, getNodeClassifiers, getTokenClassifiers, result, cancellationToken).ConfigureAwait(false);
 
            if (options.ClassifyReassignedVariables)
            {
                var reassignedVariableService = document.GetRequiredLanguageService<IReassignedVariableService>();
                var reassignedVariableSpans = await reassignedVariableService.GetLocationsAsync(document, textSpans, cancellationToken).ConfigureAwait(false);
                foreach (var span in reassignedVariableSpans)
                    result.Add(new ClassifiedSpan(span, ClassificationTypeNames.ReassignedVariable));
            }
 
            if (options.ClassifyObsoleteSymbols)
            {
                var obsoleteSymbolService = document.GetRequiredLanguageService<IObsoleteSymbolService>();
                var obsoleteSymbolSpans = await obsoleteSymbolService.GetLocationsAsync(document, textSpans, cancellationToken).ConfigureAwait(false);
                foreach (var span in obsoleteSymbolSpans)
                    result.Add(new ClassifiedSpan(span, ClassificationTypeNames.ObsoleteSymbol));
            }
        }
        else if (type == ClassificationType.EmbeddedLanguage)
        {
            var embeddedLanguageService = document.GetLanguageService<IEmbeddedLanguageClassificationService>();
            if (embeddedLanguageService != null)
            {
                await embeddedLanguageService.AddEmbeddedLanguageClassificationsAsync(
                    document, textSpans, options, result, cancellationToken).ConfigureAwait(false);
            }
        }
        else
        {
            throw ExceptionUtilities.UnexpectedValue(type);
        }
 
        return;
 
        (Func<SyntaxNode, ImmutableArray<ISyntaxClassifier>>, Func<SyntaxToken, ImmutableArray<ISyntaxClassifier>>) GetExtensionClassifiers(
            Document document, ISyntaxClassificationService classificationService)
        {
            if (_getNodeClassifiers == null || _getTokenClassifiers == null)
            {
                var extensionManager = document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
                var classifiers = classificationService.GetDefaultSyntaxClassifiers();
 
                _getNodeClassifiers = extensionManager.CreateNodeExtensionGetter(classifiers, static c => c.SyntaxNodeTypes);
                _getTokenClassifiers = extensionManager.CreateTokenExtensionGetter(classifiers, static c => c.SyntaxTokenKinds);
            }
 
            return (_getNodeClassifiers, _getTokenClassifiers);
        }
    }
 
    public async Task AddSyntacticClassificationsAsync(Document document, ImmutableArray<TextSpan> textSpans, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        AddSyntacticClassifications(document.Project.Solution.Services, root, textSpans, result, cancellationToken);
    }
 
    public void AddSyntacticClassifications(
        SolutionServices services, SyntaxNode? root, ImmutableArray<TextSpan> textSpans, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
    {
        if (root is null)
            return;
 
        _syntaxClassificationService.AddSyntacticClassifications(root, textSpans, result, cancellationToken);
    }
 
    /// <summary>
    /// Helper to add all the values of <paramref name="temp"/> into <paramref name="result"/>
    /// without causing any allocations or boxing of enumerators.
    /// </summary>
    protected static void AddRange(ArrayBuilder<ClassifiedSpan> temp, List<ClassifiedSpan> result)
    {
        foreach (var span in temp)
        {
            result.Add(span);
        }
    }
 
    public ValueTask<TextChangeRange?> ComputeSyntacticChangeRangeAsync(Document oldDocument, Document newDocument, TimeSpan timeout, CancellationToken cancellationToken)
        => default;
 
    public TextChangeRange? ComputeSyntacticChangeRange(SolutionServices services, SyntaxNode oldRoot, SyntaxNode newRoot, TimeSpan timeout, CancellationToken cancellationToken)
    {
        var classificationService = services.GetLanguageServices(oldRoot.Language).GetService<ISyntaxClassificationService>();
        return classificationService?.ComputeSyntacticChangeRange(oldRoot, newRoot, timeout, cancellationToken);
    }
}