diff --git a/build/Packages.props b/build/Packages.props index 3d0cd11973..029f591b78 100644 --- a/build/Packages.props +++ b/build/Packages.props @@ -76,6 +76,8 @@ + + diff --git a/src/OmniSharp.Host/WorkspaceInitializer.cs b/src/OmniSharp.Host/WorkspaceInitializer.cs index c559d185c5..146b39ffdc 100644 --- a/src/OmniSharp.Host/WorkspaceInitializer.cs +++ b/src/OmniSharp.Host/WorkspaceInitializer.cs @@ -28,7 +28,7 @@ public static void Initialize(IServiceProvider serviceProvider, CompositionHost var projectEventForwarder = compositionHost.GetExport(); projectEventForwarder.Initialize(); var projectSystems = compositionHost.GetExports(); - + foreach (var projectSystem in projectSystems) { try diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs index 4aadcadfd0..5f85ccd68b 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs @@ -90,13 +90,21 @@ protected async Task> GetAvailableCodeActions(I var distinctActions = codeActions.GroupBy(x => x.Title).Select(x => x.First()); - // Be sure to filter out any code actions that inherit from CodeActionWithOptions. - // This isn't a great solution and might need changing later, but every Roslyn code action - // derived from this type tries to display a dialog. For now, this is a reasonable solution. - var availableActions = ConvertToAvailableCodeAction(distinctActions) - .Where(a => !a.CodeAction.GetType().GetTypeInfo().IsSubclassOf(typeof(CodeActionWithOptions))); + var availableActions = ConvertToAvailableCodeAction(distinctActions); - return availableActions; + return FilterBlacklistedCodeActions(availableActions); + } + + private static IEnumerable FilterBlacklistedCodeActions(IEnumerable codeActions) + { + // Most of actions with UI works fine with defaults, however there's few exceptions: + return codeActions.Where(x => { + var actionName = x.CodeAction.GetType().Name; + + return actionName != "GenerateTypeCodeActionWithOption" && // Blacklisted because doesn't give additional value over non UI generate type (when defaults used.) + actionName != "ChangeSignatureCodeAction" && // Blacklisted because cannot be used without proper UI. + actionName != "PullMemberUpWithDialogCodeAction"; // Blacklisted because doesn't give additional value over non UI generate type (when defaults used.) + }); } private TextSpan GetTextSpan(ICodeActionRequest request, SourceText sourceText) diff --git a/src/OmniSharp.Roslyn/OmniSharp.Roslyn.csproj b/src/OmniSharp.Roslyn/OmniSharp.Roslyn.csproj index e9f171fd54..daa26b3a9b 100644 --- a/src/OmniSharp.Roslyn/OmniSharp.Roslyn.csproj +++ b/src/OmniSharp.Roslyn/OmniSharp.Roslyn.csproj @@ -20,7 +20,5 @@ - - diff --git a/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceService.cs b/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceService.cs new file mode 100644 index 0000000000..6dba8d05d9 --- /dev/null +++ b/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceService.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; + +namespace OmniSharp +{ + public class ExtractInterfaceWorkspaceService : DispatchProxy + { + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + // IExtractInterfaceOptionsService and extract interface results are internal types -> workaround with proxy. + // This service simply passes all members through as selected and doesn't try show UI. + // When roslyn exposes this interface and members -> remove this workaround. + var resultTypeInternal = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult"); + var enumType = resultTypeInternal.GetNestedTypes().Single(x => x.Name == "ExtractLocation"); + + var toSameFileEnumValue = Enum.Parse(enumType, "SameFile"); + + var interfaceName = args[3] ?? throw new InvalidOperationException($"{nameof(ExtractInterfaceWorkspaceService)} default interface name was null."); + + var resultObject = Activator.CreateInstance(resultTypeInternal, new object[] { + false, // isCancelled + ((List)args[2]).ToImmutableArray(), // InterfaceMembers selected -> select all. + interfaceName, + $"{interfaceName}.cs", + toSameFileEnumValue + }); + + return typeof(Task).GetMethod("FromResult").MakeGenericMethod(resultTypeInternal).Invoke(null, new[] { resultObject }); + } + } +} diff --git a/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceServiceFactory.cs b/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceServiceFactory.cs new file mode 100644 index 0000000000..3f6b63679a --- /dev/null +++ b/src/OmniSharp.Roslyn/WorkspaceServices/ExtractInterfaceWorkspaceServiceFactory.cs @@ -0,0 +1,19 @@ +using System.Composition; +using System.Reflection; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace OmniSharp +{ + [Shared] + [ExportWorkspaceServiceFactoryWithAssemblyQualifiedName("Microsoft.CodeAnalysis.Features", "Microsoft.CodeAnalysis.ExtractInterface.IExtractInterfaceOptionsService")] + public class ExtractInterfaceWorkspaceServiceFactory : IWorkspaceServiceFactory + { + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + // Generates proxy class to get around issue that IExtractInterfaceOptionsService is internal at this point. + var internalType = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.ExtractInterface.IExtractInterfaceOptionsService"); + return (IWorkspaceService)typeof(DispatchProxy).GetMethod(nameof(DispatchProxy.Create)).MakeGenericMethod(internalType, typeof(ExtractInterfaceWorkspaceService)).Invoke(null, null); + } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceService.cs b/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceService.cs new file mode 100644 index 0000000000..e3ce331500 --- /dev/null +++ b/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceService.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace OmniSharp +{ + public class PickMemberWorkspaceService : DispatchProxy + { + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + // IPickMember and PickMemberResults are internal types -> workaround with proxy. + // This service simply passes all members through as selected and doesn't try show UI. + // When roslyn exposes this interface and members -> remove this workaround. + var resultTypeInternal = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.PickMembers.PickMembersResult"); + return Activator.CreateInstance(resultTypeInternal, new object[] { args[1], args[2] }); + } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceServiceFactory.cs b/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceServiceFactory.cs new file mode 100644 index 0000000000..ae2ed63200 --- /dev/null +++ b/src/OmniSharp.Roslyn/WorkspaceServices/PickMemberWorkspaceServiceFactory.cs @@ -0,0 +1,19 @@ +using System.Composition; +using System.Reflection; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace OmniSharp +{ + [Shared] + [ExportWorkspaceServiceFactoryWithAssemblyQualifiedName("Microsoft.CodeAnalysis.Features", "Microsoft.CodeAnalysis.PickMembers.IPickMembersService")] + public class PickMemberWorkspaceServiceFactory : IWorkspaceServiceFactory + { + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + // Generates proxy class to get around issue that IPickMembersService is internal at this point. + var internalType = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.PickMembers.IPickMembersService"); + return (IWorkspaceService)typeof(DispatchProxy).GetMethod(nameof(DispatchProxy.Create)).MakeGenericMethod(internalType, typeof(PickMemberWorkspaceService)).Invoke(null, null); + } + } +} \ No newline at end of file diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs index 33f9766a5c..663c84ea87 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs @@ -12,10 +12,8 @@ namespace OmniSharp.Roslyn.CSharp.Tests { - public class CodeActionsV2Facts : AbstractTestFixture + public class CodeActionsV2Facts : AbstractCodeActionsTestFixture { - private readonly string BufferPath = $"{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer.cs"; - public CodeActionsV2Facts(ITestOutputHelper output) : base(output) { @@ -88,7 +86,7 @@ public class c {public c() {Guid.NewGuid();}}"; public class c {public c() {Guid.NewGuid();}}"; - var response = await RunRefactoringAsync(code, "Remove Unnecessary Usings", roslynAnalyzersEnabled: roslynAnalyzersEnabled); + var response = await RunRefactoringAsync(code, "Remove Unnecessary Usings", isAnalyzersEnabled: roslynAnalyzersEnabled); AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } @@ -184,7 +182,7 @@ private static void NewMethod() Console.Write(""should be using System;""); } }"; - var response = await RunRefactoringAsync(code, "Extract Method", roslynAnalyzersEnabled: roslynAnalyzersEnabled); + var response = await RunRefactoringAsync(code, "Extract Method", isAnalyzersEnabled: roslynAnalyzersEnabled); AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } @@ -194,7 +192,7 @@ private static void NewMethod() public async Task Can_generate_type_and_return_name_of_new_file(bool roslynAnalyzersEnabled) { using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMissingType")) - using (var host = CreateOmniSharpHost(testProject.Directory, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) + using (var host = OmniSharpTestHost.Create(testProject.Directory, testOutput: TestOutput, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) { var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); var document = host.Workspace.CurrentSolution.Projects.First().Documents.First(); @@ -235,7 +233,7 @@ internal class Z public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames_file(bool roslynAnalyzersEnabled) { using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMismatchedFileName")) - using (var host = CreateOmniSharpHost(testProject.Directory, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) + using (var host = OmniSharpTestHost.Create(testProject.Directory, testOutput: TestOutput, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) { var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); var document = host.Workspace.CurrentSolution.Projects.First().Documents.First(); @@ -263,84 +261,5 @@ public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames Assert.Equal(FileModificationType.Opened, changes[1].ModificationType); } } - - private static void AssertIgnoringIndent(string expected, string actual) - { - Assert.Equal(TrimLines(expected), TrimLines(actual), false, true, true); - } - - private static string TrimLines(string source) - { - return string.Join("\n", source.Split('\n').Select(s => s.Trim())); - } - - private async Task RunRefactoringAsync(string code, string refactoringName, bool wantsChanges = false, bool roslynAnalyzersEnabled = false) - { - var refactorings = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); - Assert.Contains(refactoringName, refactorings.Select(a => a.Name)); - - var identifier = refactorings.First(action => action.Name.Equals(refactoringName)).Identifier; - return await RunRefactoringsAsync(code, identifier, wantsChanges); - } - - private async Task> FindRefactoringNamesAsync(string code, bool roslynAnalyzersEnabled = false) - { - var codeActions = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); - - return codeActions.Select(a => a.Name); - } - - private async Task> FindRefactoringsAsync(string code, IDictionary configurationData = null) - { - var testFile = new TestFile(BufferPath, code); - - using (var host = CreateOmniSharpHost(new[] { testFile }, configurationData)) - { - var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.GetCodeActions); - - var span = testFile.Content.GetSpans().Single(); - var range = testFile.Content.GetRangeFromSpan(span); - - var request = new GetCodeActionsRequest - { - Line = range.Start.Line, - Column = range.Start.Offset, - FileName = BufferPath, - Buffer = testFile.Content.Code, - Selection = range.GetSelection(), - }; - - var response = await requestHandler.Handle(request); - - return response.CodeActions; - } - } - - private async Task RunRefactoringsAsync(string code, string identifier, bool wantsChanges = false) - { - var testFile = new TestFile(BufferPath, code); - - using (var host = CreateOmniSharpHost(testFile)) - { - var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); - - var span = testFile.Content.GetSpans().Single(); - var range = testFile.Content.GetRangeFromSpan(span); - - var request = new RunCodeActionRequest - { - Line = range.Start.Line, - Column = range.Start.Offset, - Selection = range.GetSelection(), - FileName = BufferPath, - Buffer = testFile.Content.Code, - Identifier = identifier, - WantsTextChanges = wantsChanges, - WantsAllCodeActionOperations = true - }; - - return await requestHandler.Handle(request); - } - } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs new file mode 100644 index 0000000000..43741158b7 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsWithOptionsFacts.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using OmniSharp.Models; +using OmniSharp.Models.V2; +using OmniSharp.Models.V2.CodeActions; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class CodeActionsWithOptionsFacts : AbstractCodeActionsTestFixture + { + public CodeActionsWithOptionsFacts(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public async Task Can_generate_constructor_with_default_arguments() + { + const string code = + @"public class Class1[||] + { + public string PropertyHere { get; set; } + }"; + const string expected = + @"public class Class1 + { + public Class1(string propertyHere) + { + PropertyHere = propertyHere; + } + + public string PropertyHere { get; set; } + } + "; + var response = await RunRefactoringAsync(code, "Generate constructor..."); + AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + } + + [Fact] + public async Task Can_generate_overrides() + { + const string code = + @"public class Class1[||] + { + }"; + const string expected = + @"public class Class1 + { + public override bool Equals(object obj) + { + return base.Equals(obj); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override string ToString() + { + return base.ToString(); + } + } + "; + var response = await RunRefactoringAsync(code, "Generate overrides..."); + AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + } + + [Fact] + public async Task Can_generate_equals_for_object() + { + const string code = + @"public class Class1[||] + { + public string PropertyHere { get; set; } + }"; + const string expected = + @"public class Class1 + { + public string PropertyHere { get; set; } + + public override bool Equals(object obj) + { + return obj is Class1 @class && + PropertyHere == @class.PropertyHere; + } + } + "; + var response = await RunRefactoringAsync(code, "Generate Equals(object)..."); + AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + } + + [Fact] + public async Task Can_generate_equals_and_hashcode_for_object() + { + const string code = + @"public class Class1[||] + { + public string PropertyHere { get; set; } + }"; + + var response = await RunRefactoringAsync(code, "Generate Equals and GetHashCode..."); + + // This doesn't check exact class form because different framework version implement hashcode differently. + Assert.Contains("public override int GetHashCode()", ((ModifiedFileResponse)response.Changes.First()).Buffer); + Assert.Contains("public override bool Equals(object obj)", ((ModifiedFileResponse)response.Changes.First()).Buffer); + } + + [Fact] + // How to implement this witout proper UI? + public async Task Blacklists_Change_signature() + { + const string code = + @"public class Class1 + { + public void Foo(string a[||], string b) + { + } + }"; + + var response = await FindRefactoringNamesAsync(code); + + Assert.DoesNotContain("Change signature...", response); + } + + [Fact] + // Theres generate type without options that gives ~same result with default ui. No point to bring this. + public async Task Blacklists_generate_type_with_UI() + { + const string code = + @"public class Class1: NonExistentBaseType[||] + { + }"; + + var response = await FindRefactoringNamesAsync(code); + + Assert.DoesNotContain("Generate type 'NonExistentBaseType' -> Generate new type...", response); + } + + [Fact] + // Theres generate type without options that gives ~same result with default ui. No point to bring this. + public async Task Blacklists_pull_members_up_with_UI() + { + const string code = + @" + public class Class1: BaseClass + { + public string Foo[||] { get; set; } + } + + public class BaseClass {} +"; + + var response = await FindRefactoringNamesAsync(code); + + Assert.DoesNotContain("Pull 'Foo' up -> Pull members up to base type...", response); + } + + [Fact] + public async Task Can_extract_interface() + { + const string code = + @"public class Class1[||] + { + public string PropertyHere { get; set; } + }"; + + const string expected = + @" + public interface IClass1 + { + string PropertyHere { get; set; } + } + + public class Class1 : IClass1 + { + public string PropertyHere { get; set; } + } + "; + var response = await RunRefactoringAsync(code, "Extract Interface..."); + + AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); + } + + [Fact] + public void Check_better_alternative_available_for_codeaction_with_options() + { + // This keeps record is Microsoft.CodeAnalysis.PickMembers.IPickMembersService public or not + // and should fail on case where interface is exposed. Which likely means that this is officially + // supported scenario by roslyn. + // + // In case it's exposed by roslyn team these services can be simplified. + // Steps are likely following: + // - Remove proxies + // - Replace ExportWorkspaceServiceFactoryWithAssemblyQualifiedName with Microsoft.CodeAnalysis.Host.Mef.ExportWorkspaceServiceAttribute + // - Fix proxy classes to implement IPickMembersService / IExtractInterfaceOptionsService ... instead of proxy and reflection. + // - Remove all factories using ExportWorkspaceServiceFactoryWithAssemblyQualifiedName and factory itself. + // Following issue may have additional information: https://github.com/dotnet/roslyn/issues/33277 + var pickMemberServiceType = Assembly.Load("Microsoft.CodeAnalysis.Features").GetType("Microsoft.CodeAnalysis.PickMembers.IPickMembersService"); + Assert.False(pickMemberServiceType.IsPublic); + } + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs index 532c179dc0..6c1d718978 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs @@ -250,7 +250,7 @@ public async Task WhenDiagnosticsRulesAreUpdated_ThenReAnalyzerFilesInProject() host.Workspace.WorkspaceChanged += (_, e) => { if (e.Kind == WorkspaceChangeKind.ProjectChanged) { workspaceUpdatedCheck.Set(); } }; host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRulesUpdated.ToImmutableDictionary()); - Assert.True(workspaceUpdatedCheck.WaitOne(timeout: TimeSpan.FromSeconds(3))); + Assert.True(workspaceUpdatedCheck.WaitOne(timeout: TimeSpan.FromSeconds(15))); var result = await host.RequestCodeCheckAsync("testFile_4.cs"); Assert.DoesNotContain(result.QuickFixes, f => f.Text.Contains(testAnalyzerRef.Id.ToString())); diff --git a/tests/TestUtility/AbstractCodeActionsTestFixture.cs b/tests/TestUtility/AbstractCodeActionsTestFixture.cs new file mode 100644 index 0000000000..8ea3143c2a --- /dev/null +++ b/tests/TestUtility/AbstractCodeActionsTestFixture.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OmniSharp; +using OmniSharp.Models.V2; +using OmniSharp.Models.V2.CodeActions; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using TestUtility.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace TestUtility +{ + public abstract class AbstractCodeActionsTestFixture + { + private readonly string _fileTypeExtension; + + protected ITestOutputHelper TestOutput { get; } + + private string BufferPath => $"{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer{_fileTypeExtension}"; + + protected AbstractCodeActionsTestFixture(ITestOutputHelper output, string fileTypeExtension = ".cs") + { + TestOutput = output; + + _fileTypeExtension = fileTypeExtension; + } + + protected void AssertIgnoringIndent(string expected, string actual) + { + Assert.Equal(TrimLines(expected), TrimLines(actual), false, true, true); + } + + private static string TrimLines(string source) + { + return string.Join("", source.Split('\n').Select(s => s.Trim())); + } + + protected OmniSharpTestHost CreateOmniSharpHost(TestFile[] testFiles, IEnumerable> configurationData = null) + { + var host = OmniSharpTestHost.Create(path: null, testOutput: this.TestOutput, configurationData: configurationData); + + if (testFiles.Length > 0) + { + host.AddFilesToWorkspace(testFiles); + } + + return host; + } + + protected async Task RunRefactoringAsync(string code, string refactoringName, bool wantsChanges = false, bool isAnalyzersEnabled = true) + { + var refactorings = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); + Assert.Contains(refactoringName, refactorings.Select(a => a.Name)); + + var identifier = refactorings.First(action => action.Name.Equals(refactoringName)).Identifier; + return await RunRefactoringsAsync(code, identifier, wantsChanges); + } + + protected async Task> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true) + { + var codeActions = await FindRefactoringsAsync(code, TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); + + return codeActions.Select(a => a.Name); + } + + protected async Task> FindRefactoringsAsync(string code, IDictionary configurationData = null) + { + var testFile = new TestFile(BufferPath, code); + + using (var host = CreateOmniSharpHost(new[] { testFile }, configurationData)) + { + var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.GetCodeActions); + + var span = testFile.Content.GetSpans().Single(); + var range = testFile.Content.GetRangeFromSpan(span); + + var request = new GetCodeActionsRequest + { + Line = range.Start.Line, + Column = range.Start.Offset, + FileName = BufferPath, + Buffer = testFile.Content.Code, + Selection = GetSelection(range), + }; + + var response = await requestHandler.Handle(request); + + return response.CodeActions; + } + } + + private async Task RunRefactoringsAsync(string code, string identifier, bool wantsChanges = false) + { + var testFile = new TestFile(BufferPath, code); + + using (var host = CreateOmniSharpHost(new [] { testFile })) + { + var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); + + var span = testFile.Content.GetSpans().Single(); + var range = testFile.Content.GetRangeFromSpan(span); + + var request = new RunCodeActionRequest + { + Line = range.Start.Line, + Column = range.Start.Offset, + Selection = GetSelection(range), + FileName = BufferPath, + Buffer = testFile.Content.Code, + Identifier = identifier, + WantsTextChanges = wantsChanges, + WantsAllCodeActionOperations = true + }; + + return await requestHandler.Handle(request); + } + } + + private static Range GetSelection(TextRange range) + { + if (range.IsEmpty) + { + return null; + } + + return new Range + { + Start = new Point { Line = range.Start.Line, Column = range.Start.Offset }, + End = new Point { Line = range.End.Line, Column = range.End.Offset } + }; + } + } +}