File: Classification\Syntactic\SyntacticClassificationTaggerProvider.TagComputer.cs
Web Access
Project: ..\..\..\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Classification
{
    internal partial class SyntacticClassificationTaggerProvider
    {
        /// <summary>
        /// A classifier that operates only on the syntax of the source and not the semantics.  Note:
        /// this class operates in a hybrid sync/async manner.  Specifically, while classification
        /// happens synchronously, it may be synchronous over a parse tree which is out of date.  Then,
        /// asynchronously, we will attempt to get an up to date parse tree for the file. When we do, we
        /// will determine which sections of the file changed and we will use that to notify the editor
        /// about what needs to be reclassified.
        /// </summary>
        internal partial class TagComputer
        {
            private readonly SyntacticClassificationTaggerProvider _taggerProvider;
            private readonly ITextBuffer2 _subjectBuffer;
            private readonly IAsynchronousOperationListener _listener;
            private readonly IClassificationTypeMap _typeMap;
 
            private readonly WorkspaceRegistration _workspaceRegistration;
 
            private readonly CancellationTokenSource _disposalCancellationSource = new();
 
            /// <summary>
            /// Work queue we use to batch up notifications about changes that will cause
            /// us to classify.  This ensures that if we hear a flurry of changes, we don't
            /// kick off an excessive amount of background work to process them.
            /// </summary>
            private readonly AsyncBatchingWorkQueue<ITextSnapshot> _workQueue;
 
            /// <summary>
            /// Timeout before we cancel the work to diff and return whatever we have.
            /// </summary>
            private readonly TimeSpan _diffTimeout;
 
            private Workspace? _workspace;
 
            // The latest data about the document being classified that we've cached.  objects can 
            // be accessed from both threads, and must be obtained when this lock is held. 
            //
            // _lastProcessedData.lastRoot is optional and is held onto for languages that support syntax.
            // it allows computing the root, and changed-spans, in the background so that we can
            // report a smaller change range, and have the data ready for classifying with GetTags
            // get called.
 
            private readonly object _gate = new();
            private (ITextSnapshot? lastSnapshot, Document? lastDocument, SyntaxNode? lastRoot) _lastProcessedData;
 
            // this will cache previous classification information for a span, so that we can avoid
            // digging into same tree again and again to find exactly same answer
            private readonly LastLineCache _lastLineCache;
 
            private int _taggerReferenceCount;
 
            public TagComputer(
                SyntacticClassificationTaggerProvider taggerProvider,
                ITextBuffer2 subjectBuffer,
                IAsynchronousOperationListener asyncListener,
                IClassificationTypeMap typeMap,
                TimeSpan diffTimeout)
            {
                _taggerProvider = taggerProvider;
                _subjectBuffer = subjectBuffer;
                _listener = asyncListener;
                _typeMap = typeMap;
                _diffTimeout = diffTimeout;
                _workQueue = new AsyncBatchingWorkQueue<ITextSnapshot>(
                    DelayTimeSpan.NearImmediate,
                    ProcessChangesAsync,
                    equalityComparer: null,
                    asyncListener,
                    _disposalCancellationSource.Token);
 
                _lastLineCache = new LastLineCache(taggerProvider._threadingContext);
 
                _workspaceRegistration = Workspace.GetWorkspaceRegistration(subjectBuffer.AsTextContainer());
                _workspaceRegistration.WorkspaceChanged += OnWorkspaceRegistrationChanged;
 
                if (_workspaceRegistration.Workspace != null)
                    ConnectToWorkspace(_workspaceRegistration.Workspace);
 
                _subjectBuffer.ChangedOnBackground += this.OnSubjectBufferChanged;
            }
 
            public event EventHandler<SnapshotSpanEventArgs>? TagsChanged;
 
            private IClassificationService? TryGetClassificationService(ITextSnapshot snapshot)
                => _workspace?.Services.SolutionServices.GetProjectServices(snapshot.ContentType)?.GetService<IClassificationService>();
 
            #region Workspace Hookup
 
            private void OnWorkspaceRegistrationChanged(object? sender, EventArgs e)
            {
                var token = _listener.BeginAsyncOperation(nameof(OnWorkspaceRegistrationChanged));
                var task = SwitchToMainThreadAndHookupWorkspaceAsync();
                task.CompletesAsyncOperation(token);
            }
 
            private async Task SwitchToMainThreadAndHookupWorkspaceAsync()
            {
                try
                {
                    await _taggerProvider._threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(_disposalCancellationSource.Token);
 
                    // We both try to connect synchronously, and register for workspace registration events.
                    // It's possible (particularly in tests), to connect in the startup path, but then get a
                    // previously scheduled, but not yet delivered event.  Don't bother connecting to the
                    // same workspace again in that case.
                    var newWorkspace = _workspaceRegistration.Workspace;
                    if (newWorkspace == _workspace)
                        return;
 
                    DisconnectFromWorkspace();
 
                    if (newWorkspace != null)
                        ConnectToWorkspace(newWorkspace);
                }
                catch (OperationCanceledException)
                {
                    // can happen if we were disposed of.
                }
                catch (Exception e) when (FatalError.ReportAndCatch(e))
                {
                    // We were in a fire and forget task.  So just report the NFW and do not let
                    // this bleed out to an unhandled exception.
                }
            }
 
            internal void IncrementReferenceCount()
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
                _taggerReferenceCount++;
            }
 
            internal void DecrementReferenceCount()
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
                _taggerReferenceCount--;
 
                if (_taggerReferenceCount == 0)
                {
                    // stop any bg work we're doing.
                    _disposalCancellationSource.Cancel();
 
                    _subjectBuffer.ChangedOnBackground -= this.OnSubjectBufferChanged;
 
                    DisconnectFromWorkspace();
                    _workspaceRegistration.WorkspaceChanged -= OnWorkspaceRegistrationChanged;
                    _taggerProvider.DisconnectTagComputer(_subjectBuffer);
                }
            }
 
            private void ConnectToWorkspace(Workspace workspace)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                _workspace = workspace;
                _workspace.WorkspaceChanged += this.OnWorkspaceChanged;
                _workspace.DocumentActiveContextChanged += this.OnDocumentActiveContextChanged;
 
                // Now that we've connected to the workspace, kick off work to reclassify this buffer.
                _workQueue.AddWork(_subjectBuffer.CurrentSnapshot);
            }
 
            public void DisconnectFromWorkspace()
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                lock (_gate)
                {
                    _lastProcessedData = default;
                }
 
                if (_workspace != null)
                {
                    _workspace.WorkspaceChanged -= this.OnWorkspaceChanged;
                    _workspace.DocumentActiveContextChanged -= this.OnDocumentActiveContextChanged;
 
                    _workspace = null;
 
                    // Now that we've disconnected to the workspace, kick off work to reclassify this buffer.
                    _workQueue.AddWork(_subjectBuffer.CurrentSnapshot);
                }
            }
 
            #endregion
 
            #region Event Handling
 
            private void OnSubjectBufferChanged(object? sender, TextContentChangedEventArgs args)
            {
                // we know a change to our buffer is always affecting this document.  So we can
                // just enqueue the work to reclassify things unilaterally.
                _workQueue.AddWork(args.After);
            }
 
            private void OnDocumentActiveContextChanged(object? sender, DocumentActiveContextChangedEventArgs args)
            {
                if (_workspace == null)
                    return;
 
                var documentId = args.NewActiveContextDocumentId;
                var bufferDocumentId = _workspace.GetDocumentIdInCurrentContext(_subjectBuffer.AsTextContainer());
                if (bufferDocumentId != documentId)
                    return;
 
                _workQueue.AddWork(_subjectBuffer.CurrentSnapshot);
            }
 
            private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs args)
            {
                // We may be getting an event for a workspace we already disconnected from.  If so,
                // ignore them.  We won't be able to find the Document corresponding to our text buffer,
                // so we can't reasonably classify this anyways.
                if (args.NewSolution.Workspace != _workspace)
                    return;
 
                if (args.Kind != WorkspaceChangeKind.ProjectChanged)
                    return;
 
                var documentId = _workspace.GetDocumentIdInCurrentContext(_subjectBuffer.AsTextContainer());
                if (args.ProjectId != documentId?.ProjectId)
                    return;
 
                var oldProject = args.OldSolution.GetProject(args.ProjectId);
                var newProject = args.NewSolution.GetProject(args.ProjectId);
 
                // In case of parse options change reclassify the doc as it may have affected things
                // like preprocessor directives.
                if (Equals(oldProject?.ParseOptions, newProject?.ParseOptions))
                    return;
 
                _workQueue.AddWork(_subjectBuffer.CurrentSnapshot);
            }
 
            #endregion
 
            /// <summary>
            /// Parses the document in the background and determines what has changed to report to
            /// the editor.  Calls to <see cref="ProcessChangesAsync"/> are serialized by <see cref="AsyncBatchingWorkQueue{TItem}"/>
            /// so we don't need to worry about multiple calls to this happening concurrently.
            /// </summary>
            private async ValueTask ProcessChangesAsync(ImmutableSegmentedList<ITextSnapshot> snapshots, CancellationToken cancellationToken)
            {
                // We have potentially heard about several changes to the subject buffer.  However
                // we only need to process the latest once.
                Contract.ThrowIfTrue(snapshots.IsDefault || snapshots.IsEmpty);
                var currentSnapshot = GetLatest(snapshots);
 
                var classificationService = TryGetClassificationService(currentSnapshot);
                if (classificationService == null)
                    return;
 
                var currentDocument = currentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                if (currentDocument == null)
                    return;
 
                var (previousSnapshot, previousDocument, previousRoot) = GetLastProcessedData();
 
                // Optionally pre-calculate the root of the doc so that it is ready to classify
                // once GetTags is called.  Also, attempt to determine a smaller change range span
                // for this document so that we can avoid reporting the entire document as changed.
 
                var currentRoot = await currentDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var changedSpan = await ComputeChangedSpanAsync().ConfigureAwait(false);
 
                lock (_gate)
                {
                    _lastProcessedData = (currentSnapshot, currentDocument, currentRoot);
                }
 
                // Notify the editor now that there were changes.  Note: we do not need to go the
                // UI to do this.  The editor supports TagsChanged being called on any thread.
                this.TagsChanged?.Invoke(this, new SnapshotSpanEventArgs(changedSpan));
 
                return;
 
                static ITextSnapshot GetLatest(ImmutableSegmentedList<ITextSnapshot> snapshots)
                {
                    var latest = snapshots[0];
                    for (var i = 1; i < snapshots.Count; i++)
                    {
                        var snapshot = snapshots[i];
                        if (snapshot.Version.VersionNumber > latest.Version.VersionNumber)
                            latest = snapshot;
                    }
 
                    return latest;
                }
 
                async ValueTask<SnapshotSpan> ComputeChangedSpanAsync()
                {
                    var changeRange = await ComputeChangedRangeAsync().ConfigureAwait(false);
                    return changeRange != null
                        ? currentSnapshot.GetSpan(changeRange.Value.Span.Start, changeRange.Value.NewLength)
                        : currentSnapshot.GetFullSpan();
                }
 
                ValueTask<TextChangeRange?> ComputeChangedRangeAsync()
                {
                    // If we have syntax available fast path the change computation without async or blocking.
                    if (previousRoot != null && currentRoot != null)
                        return new(classificationService.ComputeSyntacticChangeRange(currentDocument.Project.Solution.Services, previousRoot, currentRoot, _diffTimeout, cancellationToken));
 
                    // Otherwise, fall back to the language to compute the difference based on the document contents.
                    if (previousDocument != null)
                        return classificationService.ComputeSyntacticChangeRangeAsync(previousDocument, currentDocument, _diffTimeout, cancellationToken);
 
                    return new ValueTask<TextChangeRange?>();
                }
            }
 
            private (ITextSnapshot? lastSnapshot, Document? lastDocument, SyntaxNode? lastRoot) GetLastProcessedData()
            {
                lock (_gate)
                    return _lastProcessedData;
            }
 
            public IEnumerable<ITagSpan<IClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                using (Logger.LogBlock(FunctionId.Tagger_SyntacticClassification_TagComputer_GetTags, CancellationToken.None))
                {
                    return GetTagsWorker(spans) ?? SpecializedCollections.EmptyEnumerable<ITagSpan<IClassificationTag>>();
                }
            }
 
            private IEnumerable<ITagSpan<IClassificationTag>>? GetTagsWorker(NormalizedSnapshotSpanCollection spans)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
                if (spans.Count == 0 || _workspace == null)
                    return null;
 
                var snapshot = spans[0].Snapshot;
 
                var classificationService = TryGetClassificationService(snapshot);
                if (classificationService == null)
                    return null;
 
                using var _ = ArrayBuilder<ClassifiedSpan>.GetInstance(out var classifiedSpans);
 
                foreach (var span in spans)
                    AddClassifications(span);
 
                return ClassificationUtilities.Convert(_typeMap, snapshot, classifiedSpans);
 
                void AddClassifications(SnapshotSpan span)
                {
                    _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                    // First, get the tree and snapshot that we'll be operating over.
                    var (lastProcessedSnapshot, lastProcessedDocument, lastProcessedRoot) = GetLastProcessedData();
 
                    if (lastProcessedDocument == null)
                    {
                        // We don't have a syntax tree yet.  Just do a lexical classification of the document.
                        AddLexicalClassifications(classificationService, span, classifiedSpans);
                        return;
                    }
 
                    // If we have a document, we must have a snapshot as well.
                    Contract.ThrowIfNull(lastProcessedSnapshot);
 
                    // We have a tree.  However, the tree may be for an older version of the snapshot.
                    // If it is for an older version, then classify that older version and translate
                    // the classifications forward.  Otherwise, just classify normally.
 
                    if (lastProcessedSnapshot.Version.ReiteratedVersionNumber != span.Snapshot.Version.ReiteratedVersionNumber)
                    {
                        // Slightly more complicated.  We have a parse tree, it's just not for the snapshot we're being asked for.
                        AddClassifiedSpansForPreviousDocument(classificationService, span, lastProcessedSnapshot, lastProcessedDocument, lastProcessedRoot, classifiedSpans);
                        return;
                    }
 
                    // Mainline case.  We have the corresponding document for the snapshot we're classifying.
                    AddSyntacticClassificationsForDocument(classificationService, span, lastProcessedDocument, lastProcessedRoot, classifiedSpans);
                }
            }
 
            private void AddLexicalClassifications(IClassificationService classificationService, SnapshotSpan span, ArrayBuilder<ClassifiedSpan> classifiedSpans)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                classificationService.AddLexicalClassifications(
                    span.Snapshot.AsText(), span.Span.ToTextSpan(), classifiedSpans, CancellationToken.None);
            }
 
            private void AddSyntacticClassificationsForDocument(
                IClassificationService classificationService, SnapshotSpan span,
                Document document, SyntaxNode? root, ArrayBuilder<ClassifiedSpan> classifiedSpans)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
                var cancellationToken = CancellationToken.None;
 
                if (_lastLineCache.TryUseCache(span, classifiedSpans))
                    return;
 
                using var _ = ArrayBuilder<ClassifiedSpan>.GetInstance(out var tempList);
 
                // If we have a syntax root ready, use the direct, non-async/non-blocking approach to getting classifications.
                if (root == null)
                    classificationService.AddSyntacticClassificationsAsync(document, span.Span.ToTextSpan(), tempList, cancellationToken).Wait(cancellationToken);
                else
                    classificationService.AddSyntacticClassifications(document.Project.Solution.Services, root, span.Span.ToTextSpan(), tempList, cancellationToken);
 
                _lastLineCache.Update(span, tempList);
                classifiedSpans.AddRange(tempList);
            }
 
            private void AddClassifiedSpansForPreviousDocument(
                IClassificationService classificationService, SnapshotSpan span,
                ITextSnapshot lastProcessedSnapshot, Document lastProcessedDocument, SyntaxNode? lastProcessedRoot,
                ArrayBuilder<ClassifiedSpan> classifiedSpans)
            {
                _taggerProvider._threadingContext.ThrowIfNotOnUIThread();
 
                // Slightly more complicated case.  They're asking for the classifications for a
                // different snapshot than what we have a parse tree for.  So we first translate the span
                // that they're asking for so that is maps onto the tree that we have spans for.  We then
                // get the classifications from that tree.  We then take the results and translate them
                // back to the snapshot they want.  Finally, as some of the classifications may have
                // changed, we check for some common cases and touch them up manually so that things
                // look right for the user.
 
                // 1) translate the requested span onto the right span for the snapshot that corresponds
                //    to the syntax tree.
                var translatedSpan = span.TranslateTo(lastProcessedSnapshot, SpanTrackingMode.EdgeExclusive);
                if (translatedSpan.IsEmpty)
                {
                    // well, there is no information we can get from previous tree, use lexer to
                    // classify given span. soon we will re-classify the region.
                    AddLexicalClassifications(classificationService, span, classifiedSpans);
                }
                else
                {
                    using var _ = ArrayBuilder<ClassifiedSpan>.GetInstance(out var tempList);
                    AddSyntacticClassificationsForDocument(classificationService, translatedSpan, lastProcessedDocument, lastProcessedRoot, tempList);
 
                    var currentSnapshot = span.Snapshot;
                    var currentText = currentSnapshot.AsText();
                    foreach (var lastClassifiedSpan in tempList)
                    {
                        // 2) Translate those classifications forward so that they correspond to the true
                        //    requested snapshot.
                        var lastSnapshotSpan = lastClassifiedSpan.TextSpan.ToSnapshotSpan(lastProcessedSnapshot);
                        var currentSnapshotSpan = lastSnapshotSpan.TranslateTo(currentSnapshot, SpanTrackingMode.EdgeInclusive);
 
                        var currentClassifiedSpan = new ClassifiedSpan(lastClassifiedSpan.ClassificationType, currentSnapshotSpan.Span.ToTextSpan());
 
                        // 3) The classifications may be incorrect due to changes in the text.  For example,
                        //    if "clss" becomes "class", then we want to changes the classification from
                        //    'identifier' to 'keyword'.
                        currentClassifiedSpan = classificationService.AdjustStaleClassification(currentText, currentClassifiedSpan);
 
                        classifiedSpans.Add(currentClassifiedSpan);
                    }
                }
            }
        }
    }
}