-
Notifications
You must be signed in to change notification settings - Fork 419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle nested code actions #753
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ImmutableArray<CodeActionOperation>> GetOperationsAsync(CancellationToken cancellationToken) | ||
{ | ||
return CodeAction.GetOperationsAsync(cancellationToken); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<TRequest, TResponse> : RequestHandler<TRequest, TResponse> | ||
{ | ||
|
@@ -23,41 +25,63 @@ public abstract class BaseCodeActionService<TRequest, TResponse> : RequestHandle | |
|
||
private readonly CodeActionHelper _helper; | ||
|
||
private readonly MethodInfo _getNestedCodeActions; | ||
|
||
protected BaseCodeActionService(OmniSharpWorkspace workspace, CodeActionHelper helper, IEnumerable<ICodeActionProvider> 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<TResponse> Handle(TRequest request); | ||
|
||
|
||
protected async Task<IEnumerable<CodeAction>> GetActionsAsync(ICodeActionRequest request) | ||
protected async Task<IEnumerable<AvailableCodeAction>> GetAvailableCodeActions(ICodeActionRequest request) | ||
{ | ||
var actions = new List<CodeAction>(); | ||
var originalDocument = this.Workspace.GetDocument(request.FileName); | ||
if (originalDocument == null) | ||
{ | ||
return actions; | ||
return Array.Empty<AvailableCodeAction>(); | ||
} | ||
|
||
var refactoringContext = await GetRefactoringContext(originalDocument, request, actions); | ||
if (refactoringContext != null) | ||
{ | ||
await CollectRefactoringActions(refactoringContext.Value); | ||
} | ||
var actions = new List<CodeAction>(); | ||
|
||
var codeFixContext = await GetCodeFixContext(originalDocument, request, actions); | ||
if (codeFixContext != null) | ||
{ | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any thoughts on this (besides just reversing the order like we do today?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have some notion of ordering in Roslyn using attributes but it's messy. At some point, I intend to order them similarly in OmniSharp, but it's down the priority list a bit. For now, I just wanted to add a TODO to indicate that it wasn't ideal. 😄 |
||
|
||
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<CodeRefactoringContext?> GetRefactoringContext(Document originalDocument, ICodeActionRequest request, List<CodeAction> 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<AvailableCodeAction> ConvertToAvailableCodeAction(IEnumerable<CodeAction> actions) | ||
{ | ||
var codeActions = new List<AvailableCodeAction>(); | ||
|
||
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<ImmutableArray<CodeAction>>(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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👎 (at it still being internal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed making it public awhile back, but we tabled the issue because folks didn't like the word "Nested" or "Composite" because it's really about grouping related code actions together rather than making up a larger code action out of little ones. I need to bring this back up with the team again.