Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ce7faab
Add ViewMarginProvider and related code
Cosifne Aug 16, 2021
530ab19
Add the required handlers
Cosifne Aug 16, 2021
3fc2934
Remove GlyphFactory
Cosifne Aug 17, 2021
4fdd348
Auto collapse margin
Cosifne Aug 17, 2021
dee2aa6
Rename file
Cosifne Aug 17, 2021
28b17ba
Add an option to show Inheritance Margin in the indicator margin
sharwell Aug 18, 2021
248b8c1
Fix background color
Cosifne Aug 19, 2021
e187128
Modify the code to use glyph manager
Cosifne Aug 19, 2021
59c6272
Make margin non-user control
Cosifne Aug 19, 2021
f643b8c
Add ClipToBound
Cosifne Aug 19, 2021
9aafa0d
Use a differnt way to put margin to indicate margin because the ClipT…
Cosifne Aug 19, 2021
31e9b55
Polishing the main switching logic
Cosifne Aug 20, 2021
c97bb5a
Rename files
Cosifne Aug 20, 2021
f95b470
Change the content type to make sure the extra margin won't be shown …
Cosifne Aug 20, 2021
77ce8d7
Use a new implementation apporach
Cosifne Aug 20, 2021
09203c7
Remove the partial word
Cosifne Aug 20, 2021
5e59a82
Address feedback and add comments
Cosifne Aug 23, 2021
cbf8da6
Add more comments
Cosifne Aug 23, 2021
26f70e0
Add one extra check
Cosifne Aug 23, 2021
cc7d82e
Merge branch 'main' into dev/shech/InheritanceMarginViewMargin
Cosifne Aug 23, 2021
00b8112
Fix a mistake in comments
Cosifne Aug 23, 2021
d545e06
Update the visibility of the canvas when it is created
Cosifne Aug 24, 2021
c311b18
Move the per-language option to a general option and change the default
Cosifne Aug 25, 2021
3c1b63c
Address nits
Cosifne Aug 25, 2021
5e91484
Use interval tree instead of a dictionary to track all the glyphs
Cosifne Aug 25, 2021
8d66c23
Use the BinarySearchHelper of Roslyn instead
Cosifne Aug 26, 2021
ba47cdf
Check is empty instead of calling Any()
Cosifne Aug 26, 2021
3ca2421
Fix an indentation
Cosifne Aug 26, 2021
bb92dc9
Pass the newSpan
Cosifne Aug 26, 2021
2c40ba2
Merge branch 'main' into dev/shech/InheritanceMarginViewMargin
Cosifne Aug 26, 2021
1e308b8
Address feedbacks
Cosifne Aug 26, 2021
d1d0dfd
Fix formatting
Cosifne Aug 31, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ internal static class FeatureOnOffOptions
defaultValue: true,
new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.ShowInheritanceMargin"));

public static readonly Option2<bool> InheritanceMarginCombinedWithIndicatorMargin =
new(nameof(FeatureOnOffOptions),
nameof(InheritanceMarginCombinedWithIndicatorMargin),
defaultValue: true,
new RoamingProfileStorageLocation($"TextEditor.{nameof(InheritanceMarginCombinedWithIndicatorMargin)}"));

public static readonly Option2<bool> AutomaticallyCompleteStatementOnSemicolon = new(
nameof(FeatureOnOffOptions), nameof(AutomaticallyCompleteStatementOnSemicolon), defaultValue: true,
storageLocation: new RoamingProfileStorageLocation($"TextEditor.{nameof(AutomaticallyCompleteStatementOnSemicolon)}"));
Expand Down Expand Up @@ -120,6 +126,7 @@ public FeatureOnOffOptionsProvider()
FeatureOnOffOptions.AddImportsOnPaste,
FeatureOnOffOptions.OfferRemoveUnusedReferences,
FeatureOnOffOptions.ShowInheritanceMargin,
FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin,
FeatureOnOffOptions.AutomaticallyCompleteStatementOnSemicolon,
FeatureOnOffOptions.SkipAnalyzersForImplicitlyTriggeredBuilds);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@
<CheckBox x:Uid="ShowInheritanceMargin"
x:Name="ShowInheritanceMargin"
Content="{x:Static local:AdvancedOptionPageStrings.Show_inheritance_margin}"/>
<CheckBox x:Uid="InheritanceMarginCombinedWithIndicatorMargin"
x:Name="InheritanceMarginCombinedWithIndicatorMargin"
Content="{x:Static local:AdvancedOptionPageStrings.Combine_inheritance_margin_with_indicator_margin}"/>
</StackPanel>
</GroupBox>
</StackPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +18,9 @@

namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin
{
/// <summary>
/// GlyphFactory provides the InheritanceMargin shows in IndicatorMargin. (Margin shared with breakpoint)
/// </summary>
internal sealed class InheritanceGlyphFactory : IGlyphFactory
{
private readonly IThreadingContext _threadingContext;
Expand Down Expand Up @@ -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<IOptionService>();
// 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Manager controls all the glyphs of Inheritance Margin in <see cref="InheritanceMarginViewMargin"/>.
/// </summary>
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<GlyphData, GlyphDataIntrospector> _glyphDataTree;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noice.


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<InheritanceMarginGlyph, SnapshotSpan>();
_glyphDataTree = new SimpleIntervalTree<GlyphData, GlyphDataIntrospector>(new GlyphDataIntrospector(), values: null);
UpdateBackgroundColor();
}

void IDisposable.Dispose()
{
_editorFormatMap.FormatMappingChanged -= FormatMappingChanged;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider a threading assert for this method as well. (but it's ok to elave out).

}

/// <summary>
/// Generate the glyph by the given <paramref name="tag"/>, and add it to the margin.
/// It should only be called by UI thread because UI elements are manipulated by this method.
/// </summary>
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);
}
}

/// <summary>
/// Remove the glyphs covered by <paramref name="snapshotSpan"/>.
/// It should only be called by UI thread because UI elements are manipulated by this method.
/// </summary>
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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked how the Tagger remove item from the interval tree here https://sourceroslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Tagging/AbstractAsynchronousTaggerProvider.TagSource_ProduceTags.cs,369
But not sure if this is the correct way to do it here.
@CyrusNajmabadi for verify

_glyphDataTree.ClearInPlace();
foreach (var glyphData in remainingGlyphData)
{
_glyphDataTree.AddIntervalInPlace(glyphData);
}
}

/// <summary>
/// Remove the glyphs that are no long visible or covered by the <paramref name="newOrReformattedLines"/>.
/// Refresh all the other the existing glyphs with the <paramref name="snapshot"/>.
/// It should only be called by UI thread because UI elements are manipulated by this method.
/// </summary>
public void SetSnapshotAndUpdate(ITextSnapshot snapshot, IList<ITextViewLine> newOrReformattedLines, IList<ITextViewLine> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we have TagSpanIntervalTree. can you look if that would make your life easier?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like TagSpanIntervalTree is tracking the position of the the tag, but here I need a data structure that could also track the glyphs.
So far I feel SimpleIntervalTree is fine.

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<ITextViewLine> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<GlyphData>
{
public int GetStart(GlyphData data)
=> data.SnapshotSpan.Start;

public int GetLength(GlyphData data)
=> data.SnapshotSpan.Length;
}
}
}
Loading