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 }
+ };
+ }
+ }
+}