File: InheritanceMargin\InheritanceMarginViewMargin.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ozsccwvc_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.InheritanceMargin;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Formatting;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin;
 
internal sealed class InheritanceMarginViewMargin : IWpfTextViewMargin
{
    // 16 (width of the crisp image) + 2 * 1 (width of the border) = 18
    private const double HeightAndWidthOfMargin = 18;
    private readonly IWpfTextView _textView;
    private readonly IThreadingContext _threadingContext;
    private readonly ITagAggregator<InheritanceMarginTag> _tagAggregator;
    private readonly IGlobalOptionService _globalOptions;
    private readonly InheritanceGlyphManager _glyphManager;
    private readonly string _languageName;
    private readonly Canvas _mainCanvas;
 
    /// <summary>
    /// A flag indicates all the glyphs in this margin needs be refreshed when the Layout of the TextView changes.
    /// Should only be read or written to by the UI thread.
    /// </summary>
    private bool _refreshAllGlyphs;
    private bool _disposed;
 
    public InheritanceMarginViewMargin(
        Workspace workspace,
        IWpfTextView textView,
        IThreadingContext threadingContext,
        IStreamingFindUsagesPresenter streamingFindUsagesPresenter,
        IUIThreadOperationExecutor operationExecutor,
        IClassificationFormatMap classificationFormatMap,
        ClassificationTypeMap classificationTypeMap,
        ITagAggregator<InheritanceMarginTag> tagAggregator,
        IEditorFormatMap editorFormatMap,
        IGlobalOptionService globalOptions,
        IAsynchronousOperationListener listener,
        string languageName)
    {
        _textView = textView;
        _threadingContext = threadingContext;
        _tagAggregator = tagAggregator;
        _globalOptions = globalOptions;
        _languageName = languageName;
        _mainCanvas = new Canvas { ClipToBounds = true, Width = HeightAndWidthOfMargin };
        _glyphManager = new InheritanceGlyphManager(
            workspace,
            textView,
            threadingContext,
            streamingFindUsagesPresenter,
            classificationFormatMap,
            classificationTypeMap,
            operationExecutor,
            editorFormatMap,
            listener,
            _mainCanvas,
            HeightAndWidthOfMargin);
        _refreshAllGlyphs = true;
        _disposed = false;
 
        _tagAggregator.BatchedTagsChanged += OnTagsChanged;
        _textView.LayoutChanged += OnLayoutChanged;
        _textView.ZoomLevelChanged += OnZoomLevelChanged;
        _globalOptions.AddOptionChangedHandler(this, OnGlobalOptionChanged);
 
        UpdateMarginVisibility();
    }
 
    void IDisposable.Dispose()
    {
        _threadingContext.ThrowIfNotOnUIThread();
        if (!_disposed)
        {
            _disposed = true;
            _tagAggregator.BatchedTagsChanged -= OnTagsChanged;
            _textView.LayoutChanged -= OnLayoutChanged;
            _textView.ZoomLevelChanged -= OnZoomLevelChanged;
            _globalOptions.RemoveOptionChangedHandler(this, OnGlobalOptionChanged);
            _tagAggregator.Dispose();
            ((IDisposable)_glyphManager).Dispose();
        }
    }
 
    private void OnZoomLevelChanged(object sender, ZoomLevelChangedEventArgs e)
    {
        _refreshAllGlyphs = true;
    }
 
    private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        _glyphManager.SetSnapshotAndUpdate(
            _textView.TextSnapshot,
            e.NewOrReformattedLines,
            e.VerticalTranslation ? _textView.TextViewLines : e.TranslatedLines);
 
        IList<ITextViewLine> lines = _refreshAllGlyphs ? _textView.TextViewLines : e.NewOrReformattedLines;
        foreach (var line in lines)
        {
            _glyphManager.RemoveGlyphs(line.Extent);
            RefreshGlyphsOver(line);
        }
 
        _refreshAllGlyphs = false;
    }
 
    private void OnGlobalOptionChanged(object sender, object target, OptionChangedEventArgs e)
    {
        if (e.HasOption(option =>
                option.Equals(InheritanceMarginOptionsStorage.ShowInheritanceMargin) ||
                option.Equals(InheritanceMarginOptionsStorage.InheritanceMarginCombinedWithIndicatorMargin)))
        {
            UpdateMarginVisibility();
        }
    }
 
    private void UpdateMarginVisibility()
    {
        _mainCanvas.Visibility =
            (_globalOptions.GetOption(InheritanceMarginOptionsStorage.ShowInheritanceMargin, _languageName) ?? true) &&
            !_globalOptions.GetOption(InheritanceMarginOptionsStorage.InheritanceMarginCombinedWithIndicatorMargin) ? Visibility.Visible : Visibility.Collapsed;
    }
 
    private void OnTagsChanged(object sender, BatchedTagsChangedEventArgs e)
    {
        if (_textView.IsClosed)
        {
            return;
        }
 
        using var _ = CodeAnalysis.PooledObjects.ArrayBuilder<SnapshotSpan>.GetInstance(out var builder);
        foreach (var mappingSpan in e.Spans)
        {
            var normalizedSpan = mappingSpan.GetSpans(_textView.TextSnapshot);
            builder.AddRange(normalizedSpan);
        }
 
        var changedSnapshotSpans = builder.ToImmutable();
        if (changedSnapshotSpans.Length == 0)
        {
            return;
        }
 
        var startOfChangedSpan = changedSnapshotSpans.Min(span => span.Start);
        var endOfChangedSpan = changedSnapshotSpans.Max(span => span.End);
        var changedSpan = new SnapshotSpan(startOfChangedSpan, endOfChangedSpan);
 
        _glyphManager.RemoveGlyphs(changedSpan);
 
        foreach (var line in _textView.TextViewLines.GetTextViewLinesIntersectingSpan(changedSpan))
        {
            if (line.IsValid)
            {
                RefreshGlyphsOver(line);
            }
        }
    }
 
    private void RefreshGlyphsOver(ITextViewLine textViewLine)
    {
        if (!_globalOptions.GetOption(InheritanceMarginOptionsStorage.InheritanceMarginCombinedWithIndicatorMargin))
        {
            foreach (var mappingTagSpan in _tagAggregator.GetTags(textViewLine.ExtentAsMappingSpan))
            {
                // Only take tag spans with a visible start point and that map to something
                // in the edit buffer and *start* on this line
                if (mappingTagSpan.Span.Start.GetPoint(_textView.VisualSnapshot.TextBuffer, PositionAffinity.Predecessor) != null)
                {
                    var tagSpans = mappingTagSpan.Span.GetSpans(_textView.TextSnapshot);
                    if (tagSpans.Count > 0)
                    {
                        _glyphManager.AddGlyph(mappingTagSpan.Tag, tagSpans[0]);
                    }
                }
            }
        }
    }
 
    private void ThrowIfDisposed()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException(nameof(InheritanceMarginViewMargin));
        }
    }
 
    FrameworkElement IWpfTextViewMargin.VisualElement
    {
        get
        {
            ThrowIfDisposed();
            return _mainCanvas;
        }
    }
 
    double ITextViewMargin.MarginSize
    {
        get
        {
            ThrowIfDisposed();
            return _mainCanvas.ActualWidth;
        }
    }
 
    bool ITextViewMargin.Enabled
    {
        get
        {
            ThrowIfDisposed();
            return true;
        }
    }
 
    ITextViewMargin? ITextViewMargin.GetTextViewMargin(string marginName)
        => marginName == nameof(InheritanceMarginViewMargin) ? this : null;
}