Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin.MarginGlyph;

namespace Microsoft.VisualStudio.LanguageServices.InheritanceMargin
{
internal class InheritanceMarginCanvas : Canvas
{
public event EventHandler<(InheritanceMarginGlyph? glyphAdded, InheritanceMarginGlyph? glyphRemoved)>? OnGlyphsChanged;

protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
OnGlyphsChanged?.Invoke(this, (visualAdded as InheritanceMarginGlyph, visualRemoved as InheritanceMarginGlyph));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.LanguageServices.InheritanceMargin;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
Expand All @@ -25,7 +26,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMarg
internal class InheritanceMarginViewMargin : ForegroundThreadAffinitizedObject, IWpfTextViewMargin
{
// 16 (width of the crisp image) + 2 * 1 (width of the border) = 18
private const double HeightAndWidthOfMargin = 18;
internal const double HeightAndWidthOfMargin = 18;
private readonly IWpfTextView _textView;
private readonly ITagAggregator<InheritanceMarginTag> _tagAggregator;
private readonly IGlobalOptionService _globalOptions;
Expand Down Expand Up @@ -57,7 +58,7 @@ public InheritanceMarginViewMargin(IWpfTextView textView,
_tagAggregator = tagAggregator;
_globalOptions = globalOptions;
_languageName = languageName;
_mainCanvas = new Canvas { ClipToBounds = true, Width = HeightAndWidthOfMargin };
_mainCanvas = new InheritanceMarginCanvas { ClipToBounds = true, Width = HeightAndWidthOfMargin };
_grid = new Grid();
_grid.Children.Add(_mainCanvas);
_glyphManager = new InheritanceGlyphManager(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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.Text;
using System.Threading.Tasks;
using EnvDTE;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.Extensibility.Testing;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.IntegrationTest.Utilities.Input;
using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Test.Utilities;
using Roslyn.VisualStudio.IntegrationTests;
using Roslyn.VisualStudio.IntegrationTests.InProcess;
using Xunit;

namespace Roslyn.VisualStudio.NewIntegrationTests.CSharp
{
public class CSharpInheritanceMarginTests : AbstractEditorTest
{

protected override string LanguageName => LanguageNames.CSharp;

public CSharpInheritanceMarginTests()
: base(nameof(CSharpInheritanceMarginTests))
{
}

[IdeFact]
public async Task TestNavigateInSource()
{
var project = ProjectName;
await TestServices.InheritanceMargin.EnableOptionsAsync(LanguageName, cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.AddFileAsync(project, "Test.cs", cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.OpenFileAsync(project, "Test.cs", HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.SetTextAndEnsureGlyphsAppearAsync(
@"
interface IBar
{
}
class Implementation : IBar
{
}", expectedGlyphsNumberInMargin: 2, HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.ClickTheGlyphOnLine(2, HangMitigatingCancellationToken);

// Move focus to menu item of 'IBar', the destination is targeting 'class Implementation'
await TestServices.Input.SendAsync(VirtualKey.Tab);
// Navigate to the destination
await TestServices.Input.SendAsync(VirtualKey.Enter);
await TestServices.EditorVerifier.TextContainsAsync(@"class Implementation$$", assertCaretPosition: true);
}

[IdeFact]
public async Task TestMultipleItemsOnSameLine()
{
var project = ProjectName;
await TestServices.InheritanceMargin.EnableOptionsAsync(LanguageName, cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.AddFileAsync(project, "Test.cs", cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.OpenFileAsync(project, "Test.cs", HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.SetTextAndEnsureGlyphsAppearAsync(
@"
using System;
interface IBar
{
event EventHandler e1, e2;
}
class Implementation : IBar
{
public event EventHandler e1, e2;
}", expectedGlyphsNumberInMargin: 4, HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.ClickTheGlyphOnLine(5, HangMitigatingCancellationToken);

// The context menu contains two members, e1 and e2.
// Move focus to menu item of 'event e1'
await TestServices.Input.SendAsync(VirtualKey.Tab);
// Expand the submenu
await TestServices.Input.SendAsync(VirtualKey.Enter);
// Navigate to the implemention
await TestServices.Input.SendAsync(VirtualKey.Enter);
await TestServices.EditorVerifier.TextContainsAsync(@"public event EventHandler e1$$, e2;", assertCaretPosition: true);
}

[IdeFact]
public async Task TestNavigateToMetadata()
{
var project = ProjectName;
await TestServices.InheritanceMargin.EnableOptionsAsync(LanguageName, cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.AddFileAsync(project, "Test.cs", cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.OpenFileAsync(project, "Test.cs", HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.SetTextAndEnsureGlyphsAppearAsync(
@"
using System.Collections;
class Implementation : IEnumerable
{
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}", expectedGlyphsNumberInMargin: 2, HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.ClickTheGlyphOnLine(4, HangMitigatingCancellationToken);

// Move focus to menu item of 'class Implementation'
await TestServices.Input.SendAsync(VirtualKey.Tab);
// Navigate to 'IEnumerable'
await TestServices.Input.SendAsync(VirtualKey.Enter);
await TestServices.EditorVerifier.TextContainsAsync(@"public interface IEnumerable$$", assertCaretPosition: true);
await TestServices.EditorVerifier.VerifyActiveViewIsInMetadataWorkspaceAsync(HangMitigatingCancellationToken);
}

[IdeFact]
public async Task TestNavigateToDifferentProjects()
{
await TestServices.InheritanceMargin.EnableOptionsAsync(LanguageNames.CSharp, cancellationToken: HangMitigatingCancellationToken);
await TestServices.InheritanceMargin.EnableOptionsAsync(LanguageNames.VisualBasic, cancellationToken: HangMitigatingCancellationToken);

var csharpProjectName = ProjectName;
var vbProjectName = "TestVBProject";
await TestServices.SolutionExplorer.AddProjectAsync(
vbProjectName, WellKnownProjectTemplates.VisualBasicNetStandardClassLibrary, LanguageNames.VisualBasic, cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.AddFileAsync(vbProjectName, "Test.vb", @"
Namespace MyNs
Public Interface IBar
End Interface
End Namespace");

await TestServices.SolutionExplorer.AddFileAsync(csharpProjectName, "Test.cs", cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.AddProjectReferenceAsync(csharpProjectName, vbProjectName, cancellationToken: HangMitigatingCancellationToken);
await TestServices.SolutionExplorer.OpenFileAsync(csharpProjectName, "Test.cs", HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.SetTextAndEnsureGlyphsAppearAsync(
@"
using TestVBProject.MyNs;
class Implementation : IBar
{
}", expectedGlyphsNumberInMargin: 1, HangMitigatingCancellationToken);

await TestServices.InheritanceMargin.ClickTheGlyphOnLine(4, HangMitigatingCancellationToken);

// Move focus to menu item of 'class Implementation'
await TestServices.Input.SendAsync(VirtualKey.Tab);
// Navigate to 'IBar'
await TestServices.Input.SendAsync(VirtualKey.Enter);
await TestServices.EditorVerifier.TextContainsAsync(@"Public Interface IBar$$", assertCaretPosition: true);
await TestServices.EditorVerifier.VerifyActiveViewIsNotInMetadataWorkspaceAsync(HangMitigatingCancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Extensibility.Testing;
using Microsoft.VisualStudio.IntegrationTest.Utilities;
using Microsoft.VisualStudio.LanguageServices;
Expand Down Expand Up @@ -244,6 +245,36 @@ await TestServices.Workspace.WaitForAllAsyncOperationsAsync(
string.Join(Environment.NewLine, actualTags));
}

public async Task VerifyActiveViewIsInMetadataWorkspaceAsync(CancellationToken cancellationToken)
{
var activeView = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var currentSnapshot = activeView.TextBuffer.CurrentSnapshot;
var document = currentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document is not null)
{
Assert.True(document.Project.Solution.Workspace.Kind is WorkspaceKind.MetadataAsSource);
return;
}

Assert.True(false, $"Can't find document for the snapshot");
throw ExceptionUtilities.Unreachable;
}

public async Task VerifyActiveViewIsNotInMetadataWorkspaceAsync(CancellationToken cancellationToken)
{
var activeView = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var currentSnapshot = activeView.TextBuffer.CurrentSnapshot;
var document = currentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document is not null)
{
Assert.True(document.Project.Solution.Workspace.Kind is not WorkspaceKind.MetadataAsSource);
return;
}

Assert.True(false, $"Can't find document for the snapshot");
throw ExceptionUtilities.Unreachable;
}

private static WorkspaceEventRestorer WithWorkspaceChangedHandler(Workspace workspace, EventHandler<WorkspaceChangeEventArgs> eventHandler)
{
workspace.WorkspaceChanged += eventHandler;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.UnitTests;
using Microsoft.VisualStudio.CorDebugInterop;
using Microsoft.VisualStudio.Extensibility.Testing;
using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin;
using Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin.MarginGlyph;
using Microsoft.VisualStudio.LanguageServices.InheritanceMargin;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Roslyn.VisualStudio.NewIntegrationTests.InProcess
{
[TestService]
internal partial class InheritanceMarginInProcess
{
private const double HeightAndWidthOfTheGlyph = InheritanceMarginViewMargin.HeightAndWidthOfMargin;
private const string MarginName = nameof(InheritanceMarginViewMargin);

public async Task EnableOptionsAsync(string languageName, CancellationToken cancellationToken)
{
var optionService = await GetComponentModelServiceAsync<IGlobalOptionService>(cancellationToken).ConfigureAwait(false);
var showInheritanceMargin = optionService.GetOption(FeatureOnOffOptions.ShowInheritanceMargin, languageName);
var combinedWithIndicatorMargin = optionService.GetOption(FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin);

if (showInheritanceMargin != true)
{
optionService.SetGlobalOption(new OptionKey(FeatureOnOffOptions.ShowInheritanceMargin, languageName), true);
}

if (combinedWithIndicatorMargin)
{
// Glyphs in Indicator margin are owned by editor, and we don't know when the glyphs would be added/removed.
optionService.SetGlobalOption(new OptionKey(FeatureOnOffOptions.InheritanceMarginCombinedWithIndicatorMargin), false);
}
}

public async Task SetTextAndEnsureGlyphsAppearAsync(string text, int expectedGlyphsNumberInMargin, CancellationToken cancellationToken)
{
var margin = await GetTextViewMarginAsync(cancellationToken);
var marginCanvas = (InheritanceMarginCanvas)((Grid)margin.VisualElement).Children[0];
var taskCompletionSource = new TaskCompletionSource<bool>();
using var _ = cancellationToken.Register(() => taskCompletionSource.SetCanceled());

try
{
marginCanvas.OnGlyphsChanged += OnGlyphChanged;

await TestServices.Editor.SetTextAsync(text, cancellationToken);
await taskCompletionSource.Task;
}
finally
{
marginCanvas.OnGlyphsChanged -= OnGlyphChanged;
}

void OnGlyphChanged(object sender, (InheritanceMarginGlyph? glyphAdded, InheritanceMarginGlyph? glyphRemoved) _)
{
if (marginCanvas.Children.Count == expectedGlyphsNumberInMargin)
{
taskCompletionSource.SetResult(true);
}
}
}

public async Task ClickTheGlyphOnLine(int lineNumber, CancellationToken cancellationToken)
{
await WaitForApplicationIdleAsync(cancellationToken);
var glyph = await GetTheGlyphOnLineAsync(lineNumber, cancellationToken);

// Ideally, we should not rely on creating WPF event, and simulate real mouse click to open the context menu.
glyph.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
}

public async Task<InheritanceMarginGlyph> GetTheGlyphOnLineAsync(int lineNumber, CancellationToken cancellationToken)
{
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
var activeView = await TestServices.Editor.GetActiveTextViewAsync(cancellationToken);
var wpfTextViewLine = activeView.TextViewLines[lineNumber - 1];
var midOfTheLine = wpfTextViewLine.TextTop + wpfTextViewLine.Height / 2;
var margin = await GetTextViewMarginAsync(cancellationToken);

var grid = (Grid)margin.VisualElement;
// There will be only one Canvas element.
Assert.True(grid.Children.Count == 1);
var containingCanvas = (Canvas)((Grid)margin.VisualElement).Children[0];

var glyphsOnLine = new List<InheritanceMarginGlyph>();
foreach (var glyph in containingCanvas.Children)
{
if (glyph is InheritanceMarginGlyph inheritanceMarginGlyph)
{
var glyphTop = Canvas.GetTop(inheritanceMarginGlyph);
var glyphBottom = glyphTop + HeightAndWidthOfTheGlyph;
if (midOfTheLine > glyphTop && midOfTheLine < glyphBottom)
{
glyphsOnLine.Add(inheritanceMarginGlyph);
}
}
}

if (glyphsOnLine.Count != 1)
{
Assert.False(true, $"{glyphsOnLine.Count} glpyhs are found at line: {lineNumber}.");
}

return glyphsOnLine[0];
}

private async Task<IWpfTextViewMargin> GetTextViewMarginAsync(CancellationToken cancellationToken)
{
await WaitForApplicationIdleAsync(cancellationToken);
var vsTextManager = await GetRequiredGlobalServiceAsync<SVsTextManager, IVsTextManager>(cancellationToken);
var vsTextView = await vsTextManager.GetActiveViewAsync(JoinableTaskFactory, cancellationToken);
var testViewHost = await vsTextView.GetTextViewHostAsync(JoinableTaskFactory, cancellationToken);
return testViewHost.GetTextViewMargin(MarginName);
}
}
}