diff --git a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs index 8f552821c6cc7..93b57a44dd600 100644 --- a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs +++ b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs @@ -84,6 +84,12 @@ internal static class FeatureOnOffOptions defaultValue: true, new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.ShowInheritanceMargin")); + public static readonly Option2 InheritanceMarginCombinedWithIndicatorMargin = + new(nameof(FeatureOnOffOptions), + nameof(InheritanceMarginCombinedWithIndicatorMargin), + defaultValue: true, + new RoamingProfileStorageLocation($"TextEditor.{nameof(InheritanceMarginCombinedWithIndicatorMargin)}")); + public static readonly Option2 AutomaticallyCompleteStatementOnSemicolon = new( nameof(FeatureOnOffOptions), nameof(AutomaticallyCompleteStatementOnSemicolon), defaultValue: true, storageLocation: new RoamingProfileStorageLocation($"TextEditor.{nameof(AutomaticallyCompleteStatementOnSemicolon)}")); @@ -120,6 +126,7 @@ public FeatureOnOffOptionsProvider() FeatureOnOffOptions.AddImportsOnPaste, FeatureOnOffOptions.OfferRemoveUnusedReferences, FeatureOnOffOptions.ShowInheritanceMargin, + FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin, FeatureOnOffOptions.AutomaticallyCompleteStatementOnSemicolon, FeatureOnOffOptions.SkipAnalyzersForImplicitlyTriggeredBuilds); } diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml index 521428aa817e1..5d2016414348f 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml @@ -285,6 +285,9 @@ + diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs index 5d5241e25a15a..f8d55fe7324a2 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs @@ -148,6 +148,7 @@ public AdvancedOptionPageControl(OptionStore optionStore, IComponentModel compon // Leave the null converter here to make sure if the option value is get from the storage (if it is null), the feature will be enabled BindToOption(ShowInheritanceMargin, FeatureOnOffOptions.ShowInheritanceMargin, LanguageNames.CSharp, () => true); + BindToOption(InheritanceMarginCombinedWithIndicatorMargin, FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin); } // Since this dialog is constructed once for the lifetime of the application and VS Theme can be changed after the application has started, diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs index a1678aaf8a7ce..e3ae9efa4c0b5 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs @@ -282,6 +282,9 @@ public static string Option_Skip_analyzers_for_implicitly_triggered_builds public static string Show_inheritance_margin => ServicesVSResources.Show_inheritance_margin; + public static string Combine_inheritance_margin_with_indicator_margin + => ServicesVSResources.Combine_inheritance_margin_with_indicator_margin; + public static string Inheritance_Margin_experimental => ServicesVSResources.Inheritance_Margin_experimental; } diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphFactory.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphFactory.cs index 970d61feaa3be..af42774be7869 100644 --- a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphFactory.cs +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphFactory.cs @@ -4,7 +4,11 @@ using System.Windows; using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Options; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin.MarginGlyph; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Editor; @@ -14,6 +18,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin { + /// + /// GlyphFactory provides the InheritanceMargin shows in IndicatorMargin. (Margin shared with breakpoint) + /// internal sealed class InheritanceGlyphFactory : IGlyphFactory { private readonly IThreadingContext _threadingContext; @@ -45,12 +52,31 @@ public InheritanceGlyphFactory( public UIElement? GenerateGlyph(IWpfTextViewLine line, IGlyphTag tag) { if (tag is not InheritanceMarginTag inheritanceMarginTag) + { return null; + } + + var workspace = _textView.TextBuffer.GetWorkspace(); + if (workspace == null) + { + return null; + } + + var optionService = workspace.Services.GetRequiredService(); + // The life cycle of the glyphs in Indicator Margin is controlled by the editor, + // so in order to get the glyphs removed when FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin is off, + // we need + // 1. Generate tags when this option changes. + // 2. Always return null here to force the editor to remove the glyphs. + var combineWithIndicatorMargin = optionService.GetOption(FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin); + if (!combineWithIndicatorMargin) + { + return null; + } var membersOnLine = inheritanceMarginTag.MembersOnLine; Contract.ThrowIfTrue(membersOnLine.IsEmpty); - - return new MarginGlyph.InheritanceMargin( + return new InheritanceMarginGlyph( _threadingContext, _streamingFindUsagesPresenter, _classificationTypeMap, diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager.cs new file mode 100644 index 0000000000000..cbe7a184cb3a7 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager.cs @@ -0,0 +1,222 @@ +// 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.Windows.Controls; +using System.Windows.Media; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin.MarginGlyph; +using Microsoft.VisualStudio.PlatformUI; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin +{ + /// + /// Manager controls all the glyphs of Inheritance Margin in . + /// + internal partial class InheritanceGlyphManager : ForegroundThreadAffinitizedObject, IDisposable + { + // We want to our glyphs to have the same background color as the glyphs in GlyphMargin. + private const string GlyphMarginName = "Indicator Margin"; + + private readonly double _heightAndWidthOfTheGlyph; + private readonly IWpfTextView _textView; + private readonly IThreadingContext _threadingContext; + private readonly IStreamingFindUsagesPresenter _streamingFindUsagesPresenter; + private readonly ClassificationTypeMap _classificationTypeMap; + private readonly IClassificationFormatMap _classificationFormatMap; + private readonly IUIThreadOperationExecutor _operationExecutor; + private readonly IEditorFormatMap _editorFormatMap; + private readonly IAsynchronousOperationListener _listener; + private readonly Canvas _glyphsContainer; + private readonly SimpleIntervalTree _glyphDataTree; + + public InheritanceGlyphManager( + IWpfTextView textView, + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingFindUsagesPresenter, + IClassificationFormatMap classificationFormatMap, + ClassificationTypeMap classificationTypeMap, + IUIThreadOperationExecutor operationExecutor, + IEditorFormatMap editorFormatMap, + IAsynchronousOperationListener listener, + Canvas canvas, + double heightAndWidthOfTheGlyph) : base(threadingContext) + { + _textView = textView; + _threadingContext = threadingContext; + _streamingFindUsagesPresenter = streamingFindUsagesPresenter; + _classificationTypeMap = classificationTypeMap; + _classificationFormatMap = classificationFormatMap; + _operationExecutor = operationExecutor; + _editorFormatMap = editorFormatMap; + _glyphsContainer = canvas; + _listener = listener; + _heightAndWidthOfTheGlyph = heightAndWidthOfTheGlyph; + _editorFormatMap.FormatMappingChanged += FormatMappingChanged; + + // _glyphToTaggedSpan = new Dictionary(); + _glyphDataTree = new SimpleIntervalTree(new GlyphDataIntrospector(), values: null); + UpdateBackgroundColor(); + } + + void IDisposable.Dispose() + { + _editorFormatMap.FormatMappingChanged -= FormatMappingChanged; + } + + /// + /// Generate the glyph by the given , and add it to the margin. + /// It should only be called by UI thread because UI elements are manipulated by this method. + /// + public void AddGlyph(InheritanceMarginTag tag, SnapshotSpan span) + { + AssertIsForeground(); + var lines = _textView.TextViewLines; + if (lines.IntersectsBufferSpan(span) && GetStartingLine(lines, span) is IWpfTextViewLine line) + { + var glyph = CreateNewGlyph(tag); + SetTop(line, glyph); + _glyphDataTree.AddIntervalInPlace(new GlyphData(span, glyph)); + _glyphsContainer.Children.Add(glyph); + } + } + + /// + /// Remove the glyphs covered by . + /// It should only be called by UI thread because UI elements are manipulated by this method. + /// + public void RemoveGlyphs(SnapshotSpan snapshotSpan) + { + AssertIsForeground(); + var glyphDataToRemove = _glyphDataTree.GetIntervalsThatIntersectWith(snapshotSpan.Start, snapshotSpan.Length); + foreach (var (_, glyph) in glyphDataToRemove) + { + _glyphsContainer.Children.Remove(glyph); + } + + var remainingGlyphData = _glyphDataTree.Except(glyphDataToRemove).ToImmutableArray(); + _glyphDataTree.ClearInPlace(); + foreach (var glyphData in remainingGlyphData) + { + _glyphDataTree.AddIntervalInPlace(glyphData); + } + } + + /// + /// Remove the glyphs that are no long visible or covered by the . + /// Refresh all the other the existing glyphs with the . + /// It should only be called by UI thread because UI elements are manipulated by this method. + /// + public void SetSnapshotAndUpdate(ITextSnapshot snapshot, IList newOrReformattedLines, IList translatedLines) + { + AssertIsForeground(); + if (!_glyphDataTree.IsEmpty()) + { + // Go through all the existing visuals and invalidate or transform as appropriate. + var allGlyphData = _glyphDataTree.ToImmutableArray(); + _glyphDataTree.ClearInPlace(); + foreach (var (span, glyph) in allGlyphData) + { + var newSpan = span.TranslateTo(snapshot, SpanTrackingMode.EdgeInclusive); + if (!_textView.TextViewLines.IntersectsBufferSpan(newSpan) || GetStartingLine(newOrReformattedLines, newSpan) != null) + { + //Either visual is no longer visible or it crosses a line + //that was reformatted. + _glyphsContainer.Children.Remove(glyph); + } + else + { + _glyphDataTree.AddIntervalInPlace(new GlyphData(newSpan, glyph)); + var line = GetStartingLine(translatedLines, newSpan); + if (line != null) + { + SetTop(line, glyph); + } + } + } + } + } + + private void SetTop(ITextViewLine line, InheritanceMarginGlyph glyph) + => Canvas.SetTop(glyph, line.TextTop - _textView.ViewportTop); + + private static ITextViewLine? GetStartingLine(IList lines, Span span) + { + if (lines.Count > 0) + { + var index = lines.ToImmutableArray().BinarySearch(span.Start, CompareWithLineStartAndEnd); + if (index >= 0) + { + return lines[index]; + } + + var lastLine = lines[^1]; + if (lastLine.EndIncludingLineBreak == lastLine.Snapshot.Length && span.Start == lastLine.EndIncludingLineBreak) + { + // As a special case, if the last line ends at the end of the buffer and the span starts at the end of the buffer + // as well, treat is as crossing the last line in the buffer. + return lastLine; + } + } + + return null; + } + + private static int CompareWithLineStartAndEnd(ITextViewLine line, int value) + { + if (value < line.Start) + { + return 1; + } + + // EndIncludingLineBreak usually equals the start of next line (the exclusion is if this is the last line, which will be handled separately), + // and we always prefer to use the line start, so still return -1 when value == line.EndIncludingLineBreak. + if (value >= line.EndIncludingLineBreak) + { + return -1; + } + + return 0; + } + + private InheritanceMarginGlyph CreateNewGlyph(InheritanceMarginTag tag) + => new( + _threadingContext, + _streamingFindUsagesPresenter, + _classificationTypeMap, + _classificationFormatMap, + _operationExecutor, + tag, + _textView, + _listener) + { Height = _heightAndWidthOfTheGlyph, Width = _heightAndWidthOfTheGlyph }; + + private void FormatMappingChanged(object sender, FormatItemsEventArgs e) + => UpdateBackgroundColor(); + + private void UpdateBackgroundColor() + { + AssertIsForeground(); + var resourceDictionary = _editorFormatMap.GetProperties(GlyphMarginName); + if (resourceDictionary.Contains(EditorFormatDefinition.BackgroundColorId)) + { + var backgroundColor = (Color)resourceDictionary[EditorFormatDefinition.BackgroundColorId]; + // Set background color for all the glyphs + ImageThemingUtilities.SetImageBackgroundColor(_glyphsContainer, backgroundColor); + } + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager_IntervalTreeData.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager_IntervalTreeData.cs new file mode 100644 index 0000000000000..d7b26f4f0163f --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceGlyphManager_IntervalTreeData.cs @@ -0,0 +1,40 @@ +// 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.Shared.Collections; +using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin.MarginGlyph; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin +{ + internal partial class InheritanceGlyphManager + { + private record GlyphData + { + public SnapshotSpan SnapshotSpan { get; } + public InheritanceMarginGlyph Glyph { get; } + + public GlyphData(SnapshotSpan snapshotSpan, InheritanceMarginGlyph glyph) + { + SnapshotSpan = snapshotSpan; + Glyph = glyph; + } + + public void Deconstruct(out SnapshotSpan span, out InheritanceMarginGlyph glyph) + { + span = SnapshotSpan; + glyph = Glyph; + } + } + + private readonly struct GlyphDataIntrospector : IIntervalIntrospector + { + public int GetStart(GlyphData data) + => data.SnapshotSpan.Start; + + public int GetLength(GlyphData data) + => data.SnapshotSpan.Length; + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginHelpers.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginHelpers.cs index d9a665b4283c7..acef27ef77e8b 100644 --- a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginHelpers.cs +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginHelpers.cs @@ -80,7 +80,7 @@ public static ImageMoniker GetMoniker(InheritanceRelationship inheritanceRelatio throw ExceptionUtilities.UnexpectedValue(inheritanceRelationship); } - public static ImmutableArray CreateMenuItemViewModelsForSingleMember(ImmutableArray targets) + public static ImmutableArray CreateMenuItemViewModelsForSingleMember(ImmutableArray targets) => targets.OrderBy(target => target.DisplayName) .GroupBy(target => target.RelationToMember) .SelectMany(grouping => CreateMenuItemsWithHeader(grouping.Key, grouping)) @@ -98,20 +98,20 @@ public static ImmutableArray CreateMenuItemViewMod /// HeaderViewModel /// Target5ViewModel /// - public static ImmutableArray CreateMenuItemViewModelsForMultipleMembers(ImmutableArray members) + public static ImmutableArray CreateMenuItemViewModelsForMultipleMembers(ImmutableArray members) { Contract.ThrowIfTrue(members.Length <= 1); // For multiple members, check if all the targets have the same inheritance relationship. // If so, then don't add the header, because it is already indicated by the margin. // Otherwise, add the Header. - return members.SelectAsArray(MemberMenuItemViewModel.CreateWithHeaderInTargets).CastArray(); + return members.SelectAsArray(MemberMenuItemViewModel.CreateWithHeaderInTargets).CastArray(); } - public static ImmutableArray CreateMenuItemsWithHeader( + public static ImmutableArray CreateMenuItemsWithHeader( InheritanceRelationship relationship, IEnumerable targets) { - using var _ = CodeAnalysis.PooledObjects.ArrayBuilder.GetInstance(out var builder); + using var _ = CodeAnalysis.PooledObjects.ArrayBuilder.GetInstance(out var builder); var displayContent = relationship switch { InheritanceRelationship.ImplementedInterface => ServicesVSResources.Implemented_interfaces, diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginTaggerProvider.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginTaggerProvider.cs index d1a5a9671897f..9482b336782b6 100644 --- a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginTaggerProvider.cs +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginTaggerProvider.cs @@ -50,13 +50,16 @@ protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, I // Because we use frozen-partial documents for semantic classification, we may end up with incomplete // semantics (esp. during solution load). Because of this, we also register to hear when the full // compilation is available so that reclassify and bring ourselves up to date. + // Note: Also generate tags when FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin is changed, + // because we want to refresh the glyphs in indicator margin. => new CompilationAvailableTaggerEventSource( subjectBuffer, AsyncListener, TaggerEventSources.OnWorkspaceChanged(subjectBuffer, AsyncListener), TaggerEventSources.OnViewSpanChanged(ThreadingContext, textViewOpt), TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer), - TaggerEventSources.OnOptionChanged(subjectBuffer, FeatureOnOffOptions.ShowInheritanceMargin)); + TaggerEventSources.OnOptionChanged(subjectBuffer, FeatureOnOffOptions.ShowInheritanceMargin), + TaggerEventSources.OnOptionChanged(subjectBuffer, FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin)); protected override IEnumerable GetSpansToTag(ITextView textView, ITextBuffer subjectBuffer) { diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMargin.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMargin.cs new file mode 100644 index 0000000000000..95c4049492d91 --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMargin.cs @@ -0,0 +1,244 @@ +// 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 System.Windows.Media; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Options; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +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 class InheritanceMarginViewMargin : ForegroundThreadAffinitizedObject, IWpfTextViewMargin + { + // Same size as the Glyph Margin + private const double HeightAndWidthOfMargin = 17; + private readonly IWpfTextView _textView; + private readonly ITagAggregator _tagAggregator; + private readonly IOptionService _optionService; + private readonly InheritanceGlyphManager _glyphManager; + private readonly string _languageName; + private readonly Grid _grid; + private readonly Canvas _mainCanvas; + + /// + /// 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. + /// + private bool _refreshAllGlyphs; + private bool _disposed; + + public InheritanceMarginViewMargin(IWpfTextView textView, + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingFindUsagesPresenter, + IUIThreadOperationExecutor operationExecutor, + IClassificationFormatMap classificationFormatMap, + ClassificationTypeMap classificationTypeMap, + ITagAggregator tagAggregator, + IEditorFormatMap editorFormatMap, + IOptionService optionService, + IAsynchronousOperationListener listener, + string languageName) : base(threadingContext) + { + _textView = textView; + _tagAggregator = tagAggregator; + _optionService = optionService; + _languageName = languageName; + _mainCanvas = new Canvas { ClipToBounds = true, Width = HeightAndWidthOfMargin }; + _grid = new Grid(); + _grid.Children.Add(_mainCanvas); + _glyphManager = new InheritanceGlyphManager( + textView, + threadingContext, + streamingFindUsagesPresenter, + classificationFormatMap, + classificationTypeMap, + operationExecutor, + editorFormatMap, + listener, + _mainCanvas, + HeightAndWidthOfMargin); + _refreshAllGlyphs = true; + _disposed = false; + + _tagAggregator.BatchedTagsChanged += OnTagsChanged; + _textView.LayoutChanged += OnLayoutChanged; + _textView.ZoomLevelChanged += OnZoomLevelChanged; + _optionService.OptionChanged += OnRoslynOptionChanged; + + _grid.LayoutTransform = new ScaleTransform( + scaleX: _textView.ZoomLevel / 100, + scaleY: _textView.ZoomLevel / 100); + _grid.LayoutTransform.Freeze(); + UpdateMarginVisibility(); + } + + void IDisposable.Dispose() + { + AssertIsForeground(); + if (!_disposed) + { + _disposed = true; + _tagAggregator.BatchedTagsChanged -= OnTagsChanged; + _textView.LayoutChanged -= OnLayoutChanged; + _textView.ZoomLevelChanged -= OnZoomLevelChanged; + _optionService.OptionChanged -= OnRoslynOptionChanged; + _tagAggregator.Dispose(); + ((IDisposable)_glyphManager).Dispose(); + } + } + + private void OnZoomLevelChanged(object sender, ZoomLevelChangedEventArgs e) + { + _grid.LayoutTransform = e.ZoomTransform; + _refreshAllGlyphs = true; + } + + private void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) + { + _glyphManager.SetSnapshotAndUpdate( + _textView.TextSnapshot, + e.NewOrReformattedLines, + e.VerticalTranslation ? _textView.TextViewLines : e.TranslatedLines); + + IList lines = _refreshAllGlyphs ? _textView.TextViewLines : e.NewOrReformattedLines; + foreach (var line in lines) + { + _glyphManager.RemoveGlyphs(line.Extent); + RefreshGlyphsOver(line); + } + + _refreshAllGlyphs = false; + } + + private void OnRoslynOptionChanged(object sender, OptionChangedEventArgs e) + { + if (e.Option.Equals(FeatureOnOffOptions.ShowInheritanceMargin) || e.Option.Equals(FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin)) + { + UpdateMarginVisibility(); + } + } + + private void UpdateMarginVisibility() + { + var featureEnabled = _optionService.GetOption(FeatureOnOffOptions.ShowInheritanceMargin, _languageName) ?? true; + if (featureEnabled) + { + var showMargin = !_optionService.GetOption(FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin); + if (showMargin) + { + _mainCanvas.Visibility = Visibility.Visible; + return; + } + } + + _mainCanvas.Visibility = Visibility.Collapsed; + } + + private void OnTagsChanged(object sender, BatchedTagsChangedEventArgs e) + { + if (_textView.IsClosed) + { + return; + } + + using var _ = CodeAnalysis.PooledObjects.ArrayBuilder.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 (!_optionService.GetOption(FeatureOnOffOptions.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 _grid; + } + } + + double ITextViewMargin.MarginSize + { + get + { + ThrowIfDisposed(); + return _grid.ActualWidth; + } + } + + bool ITextViewMargin.Enabled + { + get + { + ThrowIfDisposed(); + return true; + } + } + + ITextViewMargin? ITextViewMargin.GetTextViewMargin(string marginName) + => marginName == nameof(InheritanceMarginViewMargin) ? this : null; + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMarginProvider.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMarginProvider.cs new file mode 100644 index 0000000000000..268f69c6ea35b --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/InheritanceMarginViewMarginProvider.cs @@ -0,0 +1,89 @@ +// 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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Editor; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin +{ + [Export(typeof(IWpfTextViewMarginProvider))] + [ContentType(ContentTypeNames.CSharpContentType)] + [ContentType(ContentTypeNames.VisualBasicContentType)] + [Name(nameof(InheritanceMarginViewMarginProvider))] + [MarginContainer(PredefinedMarginNames.Left)] + [Order(After = PredefinedMarginNames.Glyph)] + [TextViewRole(PredefinedTextViewRoles.Document)] + internal class InheritanceMarginViewMarginProvider : IWpfTextViewMarginProvider + { + private readonly IViewTagAggregatorFactoryService _tagAggregatorFactoryService; + private readonly IThreadingContext _threadingContext; + private readonly IStreamingFindUsagesPresenter _streamingFindUsagesPresenter; + private readonly IClassificationFormatMapService _classificationFormatMapService; + private readonly ClassificationTypeMap _classificationTypeMap; + private readonly IUIThreadOperationExecutor _operationExecutor; + private readonly IEditorFormatMapService _editorFormatMapService; + private readonly IAsynchronousOperationListenerProvider _listenerProvider; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InheritanceMarginViewMarginProvider( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingFindUsagesPresenter, + ClassificationTypeMap classificationTypeMap, + IClassificationFormatMapService classificationFormatMapService, + IUIThreadOperationExecutor operationExecutor, + IViewTagAggregatorFactoryService tagAggregatorFactoryService, + IEditorFormatMapService editorFormatMapService, + IAsynchronousOperationListenerProvider listenerProvider) + { + _threadingContext = threadingContext; + _streamingFindUsagesPresenter = streamingFindUsagesPresenter; + _classificationTypeMap = classificationTypeMap; + _classificationFormatMapService = classificationFormatMapService; + _operationExecutor = operationExecutor; + _tagAggregatorFactoryService = tagAggregatorFactoryService; + _editorFormatMapService = editorFormatMapService; + _listenerProvider = listenerProvider; + } + + public IWpfTextViewMargin? CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer) + { + var textView = wpfTextViewHost.TextView; + var tagAggregator = _tagAggregatorFactoryService.CreateTagAggregator(textView); + var editorFormatMap = _editorFormatMapService.GetEditorFormatMap(textView); + + var document = wpfTextViewHost.TextView.TextBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document == null) + { + return null; + } + + var optionService = document.Project.Solution.Workspace.Services.GetRequiredService(); + var listener = _listenerProvider.GetListener(FeatureAttribute.InheritanceMargin); + return new InheritanceMarginViewMargin( + textView, + _threadingContext, + _streamingFindUsagesPresenter, + _operationExecutor, + _classificationFormatMapService.GetClassificationFormatMap("tooltip"), + _classificationTypeMap, + tagAggregator, + editorFormatMap, + optionService, + listener, + document.Project.Language); + } + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/HeaderMenuItemViewModel.cs b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/HeaderMenuItemViewModel.cs index ae8e6d1a71fd8..7b97f8e3bfffe 100644 --- a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/HeaderMenuItemViewModel.cs +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/HeaderMenuItemViewModel.cs @@ -12,7 +12,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMarg /// 'I↓ Implementing members' /// Method 'Bar' /// - internal class HeaderMenuItemViewModel : InheritanceMenuItemViewModel + internal class HeaderMenuItemViewModel : MenuItemViewModel { public HeaderMenuItemViewModel(string displayContent, ImageMoniker imageMoniker, string automationName) : base(displayContent, imageMoniker, automationName) diff --git a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMargin.xaml b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMarginGlyph.xaml similarity index 99% rename from src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMargin.xaml rename to src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMarginGlyph.xaml index a94eca369413a..5fe5f517c54f5 100644 --- a/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMargin.xaml +++ b/src/VisualStudio/Core/Def/Implementation/InheritanceMargin/MarginGlyph/InheritanceMarginGlyph.xaml @@ -1,4 +1,4 @@ -