Skip to content

Commit

Permalink
Merge pull request #1406 from savpek/feature/codeactions-with-options
Browse files Browse the repository at this point in the history
Codeactions with options/ui.
  • Loading branch information
filipw authored Jul 10, 2019
2 parents b343cbb + 01a5aca commit 8978783
Show file tree
Hide file tree
Showing 12 changed files with 461 additions and 96 deletions.
2 changes: 2 additions & 0 deletions build/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@

<PackageReference Update="System.Reactive" Version="4.1.2" />

<PackageReference Update="System.Reflection.DispatchProxy" Version="4.5.1" />

<PackageReference Update="xunit.runner.visualstudio" Version="$(XunitPackageVersion)" />
<PackageReference Update="xunit" Version="$(XunitPackageVersion)" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/OmniSharp.Host/WorkspaceInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static void Initialize(IServiceProvider serviceProvider, CompositionHost
var projectEventForwarder = compositionHost.GetExport<ProjectEventForwarder>();
projectEventForwarder.Initialize();
var projectSystems = compositionHost.GetExports<IProjectSystem>();

foreach (var projectSystem in projectSystems)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,21 @@ protected async Task<IEnumerable<AvailableCodeAction>> 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<AvailableCodeAction> FilterBlacklistedCodeActions(IEnumerable<AvailableCodeAction> 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)
Expand Down
2 changes: 0 additions & 2 deletions src/OmniSharp.Roslyn/OmniSharp.Roslyn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,5 @@
<PackageReference Include="Microsoft.CodeAnalysis.Common" />
<PackageReference Include="System.ComponentModel.Composition" />
<PackageReference Include="System.Reflection.DispatchProxy" />

</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<ISymbol>)args[2]).ToImmutableArray(), // InterfaceMembers selected -> select all.
interfaceName,
$"{interfaceName}.cs",
toSameFileEnumValue
});

return typeof(Task).GetMethod("FromResult").MakeGenericMethod(resultTypeInternal).Invoke(null, new[] { resultObject });
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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] });
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
91 changes: 5 additions & 86 deletions tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -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<RunCodeActionService>(OmniSharpEndpoints.V2.RunCodeAction);
var document = host.Workspace.CurrentSolution.Projects.First().Documents.First();
Expand Down Expand Up @@ -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<RunCodeActionService>(OmniSharpEndpoints.V2.RunCodeAction);
var document = host.Workspace.CurrentSolution.Projects.First().Documents.First();
Expand Down Expand Up @@ -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<RunCodeActionResponse> 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<IEnumerable<string>> FindRefactoringNamesAsync(string code, bool roslynAnalyzersEnabled = false)
{
var codeActions = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled));

return codeActions.Select(a => a.Name);
}

private async Task<IEnumerable<OmniSharpCodeAction>> FindRefactoringsAsync(string code, IDictionary<string, string> configurationData = null)
{
var testFile = new TestFile(BufferPath, code);

using (var host = CreateOmniSharpHost(new[] { testFile }, configurationData))
{
var requestHandler = host.GetRequestHandler<GetCodeActionsService>(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<RunCodeActionResponse> RunRefactoringsAsync(string code, string identifier, bool wantsChanges = false)
{
var testFile = new TestFile(BufferPath, code);

using (var host = CreateOmniSharpHost(testFile))
{
var requestHandler = host.GetRequestHandler<RunCodeActionService>(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);
}
}
}
}
Loading

0 comments on commit 8978783

Please sign in to comment.