提交 92a5fb72 编写于 作者: A Allison Chou

Add tests and resource files

上级 b0fe7be3
...@@ -144,4 +144,7 @@ ...@@ -144,4 +144,7 @@
<data name="Update_suppression_format" xml:space="preserve"> <data name="Update_suppression_format" xml:space="preserve">
<value>Update suppression format</value> <value>Update suppression format</value>
</data> </data>
<data name="Suppress_or_Configure_issues" xml:space="preserve">
<value>Suppress or Configure issues</value>
</data>
</root> </root>
\ No newline at end of file
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -32,6 +32,11 @@ ...@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target> <target state="new">Remove redundant suppression</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="Suppress_or_Configure_issues">
<source>Suppress or Configure issues</source>
<target state="new">Suppress or Configure issues</target>
<note />
</trans-unit>
<trans-unit id="Update_suppression_format"> <trans-unit id="Update_suppression_format">
<source>Update suppression format</source> <source>Update suppression format</source>
<target state="new">Update suppression format</target> <target state="new">Update suppression format</target>
......
...@@ -216,7 +216,7 @@ private protected static RunCodeActionParams CreateRunCodeActionParams(string co ...@@ -216,7 +216,7 @@ private protected static RunCodeActionParams CreateRunCodeActionParams(string co
Range = location.Range, Range = location.Range,
Context = new LSP.CodeActionContext() Context = new LSP.CodeActionContext()
}, },
Title = codeActionTitle DistinctTitle = codeActionTitle
}; };
/// <summary> /// <summary>
......
...@@ -21,6 +21,6 @@ internal class RunCodeActionParams ...@@ -21,6 +21,6 @@ internal class RunCodeActionParams
/// <summary> /// <summary>
/// Title of the action to execute. /// Title of the action to execute.
/// </summary> /// </summary>
public string Title { get; set; } public string DistinctTitle { get; set; }
} }
} }
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{ {
/// <summary> /// <summary>
/// Handles the get code actions command. /// Resolves a code action by filling out its Edit or Command property.
/// </summary> /// </summary>
[ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName), Shared] [ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName), Shared]
internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeAction, LSP.VSCodeAction> internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeAction, LSP.VSCodeAction>
...@@ -74,7 +74,7 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio ...@@ -74,7 +74,7 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
return codeAction; return codeAction;
} }
var codeActionToResolve = GetCodeActionToResolve(data, codeActions); var codeActionToResolve = GetCodeActionToResolve(data.DistinctTitle, codeActions);
// We didn't find a matching action, so just return the action without an edit or command. // We didn't find a matching action, so just return the action without an edit or command.
if (codeActionToResolve == null) if (codeActionToResolve == null)
...@@ -88,33 +88,79 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio ...@@ -88,33 +88,79 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
return codeAction; return codeAction;
} }
// TO-DO: We currently must execute code actions which add new documents on the server as commands, // TO-DO:
// since there is no LSP support for adding documents yet. In the future, we should move these actions // 1) We currently must execute code actions which add new documents on the server as commands,
// to primarily executing on the client. // since there is no LSP support for adding documents yet. In the future, we should move these actions
// to execute on the client.
// 2) There is also a bug (same tracking item) where code actions that edit documents other than the
// one where the code action was invoked from do not work. We must temporarily execute these as commands
// as well.
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
var runAsCommand = false; var runAsCommand = false;
var applyChangesOperations = operations.Where(operation => operation is ApplyChangesOperation); var applyChangesOperations = operations.Where(operation => operation is ApplyChangesOperation);
if (applyChangesOperations.Any()) if (applyChangesOperations.Any())
{
var workspaceEdits = await ComputeWorkspaceEdits(applyChangesOperations, document!, cancellationToken).ConfigureAwait(false);
if (workspaceEdits.Any())
{
codeAction.Edit = new LSP.WorkspaceEdit { DocumentChanges = workspaceEdits };
}
else
{
// The workspace edit is something we don't currently support, like adding a new document.
runAsCommand = true;
}
}
// Set up to run as command on the server instead of using WorkspaceEdits.
var commandOperations = operations.All(operation => !(operation is ApplyChangesOperation));
if (commandOperations || runAsCommand)
{
codeAction.Command = SetCommand(codeAction, data);
}
return codeAction;
// Local functions
static async Task<TextDocumentEdit[]> ComputeWorkspaceEdits(
IEnumerable<CodeActionOperation> applyChangesOperations,
Document document,
CancellationToken cancellationToken)
{ {
using var _ = ArrayBuilder<TextDocumentEdit>.GetInstance(out var textDocumentEdits); using var _ = ArrayBuilder<TextDocumentEdit>.GetInstance(out var textDocumentEdits);
foreach (ApplyChangesOperation applyChangesOperation in applyChangesOperations) foreach (ApplyChangesOperation applyChangesOperation in applyChangesOperations)
{ {
var solution = document!.Project.Solution; var solution = document.Project.Solution;
var changes = applyChangesOperation.ChangedSolution.GetChanges(solution); var changes = applyChangesOperation.ChangedSolution.GetChanges(solution);
var projectChanges = changes.GetProjectChanges(); var projectChanges = changes.GetProjectChanges();
// If the change involves adding a document, execute via command instead of WorkspaceEdit. // TO-DO: If the change involves adding a document, execute via command instead of WorkspaceEdit
// until adding documents is supported in LSP: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
// After support is added, remove the below if-statement and add code to support adding documents.
var addedDocuments = projectChanges.SelectMany( var addedDocuments = projectChanges.SelectMany(
pc => pc.GetAddedDocuments().Concat(pc.GetAddedAdditionalDocuments().Concat(pc.GetAddedAnalyzerConfigDocuments()))); pc => pc.GetAddedDocuments().Concat(pc.GetAddedAdditionalDocuments().Concat(pc.GetAddedAnalyzerConfigDocuments())));
if (addedDocuments.Any()) if (addedDocuments.Any())
{ {
runAsCommand = true; return textDocumentEdits.ToArray();
break;
} }
// Changed documents
var changedDocuments = projectChanges.SelectMany(pc => pc.GetChangedDocuments()); var changedDocuments = projectChanges.SelectMany(pc => pc.GetChangedDocuments());
var changedAnalyzerConfigDocuments = projectChanges.SelectMany(pc => pc.GetChangedAnalyzerConfigDocuments());
var changedAdditionalDocuments = projectChanges.SelectMany(pc => pc.GetChangedAdditionalDocuments());
// TO-DO: If the change involves modifying any document besides the document where the code action
// was invoked, temporarily execute via command instead of WorkspaceEdit until LSP bug is fixed:
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
// After bug is fixed, remove the below if-statement and the existing code should work.
if (changedDocuments.Any(d => d != document.Id) ||
changedAnalyzerConfigDocuments.Any() ||
changedAdditionalDocuments.Any())
{
return textDocumentEdits.ToArray();
}
// Changed documents
foreach (var docId in changedDocuments) foreach (var docId in changedDocuments)
{ {
var newDoc = applyChangesOperation.ChangedSolution.GetDocument(docId); var newDoc = applyChangesOperation.ChangedSolution.GetDocument(docId);
...@@ -128,7 +174,8 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio ...@@ -128,7 +174,8 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
} }
// Changed analyzer config documents // Changed analyzer config documents
var changedAnalyzerConfigDocuments = projectChanges.SelectMany(pc => pc.GetChangedAnalyzerConfigDocuments()); // This for loop won't currently execute until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
// is fixed.
foreach (var docId in changedAnalyzerConfigDocuments) foreach (var docId in changedAnalyzerConfigDocuments)
{ {
var newDoc = applyChangesOperation.ChangedSolution.GetAnalyzerConfigDocument(docId); var newDoc = applyChangesOperation.ChangedSolution.GetAnalyzerConfigDocument(docId);
...@@ -140,36 +187,26 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio ...@@ -140,36 +187,26 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
await GetTextDocumentEdits(textDocumentEdits, newDoc, oldDoc, cancellationToken).ConfigureAwait(false); await GetTextDocumentEdits(textDocumentEdits, newDoc, oldDoc, cancellationToken).ConfigureAwait(false);
} }
}
if (!runAsCommand) // Changed additional documents
{ // This for loop won't currently execute until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
codeAction.Edit = new LSP.WorkspaceEdit { DocumentChanges = textDocumentEdits.ToArray() }; // is fixed.
} foreach (var docId in changedAdditionalDocuments)
}
// Running as command instead
var commandOperations = operations.Where(operation => !(operation is ApplyChangesOperation));
if (commandOperations.Any() || runAsCommand)
{
codeAction.Command = new LSP.Command
{
CommandIdentifier = CodeActionsHandler.RunCodeActionCommandName,
Title = codeAction.Title,
Arguments = new object[]
{ {
new RunCodeActionParams var newDoc = applyChangesOperation.ChangedSolution.GetAdditionalDocument(docId);
var oldDoc = solution.GetAdditionalDocument(docId);
if (oldDoc == null || newDoc == null)
{ {
CodeActionParams = data.CodeActionParams, continue;
Title = codeAction.Title
} }
await GetTextDocumentEdits(textDocumentEdits, newDoc, oldDoc, cancellationToken).ConfigureAwait(false);
} }
}; }
}
return codeAction; return textDocumentEdits.ToArray();
}
// Local functions
static async Task GetTextDocumentEdits( static async Task GetTextDocumentEdits(
ArrayBuilder<TextDocumentEdit> textDocumentEdits, ArrayBuilder<TextDocumentEdit> textDocumentEdits,
TextDocument newDoc, TextDocument newDoc,
...@@ -186,43 +223,57 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio ...@@ -186,43 +223,57 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
textDocumentEdits.Add(new TextDocumentEdit() { TextDocument = documentIdentifier, Edits = edits.ToArray() }); textDocumentEdits.Add(new TextDocumentEdit() { TextDocument = documentIdentifier, Edits = edits.ToArray() });
} }
static CodeAction? GetCodeActionToResolve(CodeActionResolveData data, IEnumerable<CodeAction> codeActions) static LSP.Command SetCommand(VSCodeAction codeAction, CodeActionResolveData data) => new LSP.Command
{ {
// First, we search for the matching code action. We compare against the distinct title CommandIdentifier = CodeActionsHandler.RunCodeActionCommandName,
// instead of the regular title since there's a chance that multiple code actions may have Title = codeAction.Title,
// the same name, e.g. configure code actions ("None", "Warning", etc.). Arguments = new object[]
CodeAction? codeActionToResolve = null;
foreach (var c in codeActions)
{ {
var action = CheckForMatchingAction(c, data.DistinctTitle, currentTitle: ""); new RunCodeActionParams
if (action != null)
{ {
codeActionToResolve = action; CodeActionParams = data.CodeActionParams,
break; DistinctTitle = data.DistinctTitle
} }
} }
};
}
return codeActionToResolve; internal static CodeAction? GetCodeActionToResolve(string distinctTitle, IEnumerable<CodeAction> codeActions)
} {
// Searching for the matching code action. We compare against the distinct title (e.g. "Suppress IDExxxxNone")
static CodeAction? CheckForMatchingAction(CodeAction codeAction, string goalTitle, string currentTitle) // instead of the regular title (e.g. "None") since there's a chance that multiple code actions may have
// the same regular title.
CodeAction? codeActionToResolve = null;
foreach (var c in codeActions)
{ {
if (currentTitle + codeAction.Title == goalTitle) var action = CheckForMatchingAction(c, distinctTitle, currentTitle: "");
if (action != null)
{ {
return codeAction; codeActionToResolve = action;
break;
} }
}
return codeActionToResolve;
}
foreach (var nestedAction in codeAction.NestedCodeActions) private static CodeAction? CheckForMatchingAction(CodeAction codeAction, string goalTitle, string currentTitle)
{
if (currentTitle + codeAction.Title == goalTitle)
{
return codeAction;
}
foreach (var nestedAction in codeAction.NestedCodeActions)
{
var match = CheckForMatchingAction(nestedAction, goalTitle, currentTitle + codeAction.Title);
if (match != null)
{ {
var match = CheckForMatchingAction(nestedAction, goalTitle, currentTitle + codeAction.Title); return match;
if (match != null)
{
return match;
}
} }
return null;
} }
return null;
} }
} }
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction; using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction; using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
...@@ -63,42 +64,50 @@ internal class CodeActionsHandler : AbstractRequestHandler<LSP.CodeActionParams, ...@@ -63,42 +64,50 @@ internal class CodeActionsHandler : AbstractRequestHandler<LSP.CodeActionParams,
// Filter out code actions with options since they'll show dialogs and we can't remote the UI and the options. // Filter out code actions with options since they'll show dialogs and we can't remote the UI and the options.
codeActions = codeActions.Where(c => !(c.Key is CodeActionWithOptions)); codeActions = codeActions.Where(c => !(c.Key is CodeActionWithOptions));
var suppressionActions = codeActions.Where( return GetVSCodeActions(request, codeActions);
a => a.Key is AbstractConfigurationActionWithNestedActions &&
(a.Key as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == false);
var results = new List<VSCodeAction>(); // Local functions
foreach (var codeAction in codeActions) static VSCodeAction[] GetVSCodeActions(
CodeActionParams request,
IEnumerable<KeyValuePair<CodeAction, CodeActionKind>> codeActions)
{ {
// Temporarily filter out suppress and configure code actions, as we'll later combine them under a top-level var suppressionActions = codeActions.Where(
// code action. a => a.Key is AbstractConfigurationActionWithNestedActions &&
if (codeAction.Key is AbstractConfigurationActionWithNestedActions) (a.Key as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == false);
using var _ = ArrayBuilder<VSCodeAction>.GetInstance(out var results);
foreach (var codeAction in codeActions)
{ {
continue; // Temporarily filter out suppress and configure code actions, as we'll later combine them under a top-level
// code action.
if (codeAction.Key is AbstractConfigurationActionWithNestedActions)
{
continue;
}
results.Add(GenerateVSCodeAction(request, codeAction.Key, codeAction.Value));
} }
results.Add(GenerateVSCodeAction(request, codeAction.Key, codeAction.Value)); // Special case (also dealt with specially in local Roslyn):
} // If we have configure/suppress code actions, combine them under one top-level code action.
var configureSuppressActions = codeActions.Where(a => a.Key is AbstractConfigurationActionWithNestedActions);
if (configureSuppressActions.Any())
{
results.Add(GenerateVSCodeAction(request, new CodeActionWithNestedActions(
CodeFixesResources.Suppress_or_Configure_issues,
configureSuppressActions.Select(a => a.Key).ToImmutableArray(), true), CodeActionKind.QuickFix));
}
// Special case (also dealt with specially in local Roslyn): return results.ToArray();
// If we have configure/suppress code actions, combine them under one top-level code action.
var configureSuppressActions = codeActions.Where(a => a.Key is AbstractConfigurationActionWithNestedActions);
if (configureSuppressActions.Any())
{
results.Add(GenerateVSCodeAction(request, new CodeActionWithNestedActions(
"Suppress or Configure issues",
configureSuppressActions.Select(a => a.Key).ToImmutableArray(), true), CodeActionKind.QuickFix));
} }
return results.ToArray();
static VSCodeAction GenerateVSCodeAction( static VSCodeAction GenerateVSCodeAction(
CodeActionParams request, CodeActionParams request,
CodeAction codeAction, CodeAction codeAction,
CodeActionKind codeActionKind, CodeActionKind codeActionKind,
string parentTitle = "") string parentTitle = "")
{ {
var nestedActions = new List<VSCodeAction>(); using var _ = ArrayBuilder<VSCodeAction>.GetInstance(out var nestedActions);
foreach (var action in codeAction.NestedCodeActions) foreach (var action in codeAction.NestedCodeActions)
{ {
nestedActions.Add(GenerateVSCodeAction(request, action, codeActionKind, codeAction.Title)); nestedActions.Add(GenerateVSCodeAction(request, action, codeActionKind, codeAction.Title));
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions
{
public class CodeActionResolveTests : AbstractLanguageServerProtocolTests
{
[Fact]
public async Task TestCodeActionResolveHandlerAsync()
{
var initialMarkup =
@"class A
{
void M()
{
{|caret:|}int i = 1;
}
}";
using var workspace = CreateTestWorkspace(initialMarkup, out var locations);
var unresolvedCodeAction = CodeActionsTests.CreateCodeAction(
title: CSharpAnalyzersResources.Use_implicit_type,
kind: CodeActionKind.Refactor,
children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{
CodeActionParams = CodeActionsTests.CreateCodeActionParams(locations["caret"].Single()),
DistinctTitle = CSharpAnalyzersResources.Use_implicit_type
},
diagnostics: null);
var expectedMarkup =
@"class A
{
void M()
{
var i = 1;
}
}";
var expected = CodeActionsTests.CreateCodeAction(
title: CSharpAnalyzersResources.Use_implicit_type,
kind: CodeActionKind.Refactor,
children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{
CodeActionParams = CodeActionsTests.CreateCodeActionParams(locations["caret"].Single()),
DistinctTitle = CSharpAnalyzersResources.Use_implicit_type
},
diagnostics: null,
edit: new LSP.WorkspaceEdit()
{
DocumentChanges = new TextDocumentEdit[]
{
new TextDocumentEdit()
{
TextDocument = new VersionedTextDocumentIdentifier()
{
Uri = locations["caret"].Single().Uri
},
Edits = new TextEdit[] {
new TextEdit()
{
NewText = expectedMarkup,
Range = new LSP.Range() { Start = new Position(0, 0), End = new Position(6, 1) }
}
}
}
}
});
var result = await RunGetCodeActionResolveAsync(workspace.CurrentSolution, unresolvedCodeAction);
AssertJsonEquals(expected, result);
}
[Fact]
public async Task TestCodeActionResolveHandlerAsync_NestedAction()
{
var initialMarkup =
@"class A
{
void M()
{
int {|caret:|}i = 1;
}
}";
using var workspace = CreateTestWorkspace(initialMarkup, out var locations);
var unresolvedCodeAction = CodeActionsTests.CreateCodeAction(
title: string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
kind: CodeActionKind.Refactor,
children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{
CodeActionParams = CodeActionsTests.CreateCodeActionParams(locations["caret"].Single()),
DistinctTitle = FeaturesResources.Introduce_constant + string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
},
diagnostics: null);
var expectedMarkup =
@"class A
{
private const int V = 1;
void M()
{
int i = V;
}
}";
var expected = CodeActionsTests.CreateCodeAction(
title: string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
kind: CodeActionKind.Refactor,
children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{
CodeActionParams = CodeActionsTests.CreateCodeActionParams(locations["caret"].Single()),
DistinctTitle = FeaturesResources.Introduce_constant + string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
},
diagnostics: null,
edit: new LSP.WorkspaceEdit()
{
DocumentChanges = new TextDocumentEdit[]
{
new TextDocumentEdit()
{
TextDocument = new VersionedTextDocumentIdentifier()
{
Uri = locations["caret"].Single().Uri
},
Edits = new TextEdit[] {
new TextEdit()
{
NewText = expectedMarkup,
Range = new LSP.Range() { Start = new Position(0, 0), End = new Position(6, 1) }
}
}
}
}
});
var result = await RunGetCodeActionResolveAsync(workspace.CurrentSolution, unresolvedCodeAction);
AssertJsonEquals(expected, result);
}
private static async Task<LSP.VSCodeAction> RunGetCodeActionResolveAsync(
Solution solution,
VSCodeAction unresolvedCodeAction,
LSP.ClientCapabilities clientCapabilities = null)
{
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.VSCodeAction, LSP.VSCodeAction>(
LSP.MSLSPMethods.TextDocumentCodeActionResolveName, unresolvedCodeAction,
clientCapabilities, null, CancellationToken.None);
return result;
}
}
}
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Newtonsoft.Json.Linq; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities; using Roslyn.Test.Utilities;
using Xunit; using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
...@@ -16,7 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions ...@@ -16,7 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions
public class CodeActionsTests : AbstractLanguageServerProtocolTests public class CodeActionsTests : AbstractLanguageServerProtocolTests
{ {
[Fact] [Fact]
public async Task TestGetCodeActionsAsync() public async Task TestCodeActionHandlerAsync()
{ {
var markup = var markup =
@"class A @"class A
...@@ -27,31 +29,68 @@ void M() ...@@ -27,31 +29,68 @@ void M()
} }
}"; }";
using var workspace = CreateTestWorkspace(markup, out var locations); using var workspace = CreateTestWorkspace(markup, out var locations);
var expected = CreateCommand(CSharpAnalyzersResources.Use_implicit_type, locations["caret"].Single());
var clientCapabilities = CreateClientCapabilitiesWithExperimentalValue("supportsWorkspaceEdits", JToken.FromObject(false));
var results = await RunGetCodeActionsAsync(workspace.CurrentSolution, locations["caret"].Single(), clientCapabilities); var expected = CreateCodeAction(
var useImplicitTypeResult = results.Single(r => r.Title == CSharpAnalyzersResources.Use_implicit_type); title: CSharpAnalyzersResources.Use_implicit_type,
AssertJsonEquals(expected, useImplicitTypeResult); kind: CodeActionKind.Refactor,
children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{
CodeActionParams = CreateCodeActionParams(locations["caret"].Single()),
DistinctTitle = CSharpAnalyzersResources.Use_implicit_type
},
diagnostics: null);
var results = await RunGetCodeActionsAsync(workspace.CurrentSolution, locations["caret"].Single());
var useImplicitType = results.FirstOrDefault(r => r.Title == CSharpAnalyzersResources.Use_implicit_type);
AssertJsonEquals(expected, useImplicitType);
} }
private static async Task<LSP.Command[]> RunGetCodeActionsAsync(Solution solution, LSP.Location caret, LSP.ClientCapabilities clientCapabilities = null) [Fact]
public async Task TestCodeActionHandlerAsync_NestedAction()
{ {
var results = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CodeActionParams, LSP.SumType<LSP.Command, LSP.CodeAction>[]>(LSP.Methods.TextDocumentCodeActionName, var markup =
CreateCodeActionParams(caret), clientCapabilities, null, CancellationToken.None); @"class A
return results.Select(r => (LSP.Command)r).ToArray(); {
} void M()
{
int {|caret:|}i = 1;
}
}";
using var workspace = CreateTestWorkspace(markup, out var locations);
private static LSP.ClientCapabilities CreateClientCapabilitiesWithExperimentalValue(string experimentalProperty, JToken value) var expected = CreateCodeAction(
=> new LSP.ClientCapabilities() title: string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
{ kind: CodeActionKind.Refactor,
Experimental = new JObject children: Array.Empty<LSP.VSCodeAction>(),
data: new CodeActionResolveData
{ {
{ experimentalProperty, value } CodeActionParams = CreateCodeActionParams(locations["caret"].Single()),
} DistinctTitle = FeaturesResources.Introduce_constant + string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
}; },
diagnostics: null);
private static LSP.CodeActionParams CreateCodeActionParams(LSP.Location caret) var results = await RunGetCodeActionsAsync(workspace.CurrentSolution, locations["caret"].Single());
var introduceConstant = results[0].Children.FirstOrDefault(
r => ((CodeActionResolveData)r.Data).DistinctTitle == FeaturesResources.Introduce_constant
+ string.Format(FeaturesResources.Introduce_constant_for_0, "1"));
AssertJsonEquals(expected, introduceConstant);
}
private static async Task<LSP.VSCodeAction[]> RunGetCodeActionsAsync(
Solution solution,
LSP.Location caret,
LSP.ClientCapabilities clientCapabilities = null)
{
var result = await GetLanguageServer(solution).ExecuteRequestAsync<LSP.CodeActionParams, LSP.VSCodeAction[]>(
LSP.Methods.TextDocumentCodeActionName, CreateCodeActionParams(caret),
clientCapabilities, null, CancellationToken.None);
return result;
}
internal static LSP.CodeActionParams CreateCodeActionParams(LSP.Location caret)
=> new LSP.CodeActionParams() => new LSP.CodeActionParams()
{ {
TextDocument = CreateTextDocumentIdentifier(caret.Uri), TextDocument = CreateTextDocumentIdentifier(caret.Uri),
...@@ -62,15 +101,31 @@ private static LSP.CodeActionParams CreateCodeActionParams(LSP.Location caret) ...@@ -62,15 +101,31 @@ private static LSP.CodeActionParams CreateCodeActionParams(LSP.Location caret)
} }
}; };
private static LSP.Command CreateCommand(string title, LSP.Location location) internal static LSP.VSCodeAction CreateCodeAction(
=> new LSP.Command() string title, LSP.CodeActionKind kind, LSP.VSCodeAction[] children,
CodeActionResolveData data, LSP.Diagnostic[] diagnostics,
LSP.WorkspaceEdit edit = null, LSP.Command command = null)
{
var action = new LSP.VSCodeAction
{ {
Title = title, Title = title,
CommandIdentifier = "Roslyn.RunCodeAction", Kind = kind,
Arguments = new object[] Children = children,
{ Data = data,
CreateRunCodeActionParams(title, location) Diagnostics = diagnostics,
}
}; };
if (edit != null)
{
action.Edit = edit;
}
if (command != null)
{
action.Command = command;
}
return action;
}
} }
} }
...@@ -7,11 +7,9 @@ ...@@ -7,11 +7,9 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Composition; using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageClient namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageClient
......
...@@ -41,7 +41,7 @@ public async Task<LSP.TextEdit[]> HandleAsync(RunCodeActionParams request, Reque ...@@ -41,7 +41,7 @@ public async Task<LSP.TextEdit[]> HandleAsync(RunCodeActionParams request, Reque
request.CodeActionParams.Range, request.CodeActionParams.Range,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
var actionToRun = codeActions?.FirstOrDefault(a => a.Title == request.Title); var actionToRun = codeActions?.FirstOrDefault(a => a.Title == request.DistinctTitle);
if (actionToRun != null) if (actionToRun != null)
{ {
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
...@@ -22,8 +21,8 @@ ...@@ -22,8 +21,8 @@
namespace Microsoft.VisualStudio.LanguageServices.LiveShare namespace Microsoft.VisualStudio.LanguageServices.LiveShare
{ {
/// <summary> /// <summary>
/// Run code actions handler. Called when lightbulb invoked. /// Runs code actions as a command on the server.
/// Code actions must be applied from the UI thread in VS. /// Commands must be applied from the UI thread in VS.
/// </summary> /// </summary>
[ExportExecuteWorkspaceCommand(CodeActionsHandler.RunCodeActionCommandName)] [ExportExecuteWorkspaceCommand(CodeActionsHandler.RunCodeActionCommandName)]
internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
...@@ -56,12 +55,14 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler ...@@ -56,12 +55,14 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
var document = _solutionProvider.GetDocument(runRequest.CodeActionParams.TextDocument); var document = _solutionProvider.GetDocument(runRequest.CodeActionParams.TextDocument);
var codeActions = await CodeActionsHandler.GetCodeActionsAsync(document, _codeFixService, _codeRefactoringService, var codeActions = await CodeActionsHandler.GetCodeActionsAsync(document, _codeFixService, _codeRefactoringService,
runRequest.CodeActionParams.Range, cancellationToken).ConfigureAwait(false); runRequest.CodeActionParams.Range, cancellationToken).ConfigureAwait(false);
if (codeActions == null)
{
return false;
}
var actionToRun = codeActions?.FirstOrDefault(a => a.Title == runRequest.Title); var actionToRun = CodeActionResolveHandler.GetCodeActionToResolve(runRequest.DistinctTitle, codeActions);
if (actionToRun != null) if (actionToRun != null)
{ {
// add check here
foreach (var operation in await actionToRun.GetOperationsAsync(cancellationToken).ConfigureAwait(false)) foreach (var operation in await actionToRun.GetOperationsAsync(cancellationToken).ConfigureAwait(false))
{ {
// TODO - This UI thread dependency should be removed. // TODO - This UI thread dependency should be removed.
...@@ -69,9 +70,11 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler ...@@ -69,9 +70,11 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
operation.Apply(document.Project.Solution.Workspace, cancellationToken); operation.Apply(document.Project.Solution.Workspace, cancellationToken);
} }
return true;
} }
return true; return false;
} }
} }
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册