File: Classification\CopyPasteAndPrintingClassificationBufferTaggerProvider.Tagger.cs
Web Access
Project: src\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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Classification;
 
internal partial class CopyPasteAndPrintingClassificationBufferTaggerProvider
{
    public sealed class Tagger : IAccurateTagger<IClassificationTag>, IDisposable
    {
        private readonly CopyPasteAndPrintingClassificationBufferTaggerProvider _owner;
        private readonly ITaggerEventSource _eventSource;
        private readonly IGlobalOptionService _globalOptions;
 
        // State for the tagger.  Can be accessed from any thread.  Access should be protected by _gate.
 
        private readonly object _gate = new();
        private TagSpanIntervalTree<IClassificationTag>? _cachedTags;
        private SnapshotSpan? _cachedTaggedSpan;
 
        public Tagger(
            CopyPasteAndPrintingClassificationBufferTaggerProvider owner,
            ITextBuffer subjectBuffer,
            IAsynchronousOperationListener asyncListener,
            IGlobalOptionService globalOptions)
        {
            _owner = owner;
            _globalOptions = globalOptions;
 
            _eventSource = TaggerEventSources.Compose(
                TaggerEventSources.OnWorkspaceChanged(subjectBuffer, asyncListener),
                TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer));
 
            _eventSource.Changed += OnEventSourceChanged;
            _eventSource.Connect();
        }
 
        // Explicitly a no-op.  This classifier does not support change notifications. See comment in
        // OnEventSourceChanged_OnForeground for more details.
        public event EventHandler<SnapshotSpanEventArgs> TagsChanged { add { } remove { } }
 
        public void Dispose()
        {
            _owner._threadingContext.ThrowIfNotOnUIThread();
            _eventSource.Changed -= OnEventSourceChanged;
            _eventSource.Disconnect();
        }
 
        private void OnEventSourceChanged(object? sender, TaggerEventArgs _)
        {
            lock (_gate)
            {
                _cachedTags = null;
                _cachedTaggedSpan = null;
            }
 
            // Note: we explicitly do *not* call into TagsChanged here.  This type exists only for the copy/paste
            // scenario, and in the case the editor always calls into us for the span in question, ignoring
            // TagsChanged, as per DPugh:
            //
            //    For rich text copy, we always call the buffer classifier to get the classifications of the copied
            //    text. It ignores any tags changed events.
            //
            // It's important that we do not call TagsChanged here as the only thing we could do is notify that the
            // entire doc is changed, and that incurs a heavy cost for the editor reacting to that notification.
        }
 
        public IEnumerable<ITagSpan<IClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
        {
            // we never return any tags for GetTags.  This tagger is only for 'Accurate' scenarios.
            return [];
        }
 
        private static IEnumerable<TagSpan<IClassificationTag>> GetIntersectingTags(NormalizedSnapshotSpanCollection spans, TagSpanIntervalTree<IClassificationTag> cachedTags)
        {
            using var pooledObject = SegmentedListPool.GetPooledList<TagSpan<IClassificationTag>>(out var list);
 
            cachedTags.AddIntersectingTagSpans(spans, list);
 
            // Use yield return mechanism to allow the segmented list to get returned back to the
            // pool after usage. This does cause an allocation for the yield state machinery, but
            // that is better than not freeing a potentially large segmented list back to the pool.
            foreach (var item in list)
                yield return item;
        }
 
        IEnumerable<ITagSpan<IClassificationTag>> IAccurateTagger<IClassificationTag>.GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancellationToken)
            => GetAllTags(spans, cancellationToken);
 
        public IEnumerable<TagSpan<IClassificationTag>> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancellationToken)
        {
            if (spans.Count == 0)
                return [];
 
            var snapshot = spans.First().Snapshot;
 
            var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document == null)
                return [];
 
            // We want to classify from the start of the first requested span to the end of the 
            // last requested span.
            var spanToTag = new SnapshotSpan(snapshot, Span.FromBounds(spans.First().Start, spans.Last().End));
 
            var (cachedTaggedSpan, cachedTags) = GetCachedInfo();
 
            // We don't need to actually classify if what we're being asked for is a subspan
            // of the last classification we performed.
            if (cachedTaggedSpan?.Snapshot == snapshot &&
                cachedTaggedSpan.Value.Contains(spanToTag))
            {
                Contract.ThrowIfNull(cachedTags);
                return GetIntersectingTags(spans, cachedTags);
            }
            else
            {
                return ComputeAndCacheAllTags(spans, snapshot, document, spanToTag, cancellationToken);
            }
        }
 
        private IEnumerable<TagSpan<IClassificationTag>> ComputeAndCacheAllTags(
            NormalizedSnapshotSpanCollection spans,
            ITextSnapshot snapshot,
            Document document,
            SnapshotSpan spanToTag,
            CancellationToken cancellationToken)
        {
            var classificationService = document.GetRequiredLanguageService<IClassificationService>();
 
            // Our cache is not there, or is out of date.  We need to compute the up to date results.
            var options = _globalOptions.GetClassificationOptions(document.Project.Language);
 
            // Final list of tags to produce, containing syntax/semantic/embedded classification tags.
            using var _ = SegmentedListPool.GetPooledList<TagSpan<IClassificationTag>>(out var mergedTags);
 
            _owner._threadingContext.JoinableTaskFactory.Run(async () =>
            {
                // Defer to our helper which will compute syntax/semantic/embedded classifications, properly
                // layering them into the final result we return.
                await TotalClassificationAggregateTagger.AddTagsAsync(
                    new NormalizedSnapshotSpanCollection(spanToTag),
                    mergedTags,
                    // We should only be asking for a single span when getting the syntactic classifications
                    GetTaggingFunction(requireSingleSpan: true, (span, buffer) => classificationService.AddSyntacticClassificationsAsync(document, span, buffer, cancellationToken)),
                    // We should only be asking for a single span when getting the semantic classifications
                    GetTaggingFunction(requireSingleSpan: true, (span, buffer) => classificationService.AddSemanticClassificationsAsync(document, span, options, buffer, cancellationToken)),
                    //  Note: many string literal spans may be passed in when getting embedded classifications
                    GetTaggingFunction(requireSingleSpan: false, (span, buffer) => classificationService.AddEmbeddedLanguageClassificationsAsync(document, span, options, buffer, cancellationToken)),
                    arg: default).ConfigureAwait(false);
            });
 
            var cachedTags = new TagSpanIntervalTree<IClassificationTag>(snapshot, SpanTrackingMode.EdgeExclusive, mergedTags);
            lock (_gate)
            {
                _cachedTaggedSpan = spanToTag;
                _cachedTags = cachedTags;
            }
 
            return GetIntersectingTags(spans, cachedTags);
 
            Func<NormalizedSnapshotSpanCollection, SegmentedList<TagSpan<IClassificationTag>>, VoidResult, Task> GetTaggingFunction(
                bool requireSingleSpan, Func<TextSpan, SegmentedList<ClassifiedSpan>, Task> addTagsAsync)
            {
                Contract.ThrowIfTrue(requireSingleSpan && spans.Count != 1, "We should only be asking for a single span");
                return (spans, tempBuffer, _) => AddSpansAsync(spans, tempBuffer, addTagsAsync);
            }
 
            async Task AddSpansAsync(
                NormalizedSnapshotSpanCollection spans,
                SegmentedList<TagSpan<IClassificationTag>> result,
                Func<TextSpan, SegmentedList<ClassifiedSpan>, Task> addAsync)
            {
                // temp buffer we can use across all our classification calls.  Should be cleared between each call.
                using var _ = Classifier.GetPooledList(out var tempBuffer);
 
                foreach (var span in spans)
                {
                    tempBuffer.Clear();
                    await addAsync(span.Span.ToTextSpan(), tempBuffer).ConfigureAwait(false);
 
                    foreach (var classifiedSpan in tempBuffer)
                        result.Add(ClassificationUtilities.Convert(_owner._typeMap, snapshot, classifiedSpan));
                }
            }
        }
 
        private (SnapshotSpan? cachedTaggedSpan, TagSpanIntervalTree<IClassificationTag>? cachedTags) GetCachedInfo()
        {
            lock (_gate)
                return (_cachedTaggedSpan, _cachedTags);
        }
    }
}