diff --git a/src/OmniSharp.Abstractions/Extensions/ReflectionExtensions.cs b/src/OmniSharp.Abstractions/Extensions/ReflectionExtensions.cs index 64228a3e34..19bcad3c12 100644 --- a/src/OmniSharp.Abstractions/Extensions/ReflectionExtensions.cs +++ b/src/OmniSharp.Abstractions/Extensions/ReflectionExtensions.cs @@ -34,11 +34,12 @@ public static Lazy LazyGetMethod(this Lazy lazyType, string me return new Lazy(() => { - var methodInfo = lazyType.Value.GetMethod(methodName); + var type = lazyType.Value; + var methodInfo = type.GetMethod(methodName); if (methodInfo == null) { - throw new InvalidOperationException($"Could not get type '{methodName}'"); + throw new InvalidOperationException($"Could not get method '{methodName}' on type '{type.FullName}'"); } return methodInfo; @@ -54,11 +55,12 @@ public static Lazy LazyGetMethod(this Lazy lazyType, string me return new Lazy(() => { - var methodInfo = lazyType.Value.GetMethod(methodName, bindingFlags); + var type = lazyType.Value; + var methodInfo = type.GetMethod(methodName, bindingFlags); if (methodInfo == null) { - throw new InvalidOperationException($"Could not get type '{methodName}'"); + throw new InvalidOperationException($"Could not get method '{methodName}' on type '{type.FullName}'"); } return methodInfo; @@ -72,11 +74,12 @@ public static MethodInfo GetMethod(this Lazy lazyType, string methodName) throw new ArgumentNullException(nameof(lazyType)); } - var methodInfo = lazyType.Value.GetMethod(methodName); + var type = lazyType.Value; + var methodInfo = type.GetMethod(methodName); if (methodInfo == null) { - throw new InvalidOperationException($"Could not get type '{methodName}'"); + throw new InvalidOperationException($"Could not get method '{methodName}' on type '{type.FullName}'"); } return methodInfo; @@ -89,11 +92,12 @@ public static MethodInfo GetMethod(this Lazy lazyType, string methodName, throw new ArgumentNullException(nameof(lazyType)); } - var methodInfo = lazyType.Value.GetMethod(methodName, bindingFlags); + var type = lazyType.Value; + var methodInfo = type.GetMethod(methodName, bindingFlags); if (methodInfo == null) { - throw new InvalidOperationException($"Could not get type '{methodName}'"); + throw new InvalidOperationException($"Could not get method '{methodName}' on type '{type.FullName}'"); } return methodInfo; diff --git a/src/OmniSharp.Roslyn.CSharp/Extensions/CodeActionExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Extensions/CodeActionExtensions.cs deleted file mode 100644 index ae69b7b490..0000000000 --- a/src/OmniSharp.Roslyn.CSharp/Extensions/CodeActionExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.CodeAnalysis.CodeActions; - -namespace OmniSharp.Roslyn.CSharp.Extensions -{ - public static class CodeActionExtensions - { - public static string GetIdentifier(this CodeAction codeAction) - { - return codeAction.EquivalenceKey ?? codeAction.Title; - } - } -} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs new file mode 100644 index 0000000000..c29e975cf8 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; + +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 +{ + public class AvailableCodeAction + { + public CodeAction CodeAction { get; } + public CodeAction ParentCodeAction { get; } + + public AvailableCodeAction(CodeAction codeAction, CodeAction parentCodeAction = null) + { + if (codeAction == null) + { + throw new ArgumentNullException(nameof(codeAction)); + } + + this.CodeAction = codeAction; + this.ParentCodeAction = parentCodeAction; + } + + public string GetIdentifier() + { + return CodeAction.EquivalenceKey ?? GetTitle(); + } + + public string GetTitle() + { + return ParentCodeAction != null + ? $"{ParentCodeAction.Title} -> {CodeAction.Title}" + : CodeAction.Title; + } + + public Task> GetOperationsAsync(CancellationToken cancellationToken) + { + return CodeAction.GetOperationsAsync(cancellationToken); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs index c56325050a..1d24b60799 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -11,9 +12,10 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Models.V2; +using OmniSharp.Roslyn.CSharp.Services.CodeActions; using OmniSharp.Services; -namespace OmniSharp.Roslyn.CSharp.Services.CodeActions +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 { public abstract class BaseCodeActionService : RequestHandler { @@ -23,31 +25,40 @@ public abstract class BaseCodeActionService : RequestHandle private readonly CodeActionHelper _helper; + private readonly MethodInfo _getNestedCodeActions; + protected BaseCodeActionService(OmniSharpWorkspace workspace, CodeActionHelper helper, IEnumerable providers, ILogger logger) { this.Workspace = workspace; this.Providers = providers; this.Logger = logger; this._helper = helper; + + // Sadly, the CodeAction.NestedCodeActions property is still internal. + var nestedCodeActionsProperty = typeof(CodeAction).GetProperty("NestedCodeActions", BindingFlags.NonPublic | BindingFlags.Instance); + if (nestedCodeActionsProperty == null) + { + throw new InvalidOperationException("Could not find CodeAction.NestedCodeActions property."); + } + + this._getNestedCodeActions = nestedCodeActionsProperty.GetGetMethod(nonPublic: true); + if (this._getNestedCodeActions == null) + { + throw new InvalidOperationException("Could not retrieve 'get' method for CodeAction.NestedCodeActions property."); + } } public abstract Task Handle(TRequest request); - - protected async Task> GetActionsAsync(ICodeActionRequest request) + protected async Task> GetAvailableCodeActions(ICodeActionRequest request) { - var actions = new List(); var originalDocument = this.Workspace.GetDocument(request.FileName); if (originalDocument == null) { - return actions; + return Array.Empty(); } - var refactoringContext = await GetRefactoringContext(originalDocument, request, actions); - if (refactoringContext != null) - { - await CollectRefactoringActions(refactoringContext.Value); - } + var actions = new List(); var codeFixContext = await GetCodeFixContext(originalDocument, request, actions); if (codeFixContext != null) @@ -55,9 +66,22 @@ protected async Task> GetActionsAsync(ICodeActionRequest await CollectCodeFixActions(codeFixContext.Value); } + var refactoringContext = await GetRefactoringContext(originalDocument, request, actions); + if (refactoringContext != null) + { + await CollectRefactoringActions(refactoringContext.Value); + } + + // TODO: Determine good way to order code actions. actions.Reverse(); - return actions; + // 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(actions) + .Where(a => !a.CodeAction.GetType().GetTypeInfo().IsSubclassOf(typeof(CodeActionWithOptions))); + + return availableActions; } private async Task GetRefactoringContext(Document originalDocument, ICodeActionRequest request, List actionsDestination) @@ -124,7 +148,7 @@ private async Task CollectCodeFixActions(CodeFixContext context) // TODO: This is a horrible hack! However, remove unnecessary usings only // responds for diagnostics that are produced by its diagnostic analyzer. // We need to provide a *real* diagnostic engine to address this. - if (codeFix.ToString() != CodeActionHelper.RemoveUnnecessaryUsingsProviderName) + if (codeFix.GetType().FullName != CodeActionHelper.RemoveUnnecessaryUsingsProviderName) { if (!diagnosticIds.Any(id => codeFix.FixableDiagnosticIds.Contains(id))) { @@ -166,5 +190,35 @@ private async Task CollectRefactoringActions(CodeRefactoringContext context) } } } + + private IEnumerable ConvertToAvailableCodeAction(IEnumerable actions) + { + var codeActions = new List(); + + foreach (var action in actions) + { + var handledNestedActions = false; + + // Roslyn supports "nested" code actions in order to allow submenus in the VS light bulb menu. + // For now, we'll just expand nested code actions in place. + var nestedActions = this._getNestedCodeActions.Invoke>(action, null); + if (nestedActions.Length > 0) + { + foreach (var nestedAction in nestedActions) + { + codeActions.Add(new AvailableCodeAction(nestedAction, action)); + } + + handledNestedActions = true; + } + + if (!handledNestedActions) + { + codeActions.Add(new AvailableCodeAction(action)); + } + } + + return codeActions; + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs index 32635bac1e..fd8eb8213e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using OmniSharp.Mef; using OmniSharp.Models.V2; -using OmniSharp.Roslyn.CSharp.Extensions; using OmniSharp.Roslyn.CSharp.Services.CodeActions; using OmniSharp.Services; @@ -27,12 +26,17 @@ public GetCodeActionsService( public override async Task Handle(GetCodeActionsRequest request) { - var actions = await GetActionsAsync(request); + var availableActions = await GetAvailableCodeActions(request); return new GetCodeActionsResponse { - CodeActions = actions.Select(a => new OmniSharpCodeAction(a.GetIdentifier(), a.Title)) + CodeActions = availableActions.Select(ConvertToOmniSharpCodeAction) }; } + + private static OmniSharpCodeAction ConvertToOmniSharpCodeAction(AvailableCodeAction availableAction) + { + return new OmniSharpCodeAction(availableAction.GetIdentifier(), availableAction.GetTitle()); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs index 222df5fa89..73971bed59 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using OmniSharp.Mef; using OmniSharp.Models; -using OmniSharp.Roslyn.CSharp.Extensions; using OmniSharp.Roslyn.CSharp.Services.CodeActions; using OmniSharp.Services; @@ -33,16 +32,16 @@ public RunCodeActionService( public override async Task Handle(RunCodeActionRequest request) { - var actions = await GetActionsAsync(request); - var action = actions.FirstOrDefault(a => a.GetIdentifier().Equals(request.Identifier)); - if (action == null) + var availableActions = await GetAvailableCodeActions(request); + var availableAction = availableActions.FirstOrDefault(a => a.GetIdentifier().Equals(request.Identifier)); + if (availableAction == null) { return new RunCodeActionResponse(); } - Logger.LogInformation($"Applying code action: {action.Title}"); + Logger.LogInformation($"Applying code action: {availableAction.GetTitle()}"); - var operations = await action.GetOperationsAsync(CancellationToken.None); + var operations = await availableAction.GetOperationsAsync(CancellationToken.None); var solution = this.Workspace.CurrentSolution; var changes = new List();