diff --git a/src/EditorFeatures/CSharpTest/MoveStaticMembers/CSharpMoveStaticMembersTests.cs b/src/EditorFeatures/CSharpTest/MoveStaticMembers/CSharpMoveStaticMembersTests.cs index 06611eeb08b5f..ee5113cb83832 100644 --- a/src/EditorFeatures/CSharpTest/MoveStaticMembers/CSharpMoveStaticMembersTests.cs +++ b/src/EditorFeatures/CSharpTest/MoveStaticMembers/CSharpMoveStaticMembersTests.cs @@ -19,7 +19,7 @@ public class CSharpMoveStaticMembersTests { private static readonly TestComposition s_testServices = FeaturesTestCompositions.Features.AddParts(typeof(TestMoveStaticMembersService)); - #region Perform Actions From Options + #region Perform New Type Action From Options [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] public async Task TestMoveField() { @@ -2193,6 +2193,433 @@ public static int TestMethod() } #endregion + #region Perform Existing Type Action From Options + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveFieldToExistingType() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Field = 1; +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestField"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestField = 1; +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMovePropertyToExistingType() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Property { get; set; } +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestProperty"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestProperty { get; set; } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveEventToExistingType() + { + var initialSourceMarkup = @" +using System; + +public class Class1 +{ + public static event EventHandler Test[||]Event; +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestEvent"); + var fixedSourceMarkup = @" +using System; + +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +using System; + +public class Class1Helpers +{ + public static event EventHandler TestEvent; +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveMethodToExistingType() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Method() + { + return 0; + } +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestMethod() + { + return 0; + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveExtensionMethodToExistingType() + { + var initialSourceMarkup = @" +public static class Class1 +{ + public static int Test[||]Method(this Other other) + { + return other.OtherInt + 2; + } +} + +public class Other +{ + public int OtherInt; + public Other() + { + OtherInt = 5; + } +}"; + var initialDestinationMarkup = @" +public static class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +public static class Class1 +{ +} + +public class Other +{ + public int OtherInt; + public Other() + { + OtherInt = 5; + } +}"; + var fixedDestinationMarkup = @" +public static class Class1Helpers +{ + public static int TestMethod(this Other other) + { + return other.OtherInt + 2; + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveConstFieldToExistingType() + { + var initialSourceMarkup = @" +public class Class1 +{ + public const int Test[||]Field = 1; +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestField"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public const int TestField = 1; +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveMethodToExistingTypeWithNamespace() + { + var initialSourceMarkup = @" +namespace TestNs +{ + public class Class1 + { + public static int Test[||]Method() + { + return 0; + } + } +}"; + var initialDestinationMarkup = @" +namespace TestNs +{ + public class Class1Helpers + { + } +}"; + var selectedDestinationName = "TestNs.Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +namespace TestNs +{ + public class Class1 + { + } +}"; + var fixedDestinationMarkup = @" +namespace TestNs +{ + public class Class1Helpers + { + public static int TestMethod() + { + return 0; + } + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveMethodToExistingTypeWithNewNamespace() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Method() + { + return 0; + } +}"; + var initialDestinationMarkup = @" +namespace TestNs +{ + public class Class1Helpers + { + } +}"; + var selectedDestinationName = "TestNs.Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +namespace TestNs +{ + public class Class1Helpers + { + public static int TestMethod() + { + return 0; + } + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveMethodToExistingTypeRefactorSourceUsage() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Method() + { + return 0; + } + + public static int TestMethod2() + { + return TestMethod(); + } +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +public class Class1 +{ + public static int TestMethod2() + { + return Class1Helpers.TestMethod(); + } +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestMethod() + { + return 0; + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] + public async Task TestMoveMethodToExistingTypeRefactorDestinationUsage() + { + var initialSourceMarkup = @" +public class Class1 +{ + public static int Test[||]Method() + { + return 0; + } +}"; + var initialDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestMethod2() + { + return Class1.TestMethod(); + } +}"; + var selectedDestinationName = "Class1Helpers"; + var selectedMembers = ImmutableArray.Create("TestMethod"); + var fixedSourceMarkup = @" +public class Class1 +{ +}"; + var fixedDestinationMarkup = @" +public class Class1Helpers +{ + public static int TestMethod() + { + return 0; + } + public static int TestMethod2() + { + return Class1Helpers.TestMethod(); + } +}"; + + await TestMovementExistingFileAsync( + initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + selectedMembers, + selectedDestinationName).ConfigureAwait(false); + } + #endregion + #region Selections and caret position [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] @@ -2567,54 +2994,38 @@ public class Class1 } #endregion - [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsMoveStaticMembers)] - public async Task NoOptionsService_NoAction() - { - var initialMarkup = @" -namespace TestNs1 -{ - public class Class1 - { - public static int TestField = 1;[||] - } -}"; - await TestNoRefactoringAsync(initialMarkup, hostServices: FeaturesTestCompositions.Features.GetHostServices()).ConfigureAwait(false); - } - private class Test : VerifyCS.Test { public Test( string destinationType, ImmutableArray selection, - string destinationName = "a.cs", - HostServices? hostServices = null) + string? destinationName, + bool createNew = true) { _destinationType = destinationType; _selection = selection; _destinationName = destinationName; - _hostServices = hostServices; + _createNew = createNew; } private readonly string _destinationType; private readonly ImmutableArray _selection; - private readonly string _destinationName; + private readonly string? _destinationName; - private readonly HostServices? _hostServices; + private readonly bool _createNew; protected override Workspace CreateWorkspaceImpl() { - var hostServices = _hostServices ?? s_testServices.GetHostServices(); + var hostServices = s_testServices.GetHostServices(); var workspace = new AdhocWorkspace(hostServices); - var testOptionsService = workspace.Services.GetService() as TestMoveStaticMembersService; - if (testOptionsService is not null) - { - testOptionsService.DestinationType = _destinationType; - testOptionsService.SelectedMembers = _selection; - testOptionsService.Filename = _destinationName; - } + var testOptionsService = (TestMoveStaticMembersService)workspace.Services.GetRequiredService(); + testOptionsService.DestinationName = _destinationType; + testOptionsService.SelectedMembers = _selection; + testOptionsService.Filename = _destinationName; + testOptionsService.CreateNew = _createNew; return workspace; } @@ -2640,9 +3051,35 @@ private static async Task TestMovementNewFileAsync( }, }.RunAsync().ConfigureAwait(false); - private static async Task TestNoRefactoringAsync(string initialMarkup, HostServices? hostServices = null) + private static async Task TestMovementExistingFileAsync( + string intialSourceMarkup, + string initialDestinationMarkup, + string fixedSourceMarkup, + string fixedDestinationMarkup, + ImmutableArray selectedMembers, + string selectedDestinationType, + string? selectedDestinationFile = null) + { + var test = new Test(selectedDestinationType, selectedMembers, selectedDestinationFile, createNew: false); + test.TestState.Sources.Add(intialSourceMarkup); + test.FixedState.Sources.Add(fixedSourceMarkup); + if (selectedDestinationFile != null) + { + test.TestState.Sources.Add((selectedDestinationFile, initialDestinationMarkup)); + test.FixedState.Sources.Add((selectedDestinationFile, fixedDestinationMarkup)); + } + else + { + test.TestState.Sources.Add(initialDestinationMarkup); + test.FixedState.Sources.Add(fixedDestinationMarkup); + } + + await test.RunAsync().ConfigureAwait(false); + } + + private static async Task TestNoRefactoringAsync(string initialMarkup) { - await new Test("", ImmutableArray.Empty, hostServices: hostServices) + await new Test("", ImmutableArray.Empty, "") { TestCode = initialMarkup, FixedCode = initialMarkup, diff --git a/src/EditorFeatures/TestUtilities/MoveStaticMembers/TestMoveStaticMembersService.cs b/src/EditorFeatures/TestUtilities/MoveStaticMembers/TestMoveStaticMembersService.cs index bc02f360ee914..0e39174413f81 100644 --- a/src/EditorFeatures/TestUtilities/MoveStaticMembers/TestMoveStaticMembersService.cs +++ b/src/EditorFeatures/TestUtilities/MoveStaticMembers/TestMoveStaticMembersService.cs @@ -4,8 +4,11 @@ using System.Collections.Immutable; using System.Composition; +using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.MoveStaticMembers; +using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.Test.Utilities.MoveStaticMembers { @@ -20,23 +23,31 @@ public TestMoveStaticMembersService() { } - public string? DestinationType { get; set; } + public string? DestinationName { get; set; } public ImmutableArray SelectedMembers { get; set; } public string? Filename { get; set; } + public bool CreateNew { get; set; } = true; + public MoveStaticMembersOptions GetMoveMembersToTypeOptions(Document document, INamedTypeSymbol selectedType, ISymbol? selectedNodeSymbol) { var selectedMembers = selectedType.GetMembers().WhereAsArray(symbol => SelectedMembers.Contains(symbol.Name)); - var namespaceDisplay = selectedType.ContainingNamespace.IsGlobalNamespace - ? string.Empty - : selectedType.ContainingNamespace.ToDisplayString(); - // just return all the selected members - return new MoveStaticMembersOptions( - Filename!, - string.Join(".", namespaceDisplay, DestinationType!), - selectedMembers); + if (CreateNew) + { + var namespaceDisplay = selectedType.ContainingNamespace.IsGlobalNamespace + ? string.Empty + : selectedType.ContainingNamespace.ToDisplayString(); + // just return all the selected members + return new MoveStaticMembersOptions( + Filename!, + string.Join(".", namespaceDisplay, DestinationName!), + selectedMembers); + } + + var destination = selectedType.ContainingNamespace.GetAllTypes(CancellationToken.None).First(t => t.ToDisplayString() == DestinationName); + return new MoveStaticMembersOptions(destination, selectedMembers); } } } diff --git a/src/EditorFeatures/VisualBasicTest/MoveStaticMembers/VisualBasicMoveStaticMembersTests.vb b/src/EditorFeatures/VisualBasicTest/MoveStaticMembers/VisualBasicMoveStaticMembersTests.vb index fd4c3d85b4b84..6d8431c459ed0 100644 --- a/src/EditorFeatures/VisualBasicTest/MoveStaticMembers/VisualBasicMoveStaticMembersTests.vb +++ b/src/EditorFeatures/VisualBasicTest/MoveStaticMembers/VisualBasicMoveStaticMembersTests.vb @@ -14,7 +14,7 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.MoveStaticMembers Private Shared ReadOnly s_testServices As TestComposition = FeaturesTestCompositions.Features.AddParts(GetType(TestMoveStaticMembersService)) -#Region "Perform Actions From Options" +#Region "Perform New Type Action From Options" Public Async Function TestMoveField() As Task Dim initialMarkup = " @@ -1936,6 +1936,445 @@ End Namespace End Function #End Region +#Region "Perform Existing Type Action From Options" + + Public Async Function TestMoveFieldToExistingType() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Test[||]Field As Integer = 0 +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestField") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared TestField As Integer = 0 +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMovePropertyToExistingType() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared ReadOnly Property Test[||]Prop As Integer + Get + Return 0 + End Get + End Property +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestProp") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared ReadOnly Property TestProp As Integer + Get + Return 0 + End Get + End Property +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveEventToExistingType() As Task + Dim initialSourceMarkup = " +Imports System + +Public Class Class1 + Public Shared Event Test[||]Event As EventHandler +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestEvent") + Dim fixedSourceMarkup = " +Imports System + +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = "Imports System + +Public Class Class1Helpers + Public Shared Event TestEvent As EventHandler +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingType() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Function Test[||]Func() As Integer + Return 0 + End Function +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared Function TestFunc() As Integer + Return 0 + End Function +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveSubToExistingType() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Sub Test[||]Sub() + Return + End Sub +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestSub") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared Sub TestSub() + Return + End Sub +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveConstToExistingType() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Const Test[||]Field As Integer = 0 +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestField") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Const TestField As Integer = 0 +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveExtensionFunctionToExistingType() As Task + Dim initialSourceMarkup = " +Imports System.Runtime.CompilerServices + +Public Module Class1 + + Public Function Test[||]Func(other As Other) As Integer + Return other.OtherInt + 2 + End Function +End Module + +Public Class Class2 + Public Function GetOtherInt() As Integer + Dim other = New Other() + Return other.TestFunc() + End Function +End Class + +Public Class Other + Public OtherInt As Integer + + Public Sub New() + OtherInt = 5 + End Sub +End Class" + Dim initialDestinationMarkup = " +Public Module Class1Helpers +End Module" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Imports System.Runtime.CompilerServices + +Public Module Class1 +End Module + +Public Class Class2 + Public Function GetOtherInt() As Integer + Dim other = New Other() + Return other.TestFunc() + End Function +End Class + +Public Class Other + Public OtherInt As Integer + + Public Sub New() + OtherInt = 5 + End Sub +End Class" + Dim fixedDestinationMarkup = "Imports System.Runtime.CompilerServices + +Public Module Class1Helpers + + Public Function Test[||]Func(other As Other) As Integer + Return other.OtherInt + 2 + End Function +End Module" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingTypeWithNamespace() As Task + Dim initialSourceMarkup = " +Namespace TestNs + Public Class Class1 + Public Shared Function Test[||]Func() As Integer + Return 0 + End Function + End Class +End Namespace" + Dim initialDestinationMarkup = " +Namespace TestNs + Public Class Class1Helpers + End Class +End Namespace" + Dim newTypeName = "TestNs.Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Namespace TestNs + Public Class Class1 + End Class +End Namespace" + Dim fixedDestinationMarkup = " +Namespace TestNs + Public Class Class1Helpers + Public Shared Function TestFunc() As Integer + Return 0 + End Function + End Class +End Namespace" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingTypeWithNewNamespace() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Function Test[||]Func() As Integer + Return 0 + End Function +End Class" + Dim initialDestinationMarkup = " +Namespace TestNs + Public Class Class1Helpers + End Class +End Namespace" + Dim newTypeName = "TestNs.Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Namespace TestNs + Public Class Class1Helpers + Public Shared Function TestFunc() As Integer + Return 0 + End Function + End Class +End Namespace" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingTypeRefactorSourceUsage() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Function Test[||]Func() As Integer + Return 0 + End Function + + Public Shared Function TestFunc2() As Integer + Return TestFunc() + End Function +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Public Class Class1 + Public Shared Function TestFunc2() As Integer + Return Class1Helpers.TestFunc() + End Function +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared Function TestFunc() As Integer + Return 0 + End Function +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingModuleRefactorSourceUsage() As Task + Dim initialSourceMarkup = " +Public Module Class1 + Public Function Test[||]Func() As Integer + Return 0 + End Function + + Public Function TestFunc2() As Integer + Return TestFunc() + End Function +End Module" + Dim initialDestinationMarkup = " +Public Module Class1Helpers +End Module" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Public Module Class1 + Public Function TestFunc2() As Integer + Return TestFunc() + End Function +End Module" + Dim fixedDestinationMarkup = " +Public Module Class1Helpers + Public Function TestFunc() As Integer + Return 0 + End Function +End Module" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function + + + Public Async Function TestMoveFunctionToExistingTypeRefactorDestinationUsage() As Task + Dim initialSourceMarkup = " +Public Class Class1 + Public Shared Function Test[||]Func() As Integer + Return 0 + End Function +End Class" + Dim initialDestinationMarkup = " +Public Class Class1Helpers + Public Shared Function TestFunc2() As Integer + Return Class1.TestFunc() + End Function +End Class" + Dim newTypeName = "Class1Helpers" + Dim selection = ImmutableArray.Create("TestFunc") + Dim fixedSourceMarkup = " +Public Class Class1 +End Class" + Dim fixedDestinationMarkup = " +Public Class Class1Helpers + Public Shared Function TestFunc2() As Integer + Return Class1Helpers.TestFunc() + End Function + Public Shared Function TestFunc() As Integer + Return 0 + End Function +End Class" + + Await TestMovementExistingFileAsync(initialSourceMarkup, + initialDestinationMarkup, + fixedSourceMarkup, + fixedDestinationMarkup, + newTypeName, + selection).ConfigureAwait(False) + End Function +#End Region #Region "SelectionTests" @@ -2233,10 +2672,12 @@ End Namespace" Public Sub New(destinationType As String, members As ImmutableArray(Of String), - newFileName As String) + newFileName As String, + Optional newType As Boolean = True) _destinationType = destinationType _members = members _newFileName = newFileName + _newType = newType End Sub Private ReadOnly _destinationType As String @@ -2245,14 +2686,16 @@ End Namespace" Private ReadOnly _newFileName As String + Private ReadOnly _newType As Boolean + Protected Overrides Function CreateWorkspaceImpl() As Workspace Dim hostServices = s_testServices.GetHostServices() - Dim workspace = New AdhocWorkspace(hostServices) Dim optionsService = DirectCast(workspace.Services.GetRequiredService(Of IMoveStaticMembersOptionsService)(), TestMoveStaticMembersService) - optionsService.DestinationType = _destinationType + optionsService.DestinationName = _destinationType optionsService.Filename = _newFileName optionsService.SelectedMembers = _members + optionsService.CreateNew = _newType Return workspace End Function @@ -2271,7 +2714,29 @@ End Namespace" } test.FixedState.Sources.Add(expectedSource) test.FixedState.Sources.Add((newFileName, expectedNewFile)) - Await test.RunAsync() + Await test.RunAsync().ConfigureAwait(False) + End Function + + Private Shared Async Function TestMovementExistingFileAsync(initialSourceMarkup As String, + initialDestinationMarkup As String, + fixedSourceMarkup As String, + fixedDestinationMarkup As String, + destinationName As String, + selectedMembers As ImmutableArray(Of String), + Optional destinationFileName As String = Nothing) As Task + Dim test = New Test(destinationName, selectedMembers, destinationFileName, newType:=False) + test.TestState.Sources.Add(initialSourceMarkup) + test.FixedState.Sources.Add(fixedSourceMarkup) + + If destinationFileName IsNot Nothing Then + test.TestState.Sources.Add((destinationFileName, initialDestinationMarkup)) + test.FixedState.Sources.Add((destinationFileName, fixedDestinationMarkup)) + Else + test.TestState.Sources.Add(initialDestinationMarkup) + test.FixedState.Sources.Add(fixedDestinationMarkup) + End If + + Await test.RunAsync().ConfigureAwait(False) End Function Private Shared Async Function TestNoRefactoringAsync(initialMarkup As String) As Task diff --git a/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersOptions.cs b/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersOptions.cs index a9d08e971892f..6975629882700 100644 --- a/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersOptions.cs +++ b/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersOptions.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.MoveStaticMembers { @@ -13,9 +14,16 @@ internal readonly struct MoveStaticMembersOptions public string FileName { get; } - public string TypeName { get; } + public bool IsNewType { get; } - public string NamespaceDisplay { get; } + // only has value when IsNewType is false + public INamedTypeSymbol? Destination { get; } + + // only has value when IsNewType is true + public string? TypeName { get; } + + // only has value when IsNewType is true + public string? NamespaceDisplay { get; } public ImmutableArray SelectedMembers { get; } @@ -25,6 +33,23 @@ internal readonly struct MoveStaticMembersOptions ImmutableArray.Empty, isCancelled: true); + public MoveStaticMembersOptions( + INamedTypeSymbol destination, + ImmutableArray selectedMembers, + bool isCancelled = false) + { + var sourceLocation = destination.DeclaringSyntaxReferences.First(); + RoslynDebug.AssertNotNull(sourceLocation.SyntaxTree); + + IsCancelled = isCancelled; + FileName = sourceLocation.SyntaxTree.FilePath; + IsNewType = false; + Destination = destination; + TypeName = null; + NamespaceDisplay = null; + SelectedMembers = selectedMembers; + } + public MoveStaticMembersOptions( string fileName, string fullTypeName, @@ -33,6 +58,8 @@ public MoveStaticMembersOptions( { IsCancelled = isCancelled; FileName = fileName; + IsNewType = true; + Destination = null; var namespacesAndType = fullTypeName.Split(separator: '.'); TypeName = namespacesAndType.Last(); NamespaceDisplay = string.Join(separator: ".", namespacesAndType.Take(namespacesAndType.Length - 1)); diff --git a/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersWithDialogCodeAction.cs b/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersWithDialogCodeAction.cs index f3dc5a851c09e..0c27b1b51b98c 100644 --- a/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersWithDialogCodeAction.cs +++ b/src/Features/Core/Portable/MoveStaticMembers/MoveStaticMembersWithDialogCodeAction.cs @@ -75,6 +75,26 @@ protected override async Task> ComputeOperation root = root.TrackNodes(memberNodes); var sourceDoc = _document.WithSyntaxRoot(root); + if (!moveOptions.IsNewType) + { + // we already have our destination type, but we need to find the document it is in + // When it is an existing type, "FileName" points to a full path rather than just the name + // There should be no two docs that have the same file path + var destinationDocId = _document.Project.Solution.GetDocumentIdsWithFilePath(moveOptions.FileName).Single(); + var fixedSolution = await RefactorAndMoveAsync( + moveOptions.SelectedMembers, + memberNodes, + sourceDoc.Project.Solution, + moveOptions.Destination!, + // TODO: Find a way to merge/change generic type args for classes, or change PullMembersUp to handle instead + typeArgIndices: ImmutableArray.Empty, + sourceDoc.Id, + destinationDocId, + cancellationToken).ConfigureAwait(false); + return new CodeActionOperation[] { new ApplyChangesOperation(fixedSolution) }; + } + + // otherwise, we need to create a destination ourselves var typeParameters = ExtractTypeHelpers.GetRequiredTypeParametersForMembers(_selectedType, moveOptions.SelectedMembers); // which indices of the old type params should we keep for a new class reference, used for refactoring usages var typeArgIndices = Enumerable.Range(0, _selectedType.TypeParameters.Length) @@ -87,7 +107,7 @@ protected override async Task> ComputeOperation Accessibility.NotApplicable, DeclarationModifiers.Static, GetNewTypeKind(_selectedType), - moveOptions.TypeName, + moveOptions.TypeName!, typeParameters: typeParameters); var (newDoc, annotation) = await ExtractTypeHelpers.AddTypeToNewFileAsync( @@ -137,6 +157,60 @@ private static TypeKind GetNewTypeKind(INamedTypeSymbol oldType) return oldType.TypeKind; } + /// + /// Finds references, refactors them, then moves the selected members to the destination. + /// Used when the destination type/file already exists. + /// + /// selected member symbols + /// nodes corresponding to those symbols in the old solution, should have been annotated + /// solution without any members moved/refactored + /// the type to move to, should be inserted into a document already + /// generic type arg indices to keep when refactoring generic class access to the new type. Empty if not relevant + /// Id of the document where the mebers are being moved from + /// The solution with references refactored and members moved to the newType + private async Task RefactorAndMoveAsync( + ImmutableArray selectedMembers, + ImmutableArray oldMemberNodes, + Solution oldSolution, + INamedTypeSymbol newType, + ImmutableArray typeArgIndices, + DocumentId sourceDocId, + DocumentId newTypeDocId, + CancellationToken cancellationToken) + { + // annotate our new type, in case our refactoring changes it + var newTypeDoc = await oldSolution.GetRequiredDocumentAsync(newTypeDocId, cancellationToken: cancellationToken).ConfigureAwait(false); + var newTypeRoot = await newTypeDoc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newTypeNode = newType.DeclaringSyntaxReferences + .SelectAsArray(sRef => sRef.GetSyntax(cancellationToken)) + .First(node => newTypeRoot.Contains(node)); + newTypeRoot = newTypeRoot.TrackNodes(newTypeNode); + oldSolution = newTypeDoc.WithSyntaxRoot(newTypeRoot).Project.Solution; + + // refactor references across the entire solution + var memberReferenceLocations = await FindMemberReferencesAsync(selectedMembers, oldSolution, cancellationToken).ConfigureAwait(false); + var projectToLocations = memberReferenceLocations.ToLookup(loc => loc.location.Document.Project.Id); + var solutionWithFixedReferences = await RefactorReferencesAsync(projectToLocations, oldSolution, newType, typeArgIndices, cancellationToken).ConfigureAwait(false); + + var sourceDoc = solutionWithFixedReferences.GetRequiredDocument(sourceDocId); + + // get back tracked nodes from our changes + var root = await sourceDoc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await sourceDoc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var members = oldMemberNodes + .Select(node => root.GetCurrentNode(node)) + .WhereNotNull() + .SelectAsArray(node => (semanticModel.GetDeclaredSymbol(node, cancellationToken), false)); + + newTypeDoc = solutionWithFixedReferences.GetRequiredDocument(newTypeDoc.Id); + newTypeRoot = await newTypeDoc.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var newTypeSemanticModel = await newTypeDoc.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + newType = (INamedTypeSymbol)newTypeSemanticModel.GetRequiredDeclaredSymbol(newTypeRoot.GetCurrentNode(newTypeNode)!, cancellationToken); + + var pullMembersUpOptions = PullMembersUpOptionsBuilder.BuildPullMembersUpOptions(newType, members); + return await MembersPuller.PullMembersUpAsync(sourceDoc, pullMembersUpOptions, _fallbackOptions, cancellationToken).ConfigureAwait(false); + } + private static async Task RefactorReferencesAsync( ILookup projectToLocations, Solution solution, diff --git a/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialog.xaml b/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialog.xaml index f9fc94bc8df13..d656667ae1413 100644 --- a/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialog.xaml +++ b/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialog.xaml @@ -5,7 +5,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:sys="clr-namespace:System;assembly=mscorlib" - xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging" + xmlns:imaging="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.Imaging" + xmlns:imagecatalog="clr-namespace:Microsoft.VisualStudio.Imaging;assembly=Microsoft.VisualStudio.ImageCatalog" xmlns:movestaticmembers="clr-namespace:Microsoft.VisualStudio.LanguageServices.Implementation.MoveStaticMembers" d:DataContext="{d:DesignInstance Type=movestaticmembers:MoveStaticMembersDialogViewModel}" x:Uid="MoveStaticMembersDialog" @@ -14,7 +15,7 @@ x:ClassModifier="internal" mc:Ignorable="d" WindowStartupLocation="CenterOwner" - Height="398" + Height="498" Width="500" MinHeight="298" MinWidth="210" @@ -36,22 +37,62 @@ - - - - - + \ No newline at end of file diff --git a/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialogViewModel.cs b/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialogViewModel.cs index f02e161288486..3cd86f203ecc1 100644 --- a/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialogViewModel.cs +++ b/src/VisualStudio/Core/Def/MoveStaticMembers/MoveStaticMembersDialogViewModel.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Immutable; using System.ComponentModel; +using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Imaging.Interop; @@ -18,19 +20,18 @@ internal class MoveStaticMembersDialogViewModel : AbstractNotifyPropertyChanged private readonly ISyntaxFacts _syntaxFacts; - private readonly ImmutableArray _existingNames; - public MoveStaticMembersDialogViewModel( StaticMemberSelectionViewModel memberSelectionViewModel, string defaultType, - ImmutableArray existingNames, + ImmutableArray availableTypes, string prependedNamespace, ISyntaxFacts syntaxFacts) { MemberSelectionViewModel = memberSelectionViewModel; _syntaxFacts = syntaxFacts ?? throw new ArgumentNullException(nameof(syntaxFacts)); - _destinationName = defaultType; - _existingNames = existingNames; + _searchText = defaultType; + _destinationName = new TypeNameItem(defaultType); + AvailableTypes = availableTypes; PrependedNamespace = string.IsNullOrEmpty(prependedNamespace) ? prependedNamespace : prependedNamespace + "."; PropertyChanged += MoveMembersToTypeDialogViewModel_PropertyChanged; @@ -44,15 +45,35 @@ private void MoveMembersToTypeDialogViewModel_PropertyChanged(object sender, Pro case nameof(DestinationName): OnDestinationUpdated(); break; + + case nameof(SearchText): + OnSearchTextUpdated(); + break; } } + private void OnSearchTextUpdated() + { + var foundItem = AvailableTypes.FirstOrDefault(t => t.TypeName == SearchText); + if (foundItem is null) + { + DestinationName = new(PrependedNamespace + SearchText); + return; + } + + DestinationName = foundItem; + } + public void OnDestinationUpdated() { - // TODO change once we allow movement to existing types - var fullyQualifiedTypeName = PrependedNamespace + DestinationName; - var isNewType = !_existingNames.Contains(fullyQualifiedTypeName); - CanSubmit = isNewType && IsValidType(fullyQualifiedTypeName); + if (!_destinationName.IsNew) + { + CanSubmit = true; + ShowMessage = false; + return; + } + + CanSubmit = IsValidType(_destinationName.TypeName); if (CanSubmit) { @@ -89,12 +110,13 @@ private bool IsValidType(string typeName) } public string PrependedNamespace { get; } + public ImmutableArray AvailableTypes { get; } - private string _destinationName; - public string DestinationName + private TypeNameItem _destinationName; + public TypeNameItem DestinationName { get => _destinationName; - set => SetProperty(ref _destinationName, value); + private set => SetProperty(ref _destinationName, value); } private ImageMoniker _icon; @@ -124,5 +146,12 @@ public bool CanSubmit get => _canSubmit; set => SetProperty(ref _canSubmit, value); } + + private string _searchText; + public string SearchText + { + get => _searchText; + set => SetProperty(ref _searchText, value); + } } } diff --git a/src/VisualStudio/Core/Def/MoveStaticMembers/TypeNameItem.cs b/src/VisualStudio/Core/Def/MoveStaticMembers/TypeNameItem.cs new file mode 100644 index 0000000000000..664617257d26a --- /dev/null +++ b/src/VisualStudio/Core/Def/MoveStaticMembers/TypeNameItem.cs @@ -0,0 +1,67 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Roslyn.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.MoveStaticMembers +{ + internal class TypeNameItem + { + public string TypeName { get; } + public INamedTypeSymbol? NamedType { get; } + public string DeclarationFilePath { get; } + public string DeclarationFileName { get; } + public bool IsFromHistory { get; } + public bool IsNew { get; } + + public TypeNameItem(bool isFromHistory, string declarationFile, INamedTypeSymbol type) + { + IsFromHistory = isFromHistory; + IsNew = false; + NamedType = type; + TypeName = type.ToDisplayString(); + DeclarationFileName = PathUtilities.GetFileName(declarationFile); + DeclarationFilePath = declarationFile; + } + + public TypeNameItem(string @typeName) + { + IsFromHistory = false; + IsNew = true; + TypeName = @typeName; + NamedType = null; + DeclarationFileName = string.Empty; + DeclarationFilePath = string.Empty; + } + + public override string ToString() => TypeName; + + public static int CompareTo(TypeNameItem x, TypeNameItem y) + { + // sort so that history is first, then type name, then file name + if (x.IsFromHistory ^ y.IsFromHistory) + { + // one is from history and the other isn't + return x.IsFromHistory ? -1 : 1; + } + // compare by each namespace/finally type + var xnames = x.TypeName.Split('.'); + var ynames = y.TypeName.Split('.'); + + for (var i = 0; i < Math.Min(xnames.Length, ynames.Length); i++) + { + var comp = xnames[i].CompareTo(ynames[i]); + if (comp != 0) + { + return comp; + } + } + + return x.DeclarationFileName.CompareTo(y.DeclarationFileName); + } + } +} diff --git a/src/VisualStudio/Core/Def/MoveStaticMembers/VisualStudioMoveStaticMembersOptionsService.cs b/src/VisualStudio/Core/Def/MoveStaticMembers/VisualStudioMoveStaticMembersOptionsService.cs index 4013ff86f1364..88ca05381b3fb 100644 --- a/src/VisualStudio/Core/Def/MoveStaticMembers/VisualStudioMoveStaticMembersOptionsService.cs +++ b/src/VisualStudio/Core/Def/MoveStaticMembers/VisualStudioMoveStaticMembersOptionsService.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Composition; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -28,6 +31,10 @@ internal class VisualStudioMoveStaticMembersOptionsService : IMoveStaticMembersO private readonly IGlyphService _glyphService; private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; + private const int HistorySize = 3; + + public readonly LinkedList History = new(); + [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public VisualStudioMoveStaticMembersOptionsService( @@ -40,13 +47,19 @@ public VisualStudioMoveStaticMembersOptionsService( public MoveStaticMembersOptions GetMoveMembersToTypeOptions(Document document, INamedTypeSymbol selectedType, ISymbol? selectedNodeSymbol) { - var viewModel = GetViewModel(document, selectedType, selectedNodeSymbol, _glyphService, _uiThreadOperationExecutor); + var viewModel = GetViewModel(document, selectedType, selectedNodeSymbol, History, _glyphService, _uiThreadOperationExecutor); var dialog = new MoveStaticMembersDialog(viewModel); var result = dialog.ShowModal(); - return GenerateOptions(document.Project.Language, viewModel, result.GetValueOrDefault()); + if (result.GetValueOrDefault()) + { + UpdateHistory(viewModel); + return GenerateOptions(document.Project.Language, viewModel, result.GetValueOrDefault()); + } + + return MoveStaticMembersOptions.Cancelled; } // internal for testing purposes @@ -55,12 +68,23 @@ internal static MoveStaticMembersOptions GenerateOptions(string language, MoveSt if (dialogResult) { // if the destination name contains extra namespaces, we want the last one as that is the real type name - var typeName = viewModel.DestinationName.Split('.').Last(); + var typeName = viewModel.DestinationName.TypeName.Split('.').Last(); var newFileName = Path.ChangeExtension(typeName, language == LanguageNames.CSharp ? ".cs" : ".vb"); + var selectedMembers = viewModel.MemberSelectionViewModel.CheckedMembers.SelectAsArray(vm => vm.Symbol); + + if (viewModel.DestinationName.IsNew) + { + return new MoveStaticMembersOptions( + newFileName, + viewModel.DestinationName.TypeName, + selectedMembers); + } + + RoslynDebug.AssertNotNull(viewModel.DestinationName.NamedType); + return new MoveStaticMembersOptions( - newFileName, - viewModel.PrependedNamespace + viewModel.DestinationName, - viewModel.MemberSelectionViewModel.CheckedMembers.SelectAsArray(vm => vm.Symbol)); + viewModel.DestinationName.NamedType, + selectedMembers); } return MoveStaticMembersOptions.Cancelled; @@ -71,6 +95,7 @@ internal static MoveStaticMembersDialogViewModel GetViewModel( Document document, INamedTypeSymbol selectedType, ISymbol? selectedNodeSymbol, + LinkedList history, IGlyphService? glyphService, IUIThreadOperationExecutor uiThreadOperationExecutor) { @@ -88,7 +113,8 @@ internal static MoveStaticMembersDialogViewModel GetViewModel( using var cancellationTokenSource = new CancellationTokenSource(); var memberToDependentsMap = SymbolDependentsBuilder.FindMemberToDependentsMap(membersInType, document.Project, cancellationTokenSource.Token); - var existingTypeNames = selectedType.ContainingNamespace.GetAllTypes(cancellationTokenSource.Token).SelectAsArray(t => t.ToDisplayString()); + var existingTypes = selectedType.ContainingNamespace.GetAllTypes(cancellationTokenSource.Token).ToImmutableArray(); + var existingTypeNames = existingTypes.SelectAsArray(t => t.ToDisplayString()); var candidateName = selectedType.Name + "Helpers"; var defaultTypeName = NameGenerator.GenerateUniqueName(candidateName, name => !existingTypeNames.Contains(name)); @@ -101,11 +127,67 @@ internal static MoveStaticMembersDialogViewModel GetViewModel( memberViewModels, memberToDependentsMap); - return new MoveStaticMembersDialogViewModel(selectMembersViewModel, + var availableTypeNames = MakeTypeNameItems( + selectedType.ContainingNamespace, + selectedType, + document, + history, + cancellationTokenSource.Token); + + return new MoveStaticMembersDialogViewModel( + selectMembersViewModel, defaultTypeName, - existingTypeNames, + availableTypeNames, containingNamespaceDisplay, document.GetRequiredLanguageService()); } + + private void UpdateHistory(MoveStaticMembersDialogViewModel viewModel) + { + if (viewModel.DestinationName.IsNew || viewModel.DestinationName.IsFromHistory) + { + // if we create a new destination or already have it in the history, + // we don't need to update our history list. + return; + } + + History.AddFirst(viewModel.DestinationName.NamedType!); + if (History.Count > HistorySize) + { + History.RemoveLast(); + } + } + + private static string GetFile(Location loc) => loc.SourceTree!.FilePath; + + /// + /// Construct all the type names declared in the project, + /// + private static ImmutableArray MakeTypeNameItems( + INamespaceSymbol currentNamespace, + INamedTypeSymbol currentType, + Document currentDocument, + LinkedList history, + CancellationToken cancellationToken) + { + return currentNamespace.GetAllTypes(cancellationToken) + // only take symbols that are the same kind of type (class, module) + // and remove non-static types only when the current type is static + .Where(t => t.TypeKind == currentType.TypeKind && (t.IsStaticType() || !currentType.IsStaticType())) + .SelectMany(t => + { + // for partially declared classes, we may want multiple entries for a single type. + // filter to those actually in a real file, and that is not our current location. + return t.Locations + .Where(l => l.IsInSource && + (currentType.Name != t.Name || GetFile(l) != currentDocument.FilePath)) + .Select(l => new TypeNameItem( + history.Contains(t), + GetFile(l), + t)); + }) + .ToImmutableArrayOrEmpty() + .Sort(comparison: TypeNameItem.CompareTo); + } } } diff --git a/src/VisualStudio/Core/Test/MoveStaticMembers/MoveStaticMembersViewModelTest.vb b/src/VisualStudio/Core/Test/MoveStaticMembers/MoveStaticMembersViewModelTest.vb index 779cb600722f4..11a6f240a1b3c 100644 --- a/src/VisualStudio/Core/Test/MoveStaticMembers/MoveStaticMembersViewModelTest.vb +++ b/src/VisualStudio/Core/Test/MoveStaticMembers/MoveStaticMembersViewModelTest.vb @@ -7,6 +7,7 @@ Imports System.Threading Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.LanguageServices +Imports Microsoft.CodeAnalysis.MoveStaticMembers Imports Microsoft.CodeAnalysis.PullMemberUp Imports Microsoft.CodeAnalysis.Shared.Extensions Imports Microsoft.CodeAnalysis.Test.Utilities @@ -35,11 +36,19 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers workspaceDoc, memberSymbol.ContainingType, memberSymbol, + New LinkedList(Of INamedTypeSymbol), Nothing, workspace.GetService(Of IUIThreadOperationExecutor)) End Using End Function + Private Shared Function Submit(viewModel As MoveStaticMembersDialogViewModel, cSharp As Boolean) As MoveStaticMembersOptions + Assert.True(viewModel.CanSubmit) + Dim language = If(cSharp, LanguageNames.CSharp, LanguageNames.VisualBasic) + + Return VisualStudioMoveStaticMembersOptionsService.GenerateOptions(language, viewModel, True) + End Function + Private Shared Function FindMemberByName(name As String, memberArray As ImmutableArray(Of SymbolViewModel(Of ISymbol))) As SymbolViewModel(Of ISymbol) Dim member = memberArray.FirstOrDefault(Function(memberViewModel) memberViewModel.Symbol.Name.Equals(name)) Assert.NotNull(member) @@ -84,9 +93,9 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers ' We can call the method, but we need the document still to test submission Dim viewModel = Await GetViewModelAsync(markUp).ConfigureAwait(False) - Assert.Equal("TestClassHelpers", viewModel.DestinationName) - viewModel.DestinationName = "ExtraNs.TestClassHelpers" - Assert.Equal("ExtraNs.TestClassHelpers", viewModel.DestinationName) + Assert.Equal("TestClassHelpers", viewModel.DestinationName.TypeName) + SetSearchText(viewModel, "ExtraNs.TestClassHelpers") + Assert.Equal("TestNs.ExtraNs.TestClassHelpers", viewModel.DestinationName.TypeName) Assert.Equal("TestNs.", viewModel.PrependedNamespace) Assert.True(viewModel.CanSubmit) @@ -101,7 +110,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers End Function - Public Async Function CSTestNameConflicts() As Task + Public Async Function CSTestInvalidNames() As Task Dim markUp = @@ -168,7 +177,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers ]]> Dim viewModel = Await GetViewModelAsync(markUp) - Assert.Equal(viewModel.DestinationName, "TestClassHelpers") + Assert.Equal(viewModel.DestinationName.TypeName, "TestClassHelpers") Assert.Equal("TestNs.", viewModel.PrependedNamespace) Assert.True(viewModel.ShowMessage) @@ -177,59 +186,43 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers Assert.False(viewModel.MemberSelectionViewModel.CheckedMembers.IsEmpty) Assert.True(viewModel.CanSubmit) - viewModel.DestinationName = "ConflictingClassName" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) - - viewModel.DestinationName = "ValidName" + SetSearchText(viewModel, "ValidName") Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) ' spaces are not allowed as types - viewModel.DestinationName = "asd " + SetSearchText(viewModel, "asd ") Assert.False(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) ' different project - viewModel.DestinationName = "ConflictingClassName3" + SetSearchText(viewModel, "ConflictingClassName3") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - viewModel.DestinationName = "ITestInterface" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) - - viewModel.DestinationName = "NoNsClass" + SetSearchText(viewModel, "NoNsClass") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) ' different namespace - viewModel.DestinationName = "ConflictingClassName2" + SetSearchText(viewModel, "ConflictingClassName2") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - viewModel.DestinationName = "TestClass" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) - - viewModel.DestinationName = "ExtraNamespace.ValidName" + SetSearchText(viewModel, "ExtraNamespace.ValidName") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - - viewModel.DestinationName = "ExtraNs.ConflictingNsClassName" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) End Function + Private Shared Sub SetSearchText(viewModel As MoveStaticMembersDialogViewModel, destinationName As String) + viewModel.SearchText = destinationName + End Sub + Public Async Function CSTestMemberSelection() As Task Dim markUp = + Public Async Function CSTestTypeSelection() As Task + Dim markUp = + + + namespace TestNs + { + public static class TestClass + { + public static int Bar$$bar() + { + return 12345; + } + } + } + + + public class NoNsClass + { + } + + + namespace TestNs + { + public interface ITestInterface + { + } + } + + + namespace TestNs + { + public static class ConflictingClassName + { + } + } + + + namespace TestNs2 + { + public class ConflictingClassName2 + { + } + } + + + namespace TestNs.ExtraNs + { + public static class ConflictingNsClassName + { + } + } + + + namespace TestNs + { + public class NonStaticConflictingName + { + } + } + + + + + namespace TestNs + { + public class ConflictingClassName3 + { + } + } + + +]]> + Dim viewModel = Await GetViewModelAsync(markUp) + + Assert.Equal(viewModel.SearchText, "TestClassHelpers") + + Assert.True(viewModel.ShowMessage) + Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) + + Assert.False(viewModel.MemberSelectionViewModel.CheckedMembers.IsEmpty) + Assert.True(viewModel.CanSubmit) + + ' there should only be 2 available types that are + ' a) the same kind + ' b) static (if the current type is static) + ' b) in the same or nested namespace + ' c) in the same project + Assert.Equal(2, viewModel.AvailableTypes.Length) + Assert.Equal(1, viewModel.MemberSelectionViewModel.CheckedMembers.Length) + + viewModel.SearchText = viewModel.AvailableTypes.ElementAt(1).TypeName + Assert.Equal("TestNs.ExtraNs.ConflictingNsClassName", viewModel.DestinationName.TypeName) + Assert.NotNull(viewModel.DestinationName.NamedType) + Assert.False(viewModel.DestinationName.IsNew) + Assert.False(viewModel.ShowMessage) + Assert.True(viewModel.CanSubmit) + + viewModel.SearchText = viewModel.AvailableTypes.ElementAt(0).TypeName + Assert.Equal("TestNs.ConflictingClassName", viewModel.DestinationName.TypeName) + Assert.NotNull(viewModel.DestinationName.NamedType) + Assert.False(viewModel.DestinationName.IsNew) + Assert.False(viewModel.ShowMessage) + Assert.True(viewModel.CanSubmit) + + Dim options = Submit(viewModel, cSharp:=True) + Assert.False(options.IsNewType) + Assert.False(options.IsCancelled) + Assert.NotNull(options.Destination) + Assert.Equal("TestNs.ConflictingClassName", options.Destination.ToDisplayString()) + Assert.Equal("TestFile.cs", options.FileName) + End Function #End Region #Region "VB" @@ -341,9 +448,9 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers ' We can call the method, but we need the document still to test submission Dim viewModel = Await GetViewModelAsync(markUp).ConfigureAwait(False) - Assert.Equal("TestClassHelpers", viewModel.DestinationName) - viewModel.DestinationName = "ExtraNs.TestClassHelpers" - Assert.Equal("ExtraNs.TestClassHelpers", viewModel.DestinationName) + Assert.Equal("TestClassHelpers", viewModel.DestinationName.TypeName) + SetSearchText(viewModel, "ExtraNs.TestClassHelpers") + Assert.Equal("TestNs.ExtraNs.TestClassHelpers", viewModel.DestinationName.TypeName) Assert.Equal("TestNs.", viewModel.PrependedNamespace) Assert.True(viewModel.CanSubmit) @@ -411,7 +518,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers ]]> Dim viewModel = Await GetViewModelAsync(markUp) - Assert.Equal(viewModel.DestinationName, "TestClassHelpers") + Assert.Equal(viewModel.DestinationName.TypeName, "TestClassHelpers") Assert.Equal("TestNs.", viewModel.PrependedNamespace) Assert.True(viewModel.ShowMessage) @@ -420,57 +527,41 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers Assert.False(viewModel.MemberSelectionViewModel.CheckedMembers.IsEmpty) Assert.True(viewModel.CanSubmit) - viewModel.DestinationName = "ConflictingClassName" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) + SetSearchText(viewModel, "TestNs.ConflictingClassName") + Assert.True(viewModel.CanSubmit) + Assert.False(viewModel.ShowMessage) - viewModel.DestinationName = "ValidName" + SetSearchText(viewModel, "ValidName") Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) ' spaces are not allowed as types - viewModel.DestinationName = "asd " + SetSearchText(viewModel, "asd ") Assert.False(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) ' different project - viewModel.DestinationName = "ConflictingClassName3" + SetSearchText(viewModel, "ConflictingClassName3") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - viewModel.DestinationName = "ITestInterface" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) - - viewModel.DestinationName = "NoNsClass" + SetSearchText(viewModel, "NoNsClass") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) ' different namespace - viewModel.DestinationName = "ConflictingClassName2" + SetSearchText(viewModel, "ConflictingClassName2") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - viewModel.DestinationName = "TestClass" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) - - viewModel.DestinationName = "ExtraNamespace.ValidName" + SetSearchText(viewModel, "ExtraNamespace.ValidName") Assert.True(viewModel.CanSubmit) Assert.True(viewModel.ShowMessage) Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) - - viewModel.DestinationName = "ExtraNs.ConflictingNsClassName" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) End Function @@ -499,7 +590,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers Dim viewModel = Await GetViewModelAsync(markUp) - Assert.Equal(viewModel.DestinationName, "TestClassHelpers") + Assert.Equal(viewModel.DestinationName.TypeName, "TestClassHelpers") Assert.Equal("RootNs.TestNs.", viewModel.PrependedNamespace) Assert.True(viewModel.ShowMessage) @@ -508,10 +599,9 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers Assert.False(viewModel.MemberSelectionViewModel.CheckedMembers.IsEmpty) Assert.True(viewModel.CanSubmit) - viewModel.DestinationName = "ConflictingClassName" - Assert.False(viewModel.CanSubmit) - Assert.True(viewModel.ShowMessage) - Assert.Equal(ServicesVSResources.Invalid_type_name, viewModel.Message) + SetSearchText(viewModel, "RootNs.TestNs.ConflictingClassName") + Assert.True(viewModel.CanSubmit) + Assert.False(viewModel.ShowMessage) End Function @@ -595,6 +685,97 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.MoveStaticMembers Assert.True(FindMemberByName("Barbar", selectionVm.Members).IsChecked) Assert.Equal(2, selectionVm.CheckedMembers.Length) End Function + + + Public Async Function VBTestTypeSelection() As Task + Dim markUp = + + + Namespace TestNs + Public Class TestClass + Public Shared Function Bar$$bar() As Integer + Return 12345; + End Function + End Class + End Namespace + + + Public Class NoNsClass + End Class + + + Namespace TestNs + Public Interface ITestInterface + End Interface + End Namespace + + + Namespace TestNs + Public Class ConflictingClassName + End Class + End Namespace + + + Namespace TestNs2 + Public Class ConflictingClassName2 + End Class + End Namespace + + + Namespace TestNs.ExtraNs + Public Class ConflictingNsClassName + End Class + End Namespace + + + + + Namespace TestNs + Public Class ConflictingClassName3 + End Class + End Namespace + + +]]> + Dim viewModel = Await GetViewModelAsync(markUp) + + Assert.Equal(viewModel.SearchText, "TestClassHelpers") + + Assert.True(viewModel.ShowMessage) + Assert.Equal(ServicesVSResources.New_Type_Name_colon, viewModel.Message) + + Assert.False(viewModel.MemberSelectionViewModel.CheckedMembers.IsEmpty) + Assert.True(viewModel.CanSubmit) + + ' there should only be 2 available types that are + ' a) the same kind + ' b) in the same or nested namespace + ' c) in the same project + Assert.Equal(2, viewModel.AvailableTypes.Length) + Assert.Equal(1, viewModel.MemberSelectionViewModel.CheckedMembers.Length) + + viewModel.SearchText = viewModel.AvailableTypes.ElementAt(1).TypeName + Assert.Equal("TestNs.ExtraNs.ConflictingNsClassName", viewModel.DestinationName.TypeName) + Assert.NotNull(viewModel.DestinationName.NamedType) + Assert.False(viewModel.DestinationName.IsNew) + Assert.False(viewModel.ShowMessage) + Assert.True(viewModel.CanSubmit) + + viewModel.SearchText = viewModel.AvailableTypes.ElementAt(0).TypeName + Assert.Equal("TestNs.ConflictingClassName", viewModel.DestinationName.TypeName) + Assert.NotNull(viewModel.DestinationName.NamedType) + Assert.False(viewModel.DestinationName.IsNew) + Assert.False(viewModel.ShowMessage) + Assert.True(viewModel.CanSubmit) + + Dim options = Submit(viewModel, cSharp:=False) + Assert.False(options.IsNewType) + Assert.False(options.IsCancelled) + Assert.NotNull(options.Destination) + Assert.Equal("TestNs.ConflictingClassName", options.Destination.ToDisplayString()) + Assert.Equal("TestFile.vb", options.FileName) + End Function #End Region End Class End Namespace diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index b2e93ed2e3084..5912d3d8e25b3 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -541,6 +541,9 @@ public bool LooksGeneric(SyntaxNode simpleName) => simpleName.IsKind(SyntaxKind.GenericName) || simpleName.GetLastToken().GetNextToken().Kind() == SyntaxKind.LessThanToken; + public SeparatedSyntaxList GetTypeArgumentsOfGenericName(SyntaxNode? genericName) + => (genericName as GenericNameSyntax)?.TypeArgumentList.Arguments ?? default; + public SyntaxNode? GetTargetOfMemberBinding(SyntaxNode? node) => (node as MemberBindingExpressionSyntax).GetParentConditionalAccessExpression()?.Expression; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index ab34c9da476d9..ce7fdfafc3109 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -318,6 +318,8 @@ void GetPartsOfTupleExpression(SyntaxNode node, void GetNameAndArityOfSimpleName(SyntaxNode node, out string name, out int arity); bool LooksGeneric(SyntaxNode simpleName); + SeparatedSyntaxList GetTypeArgumentsOfGenericName(SyntaxNode? genericName); + SyntaxList GetContentsOfInterpolatedString(SyntaxNode interpolatedString); SeparatedSyntaxList GetArgumentsOfObjectCreationExpression(SyntaxNode node); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 931c63b134cce..7a0c656d0722f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -527,6 +527,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServices Return name.IsKind(SyntaxKind.GenericName) End Function + Public Function GetTypeArgumentsOfGenericName(genericName As SyntaxNode) As SeparatedSyntaxList(Of SyntaxNode) Implements ISyntaxFacts.GetTypeArgumentsOfGenericName + Dim castGenericName = TryCast(genericName, GenericNameSyntax) + If castGenericName IsNot Nothing Then + Return castGenericName.TypeArgumentList.Arguments + End If + Return Nothing + End Function + Public Function GetExpressionOfMemberAccessExpression(node As SyntaxNode, Optional allowImplicitTarget As Boolean = False) As SyntaxNode Implements ISyntaxFacts.GetExpressionOfMemberAccessExpression Return TryCast(node, MemberAccessExpressionSyntax)?.GetExpressionOfMemberAccessExpression(allowImplicitTarget) End Function