diff --git a/src/EditorFeatures/Core.Wpf/Adornments/AbstractAdornmentManagerProvider.cs b/src/EditorFeatures/Core.Wpf/Adornments/AbstractAdornmentManagerProvider.cs index b14b99ceca4df..c6265c6c55bc8 100644 --- a/src/EditorFeatures/Core.Wpf/Adornments/AbstractAdornmentManagerProvider.cs +++ b/src/EditorFeatures/Core.Wpf/Adornments/AbstractAdornmentManagerProvider.cs @@ -18,23 +18,25 @@ internal abstract class AbstractAdornmentManagerProvider : IWpfTextViewCreationListener where TTag : GraphicsTag { - private readonly IThreadingContext _threadingContext; - private readonly IViewTagAggregatorFactoryService _tagAggregatorFactoryService; - private readonly IAsynchronousOperationListener _asyncListener; + protected readonly IThreadingContext ThreadingContext; + protected readonly IViewTagAggregatorFactoryService TagAggregatorFactoryService; + protected readonly IAsynchronousOperationListener AsyncListener; protected AbstractAdornmentManagerProvider( IThreadingContext threadingContext, IViewTagAggregatorFactoryService tagAggregatorFactoryService, IAsynchronousOperationListenerProvider listenerProvider) { - _threadingContext = threadingContext; - _tagAggregatorFactoryService = tagAggregatorFactoryService; - _asyncListener = listenerProvider.GetListener(this.FeatureAttributeName); + ThreadingContext = threadingContext; + TagAggregatorFactoryService = tagAggregatorFactoryService; + AsyncListener = listenerProvider.GetListener(this.FeatureAttributeName); } protected abstract string FeatureAttributeName { get; } protected abstract string AdornmentLayerName { get; } + protected abstract void CreateAdornmentManager(IWpfTextView textView); + public void TextViewCreated(IWpfTextView textView) { if (textView == null) @@ -47,8 +49,7 @@ public void TextViewCreated(IWpfTextView textView) return; } - // the manager keeps itself alive by listening to text view events. - AdornmentManager.Create(_threadingContext, textView, _tagAggregatorFactoryService, _asyncListener, AdornmentLayerName); + CreateAdornmentManager(textView); } } } diff --git a/src/EditorFeatures/Core.Wpf/Adornments/AdornmentManager.cs b/src/EditorFeatures/Core.Wpf/Adornments/AdornmentManager.cs index 1ffa5890307cf..b9f6b997a3848 100644 --- a/src/EditorFeatures/Core.Wpf/Adornments/AdornmentManager.cs +++ b/src/EditorFeatures/Core.Wpf/Adornments/AdornmentManager.cs @@ -22,42 +22,36 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.Adornments /// /// UI manager for graphic overlay tags. These tags will simply paint something related to the text. /// - internal class AdornmentManager where T : GraphicsTag + internal abstract class AdornmentManager where T : GraphicsTag { private readonly object _invalidatedSpansLock = new object(); private readonly IThreadingContext _threadingContext; - /// View that created us. - private readonly IWpfTextView _textView; - - /// Layer where we draw adornments. - private readonly IAdornmentLayer _adornmentLayer; - - /// Aggregator that tells us where to draw. - private readonly ITagAggregator _tagAggregator; - /// Notification system about operations we do private readonly IAsynchronousOperationListener _asyncListener; /// Spans that are invalidated, and need to be removed from the layer.. private List _invalidatedSpans; - public static AdornmentManager Create( - IThreadingContext threadingContext, - IWpfTextView textView, - IViewTagAggregatorFactoryService aggregatorService, - IAsynchronousOperationListener asyncListener, - string adornmentLayerName) - { - Contract.ThrowIfNull(threadingContext); - Contract.ThrowIfNull(textView); - Contract.ThrowIfNull(aggregatorService); - Contract.ThrowIfNull(adornmentLayerName); - Contract.ThrowIfNull(asyncListener); + /// View that created us. + protected IWpfTextView TextView { get; } - return new AdornmentManager(threadingContext, textView, aggregatorService, asyncListener, adornmentLayerName); - } + /// Layer where we draw adornments. + protected IAdornmentLayer AdornmentLayer { get; } + + /// Aggregator that tells us where to draw. + protected ITagAggregator TagAggregator { get; } + + /// + /// MUST BE CALLED ON UI THREAD!!!! This method touches WPF. + /// + /// This is where we apply visuals to the text. + /// + /// It happens when another region of the view becomes visible or there is a change in tags. + /// For us the end result is the same - get tags from tagger and update visuals correspondingly. + /// + protected abstract void UpdateSpans_CallOnlyOnUIThread(NormalizedSnapshotSpanCollection changedSpanCollection, bool removeOldTags); internal AdornmentManager( IThreadingContext threadingContext, @@ -73,8 +67,8 @@ internal AdornmentManager( Contract.ThrowIfNull(asyncListener); _threadingContext = threadingContext; - _textView = textView; - _adornmentLayer = textView.GetAdornmentLayer(adornmentLayerName); + TextView = textView; + AdornmentLayer = textView.GetAdornmentLayer(adornmentLayerName); textView.LayoutChanged += OnLayoutChanged; _asyncListener = asyncListener; @@ -82,20 +76,20 @@ internal AdornmentManager( Contract.ThrowIfFalse(textView.VisualElement.Dispatcher.CheckAccess()); textView.Closed += OnTextViewClosed; - _tagAggregator = tagAggregatorFactoryService.CreateTagAggregator(textView); + TagAggregator = tagAggregatorFactoryService.CreateTagAggregator(textView); - _tagAggregator.TagsChanged += OnTagsChanged; + TagAggregator.TagsChanged += OnTagsChanged; } private void OnTextViewClosed(object sender, System.EventArgs e) { // release the aggregator - _tagAggregator.TagsChanged -= OnTagsChanged; - _tagAggregator.Dispose(); + TagAggregator.TagsChanged -= OnTagsChanged; + TagAggregator.Dispose(); // unhook from view - _textView.Closed -= OnTextViewClosed; - _textView.LayoutChanged -= OnLayoutChanged; + TextView.Closed -= OnTextViewClosed; + TextView.LayoutChanged -= OnLayoutChanged; // At this point, this object should be available for garbage collection. } @@ -110,10 +104,10 @@ private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) using (_asyncListener.BeginAsyncOperation(GetType() + ".OnLayoutChanged")) { // Make sure we're on the UI thread. - Contract.ThrowIfFalse(_textView.VisualElement.Dispatcher.CheckAccess()); + Contract.ThrowIfFalse(TextView.VisualElement.Dispatcher.CheckAccess()); var reformattedSpans = e.NewOrReformattedSpans; - var viewSnapshot = _textView.TextSnapshot; + var viewSnapshot = TextView.TextSnapshot; // No need to remove tags as these spans are reformatted anyways. UpdateSpans_CallOnlyOnUIThread(reformattedSpans, removeOldTags: false); @@ -182,7 +176,7 @@ private void OnTagsChanged(object sender, TagsChangedEventArgs e) if (needToScheduleUpdate) { // schedule an update - _threadingContext.JoinableTaskFactory.WithPriority(_textView.VisualElement.Dispatcher, DispatcherPriority.Render).RunAsync(async () => + _threadingContext.JoinableTaskFactory.WithPriority(TextView.VisualElement.Dispatcher, DispatcherPriority.Render).RunAsync(async () => { using (_asyncListener.BeginAsyncOperation(GetType() + ".OnTagsChanged.2")) { @@ -205,7 +199,7 @@ private void UpdateInvalidSpans() using (Logger.LogBlock(FunctionId.Tagger_AdornmentManager_UpdateInvalidSpans, CancellationToken.None)) { // this method should only run on UI thread as we do WPF here. - Contract.ThrowIfFalse(_textView.VisualElement.Dispatcher.CheckAccess()); + Contract.ThrowIfFalse(TextView.VisualElement.Dispatcher.CheckAccess()); List invalidated; lock (_invalidatedSpansLock) @@ -214,118 +208,25 @@ private void UpdateInvalidSpans() _invalidatedSpans = null; } - if (_textView.IsClosed) + if (TextView.IsClosed) { return; // already closed } if (invalidated != null) { - var viewSnapshot = _textView.TextSnapshot; + var viewSnapshot = TextView.TextSnapshot; var invalidatedNormalized = TranslateAndNormalize(invalidated, viewSnapshot); UpdateSpans_CallOnlyOnUIThread(invalidatedNormalized, removeOldTags: true); } } } - /// - /// MUST BE CALLED ON UI THREAD!!!! This method touches WPF. - /// - /// This is where we apply visuals to the text. - /// - /// It happens when another region of the view becomes visible or there is a change in tags. - /// For us the end result is the same - get tags from tagger and update visuals correspondingly. - /// - private void UpdateSpans_CallOnlyOnUIThread(NormalizedSnapshotSpanCollection changedSpanCollection, bool removeOldTags) - { - Contract.ThrowIfNull(changedSpanCollection); - - // this method should only run on UI thread as we do WPF here. - Contract.ThrowIfFalse(_textView.VisualElement.Dispatcher.CheckAccess()); - - var viewSnapshot = _textView.TextSnapshot; - var visualSnapshot = _textView.VisualSnapshot; - - var viewLines = _textView.TextViewLines; - if (viewLines == null || viewLines.Count == 0) - { - return; // nothing to draw on - } - - // removing is a separate pass from adding so that new stuff is not removed. - if (removeOldTags) - { - foreach (var changedSpan in changedSpanCollection) - { - // is there any effect on the view? - if (viewLines.IntersectsBufferSpan(changedSpan)) - { - _adornmentLayer.RemoveAdornmentsByVisualSpan(changedSpan); - } - } - } - - foreach (var changedSpan in changedSpanCollection) - { - // is there any effect on the view? - if (!viewLines.IntersectsBufferSpan(changedSpan)) - { - continue; - } - - var tagSpans = _tagAggregator.GetTags(changedSpan); - foreach (var tagMappingSpan in tagSpans) - { - // We don't want to draw line separators if they would intersect a collapsed outlining - // region. So we test if we can map the start of the line separator up to our visual - // snapshot. If we can't, then we just skip it. - var point = tagMappingSpan.Span.Start.GetPoint(changedSpan.Snapshot, PositionAffinity.Predecessor); - if (point == null) - { - continue; - } - - var mappedPoint = _textView.BufferGraph.MapUpToSnapshot( - point.Value, PointTrackingMode.Negative, PositionAffinity.Predecessor, _textView.VisualSnapshot); - if (mappedPoint == null) - { - continue; - } - - if (!TryMapToSingleSnapshotSpan(tagMappingSpan.Span, viewSnapshot, out var span)) - { - continue; - } - - if (!viewLines.IntersectsBufferSpan(span)) - { - // span is outside of the view so we will not get geometry for it, but may - // spent a lot of time trying. - continue; - } - - // add the visual to the adornment layer. - var geometry = viewLines.GetMarkerGeometry(span); - if (geometry != null) - { - var tag = tagMappingSpan.Tag; - var graphicsResult = tag.GetGraphics(_textView, geometry); - _adornmentLayer.AddAdornment( - behavior: AdornmentPositioningBehavior.TextRelative, - visualSpan: span, - tag: tag, - adornment: graphicsResult.VisualElement, - removedCallback: delegate { graphicsResult.Dispose(); }); - } - } - } - } - // Map the mapping span to the visual snapshot. note that as a result of projection // topology, originally single span may be mapped into several spans. Visual adornments do // not make much sense on disjoint spans. We will not decorate spans that could not make it // in one piece. - private static bool TryMapToSingleSnapshotSpan(IMappingSpan mappingSpan, ITextSnapshot viewSnapshot, out SnapshotSpan span) + protected static bool TryMapToSingleSnapshotSpan(IMappingSpan mappingSpan, ITextSnapshot viewSnapshot, out SnapshotSpan span) { // IMappingSpan.GetSpans is a surprisingly expensive function that allocates multiple // lists and collection if the view buffer is same as anchor we could just map the diff --git a/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManager.cs b/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManager.cs new file mode 100644 index 0000000000000..772406ebfbf07 --- /dev/null +++ b/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManager.cs @@ -0,0 +1,117 @@ +// 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 Microsoft.CodeAnalysis.Editor.Implementation.Adornments; +using Microsoft.CodeAnalysis.Editor.Implementation.LineSeparators; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.LineSeparators +{ + internal class LineSeparatorAdornmentManager : AdornmentManager + { + public LineSeparatorAdornmentManager(IThreadingContext threadingContext, IWpfTextView textView, + IViewTagAggregatorFactoryService tagAggregatorFactoryService, IAsynchronousOperationListener asyncListener, string adornmentLayerName) + : base(threadingContext, textView, tagAggregatorFactoryService, asyncListener, adornmentLayerName) + { + } + + /// + /// MUST BE CALLED ON UI THREAD!!!! This method touches WPF. + /// + /// This is where we apply visuals to the text. + /// + /// It happens when another region of the view becomes visible or there is a change in tags. + /// For us the end result is the same - get tags from tagger and update visuals correspondingly. + /// + protected override void UpdateSpans_CallOnlyOnUIThread(NormalizedSnapshotSpanCollection changedSpanCollection, bool removeOldTags) + { + Contract.ThrowIfNull(changedSpanCollection); + + // this method should only run on UI thread as we do WPF here. + Contract.ThrowIfFalse(TextView.VisualElement.Dispatcher.CheckAccess()); + + var viewSnapshot = TextView.TextSnapshot; + var visualSnapshot = TextView.VisualSnapshot; + + var viewLines = TextView.TextViewLines; + if (viewLines == null || viewLines.Count == 0) + { + return; // nothing to draw on + } + + // removing is a separate pass from adding so that new stuff is not removed. + if (removeOldTags) + { + foreach (var changedSpan in changedSpanCollection) + { + // is there any effect on the view? + if (viewLines.IntersectsBufferSpan(changedSpan)) + { + AdornmentLayer.RemoveAdornmentsByVisualSpan(changedSpan); + } + } + } + + foreach (var changedSpan in changedSpanCollection) + { + // is there any effect on the view? + if (!viewLines.IntersectsBufferSpan(changedSpan)) + { + continue; + } + + var tagSpans = TagAggregator.GetTags(changedSpan); + foreach (var tagMappingSpan in tagSpans) + { + // We don't want to draw line separators if they would intersect a collapsed outlining + // region. So we test if we can map the start of the line separator up to our visual + // snapshot. If we can't, then we just skip it. + var point = tagMappingSpan.Span.Start.GetPoint(changedSpan.Snapshot, PositionAffinity.Predecessor); + if (point == null) + { + continue; + } + + var mappedPoint = TextView.BufferGraph.MapUpToSnapshot( + point.Value, PointTrackingMode.Negative, PositionAffinity.Predecessor, TextView.VisualSnapshot); + if (mappedPoint == null) + { + continue; + } + + if (!TryMapToSingleSnapshotSpan(tagMappingSpan.Span, viewSnapshot, out var span)) + { + continue; + } + + if (!viewLines.IntersectsBufferSpan(span)) + { + // span is outside of the view so we will not get geometry for it, but may + // spent a lot of time trying. + continue; + } + + // add the visual to the adornment layer. + var geometry = viewLines.GetMarkerGeometry(span); + if (geometry != null) + { + var tag = tagMappingSpan.Tag; + var graphicsResult = tag.GetGraphics(TextView, geometry); + AdornmentLayer.AddAdornment( + behavior: AdornmentPositioningBehavior.TextRelative, + visualSpan: span, + tag: tag, + adornment: graphicsResult.VisualElement, + removedCallback: delegate { graphicsResult.Dispose(); }); + } + } + } + } + } +} diff --git a/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManagerProvider.cs b/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManagerProvider.cs index 04596b37595cf..c1852a17a3751 100644 --- a/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManagerProvider.cs +++ b/src/EditorFeatures/Core.Wpf/LineSeparators/LineSeparatorAdornmentManagerProvider.cs @@ -7,6 +7,9 @@ using System; using System.ComponentModel.Composition; using Microsoft.CodeAnalysis.Editor.Implementation.Adornments; +using Microsoft.CodeAnalysis.Editor.LineSeparators; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; +using Microsoft.CodeAnalysis.Editor.Shared.Options; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.TestHooks; @@ -49,5 +52,11 @@ public LineSeparatorAdornmentManagerProvider( protected override string FeatureAttributeName => FeatureAttribute.LineSeparators; protected override string AdornmentLayerName => LayerName; + + protected override void CreateAdornmentManager(IWpfTextView textView) + { + // the manager keeps itself alive by listening to text view events. + _ = new LineSeparatorAdornmentManager(ThreadingContext, textView, TagAggregatorFactoryService, AsyncListener, AdornmentLayerName); + } } }