File: Classification\AbstractClassificationService.cs
Web Access
Project: ..\..\..\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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.ReassignedVariable;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Storage;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Classification
{
    internal abstract class AbstractClassificationService : IClassificationService
    {
        public abstract void AddLexicalClassifications(SourceText text, TextSpan textSpan, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken);
        public abstract ClassifiedSpan AdjustStaleClassification(SourceText text, ClassifiedSpan classifiedSpan);
 
        public Task AddSemanticClassificationsAsync(
            Document document, TextSpan textSpan, ClassificationOptions options, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            return AddClassificationsAsync(document, textSpan, options, ClassificationType.Semantic, result, cancellationToken);
        }
 
        public Task AddEmbeddedLanguageClassificationsAsync(
            Document document, TextSpan textSpan, ClassificationOptions options, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            return AddClassificationsAsync(document, textSpan, options, ClassificationType.EmbeddedLanguage, result, cancellationToken);
        }
 
        private static async Task AddClassificationsAsync(
            Document document,
            TextSpan textSpan,
            ClassificationOptions options,
            ClassificationType type,
            ArrayBuilder<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 client = await RemoteHostClient.TryGetClientAsync(document.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 isFullyLoaded = IsFullyLoaded(document, cancellationToken);
                if (await TryGetCachedClassificationsAsync(document, textSpan, 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>(
                   document.Project,
                   (service, solutionInfo, cancellationToken) => service.GetClassificationsAsync(
                       solutionInfo, document.Id, textSpan, 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, textSpan, type, options, result, cancellationToken).ConfigureAwait(false);
            }
        }
 
        private static bool IsFullyLoaded(Document document, CancellationToken cancellationToken)
        {
            var workspaceStatusService = document.Project.Solution.Services.GetRequiredService<IWorkspaceStatusService>();
 
            // Importantly, we do not await/wait on the fullyLoadedStateTask.  We do not want to ever be waiting on work
            // that may end up touching the UI thread (As we can deadlock if GetTagsSynchronous waits on us).  Instead,
            // we only check if the Task is completed.  Prior to that we will assume we are still loading.  Once this
            // task is completed, we know that the WaitUntilFullyLoadedAsync call will have actually finished and we're
            // fully loaded.
            var isFullyLoadedTask = workspaceStatusService.IsFullyLoadedAsync(cancellationToken);
            var isFullyLoaded = isFullyLoadedTask.IsCompleted && isFullyLoadedTask.GetAwaiter().GetResult();
            return isFullyLoaded;
        }
 
        private static async Task<bool> TryGetCachedClassificationsAsync(
            Document document,
            TextSpan textSpan,
            ClassificationType type,
            RemoteHostClient client,
            bool isFullyLoaded,
            ArrayBuilder<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, textSpan, 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;
        }
 
        public static async Task AddClassificationsInCurrentProcessAsync(
            Document document,
            TextSpan textSpan,
            ClassificationType type,
            ClassificationOptions options,
            ArrayBuilder<ClassifiedSpan> result,
            CancellationToken cancellationToken)
        {
            if (type == ClassificationType.Semantic)
            {
                var classificationService = document.GetRequiredLanguageService<ISyntaxClassificationService>();
                var reassignedVariableService = document.GetRequiredLanguageService<IReassignedVariableService>();
 
                var extensionManager = document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
                var classifiers = classificationService.GetDefaultSyntaxClassifiers();
 
                var getNodeClassifiers = extensionManager.CreateNodeExtensionGetter(classifiers, c => c.SyntaxNodeTypes);
                var getTokenClassifiers = extensionManager.CreateTokenExtensionGetter(classifiers, c => c.SyntaxTokenKinds);
 
                await classificationService.AddSemanticClassificationsAsync(
                    document, textSpan, options, getNodeClassifiers, getTokenClassifiers, result, cancellationToken).ConfigureAwait(false);
 
                if (options.ClassifyReassignedVariables)
                {
                    var reassignedVariableSpans = await reassignedVariableService.GetLocationsAsync(document, textSpan, cancellationToken).ConfigureAwait(false);
                    foreach (var span in reassignedVariableSpans)
                        result.Add(new ClassifiedSpan(span, ClassificationTypeNames.ReassignedVariable));
                }
            }
            else if (type == ClassificationType.EmbeddedLanguage)
            {
                var embeddedLanguageService = document.GetLanguageService<IEmbeddedLanguageClassificationService>();
                if (embeddedLanguageService != null)
                {
                    await embeddedLanguageService.AddEmbeddedLanguageClassificationsAsync(
                        document, textSpan, options, result, cancellationToken).ConfigureAwait(false);
                }
            }
            else
            {
                throw ExceptionUtilities.UnexpectedValue(type);
            }
        }
 
        public async Task AddSyntacticClassificationsAsync(Document document, TextSpan textSpan, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            AddSyntacticClassifications(document.Project.Solution.Services, root, textSpan, result, cancellationToken);
        }
 
        public void AddSyntacticClassifications(
            SolutionServices services, SyntaxNode? root, TextSpan textSpan, ArrayBuilder<ClassifiedSpan> result, CancellationToken cancellationToken)
        {
            if (root == null)
                return;
 
            var classificationService = services.GetLanguageServices(root.Language).GetService<ISyntaxClassificationService>();
            if (classificationService == null)
                return;
 
            classificationService.AddSyntacticClassifications(root, textSpan, 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);
        }
    }
}