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

Add tests and resource files

上级 b0fe7be3
......@@ -144,4 +144,7 @@
<data name="Update_suppression_format" xml:space="preserve">
<value>Update suppression format</value>
</data>
<data name="Suppress_or_Configure_issues" xml:space="preserve">
<value>Suppress or Configure issues</value>
</data>
</root>
\ No newline at end of file
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -32,6 +32,11 @@
<target state="new">Remove redundant suppression</target>
<note />
</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">
<source>Update suppression format</source>
<target state="new">Update suppression format</target>
......
......@@ -216,7 +216,7 @@ private protected static RunCodeActionParams CreateRunCodeActionParams(string co
Range = location.Range,
Context = new LSP.CodeActionContext()
},
Title = codeActionTitle
DistinctTitle = codeActionTitle
};
/// <summary>
......
......@@ -21,6 +21,6 @@ internal class RunCodeActionParams
/// <summary>
/// Title of the action to execute.
/// </summary>
public string Title { get; set; }
public string DistinctTitle { get; set; }
}
}
......@@ -25,7 +25,7 @@
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
/// <summary>
/// Handles the get code actions command.
/// Resolves a code action by filling out its Edit or Command property.
/// </summary>
[ExportLspMethod(MSLSPMethods.TextDocumentCodeActionResolveName), Shared]
internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeAction, LSP.VSCodeAction>
......@@ -74,7 +74,7 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
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.
if (codeActionToResolve == null)
......@@ -88,33 +88,79 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
return codeAction;
}
// TO-DO: We currently must execute code actions which add new documents on the server as commands,
// since there is no LSP support for adding documents yet. In the future, we should move these actions
// to primarily executing on the client.
// TO-DO:
// 1) We currently must execute code actions which add new documents on the server as commands,
// 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/
var runAsCommand = false;
var applyChangesOperations = operations.Where(operation => operation is ApplyChangesOperation);
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);
foreach (ApplyChangesOperation applyChangesOperation in applyChangesOperations)
{
var solution = document!.Project.Solution;
var solution = document.Project.Solution;
var changes = applyChangesOperation.ChangedSolution.GetChanges(solution);
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(
pc => pc.GetAddedDocuments().Concat(pc.GetAddedAdditionalDocuments().Concat(pc.GetAddedAnalyzerConfigDocuments())));
if (addedDocuments.Any())
{
runAsCommand = true;
break;
return textDocumentEdits.ToArray();
}
// Changed documents
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)
{
var newDoc = applyChangesOperation.ChangedSolution.GetDocument(docId);
......@@ -128,7 +174,8 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
}
// 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)
{
var newDoc = applyChangesOperation.ChangedSolution.GetAnalyzerConfigDocument(docId);
......@@ -140,36 +187,26 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
await GetTextDocumentEdits(textDocumentEdits, newDoc, oldDoc, cancellationToken).ConfigureAwait(false);
}
}
if (!runAsCommand)
{
codeAction.Edit = new LSP.WorkspaceEdit { DocumentChanges = textDocumentEdits.ToArray() };
}
}
// 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[]
// Changed additional documents
// This for loop won't currently execute until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/
// is fixed.
foreach (var docId in changedAdditionalDocuments)
{
new RunCodeActionParams
var newDoc = applyChangesOperation.ChangedSolution.GetAdditionalDocument(docId);
var oldDoc = solution.GetAdditionalDocument(docId);
if (oldDoc == null || newDoc == null)
{
CodeActionParams = data.CodeActionParams,
Title = codeAction.Title
continue;
}
await GetTextDocumentEdits(textDocumentEdits, newDoc, oldDoc, cancellationToken).ConfigureAwait(false);
}
};
}
}
return codeAction;
return textDocumentEdits.ToArray();
}
// Local functions
static async Task GetTextDocumentEdits(
ArrayBuilder<TextDocumentEdit> textDocumentEdits,
TextDocument newDoc,
......@@ -186,43 +223,57 @@ internal class CodeActionResolveHandler : AbstractRequestHandler<LSP.VSCodeActio
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
// instead of the regular title since there's a chance that multiple code actions may have
// the same name, e.g. configure code actions ("None", "Warning", etc.).
CodeAction? codeActionToResolve = null;
foreach (var c in codeActions)
CommandIdentifier = CodeActionsHandler.RunCodeActionCommandName,
Title = codeAction.Title,
Arguments = new object[]
{
var action = CheckForMatchingAction(c, data.DistinctTitle, currentTitle: "");
if (action != null)
new RunCodeActionParams
{
codeActionToResolve = action;
break;
CodeActionParams = data.CodeActionParams,
DistinctTitle = data.DistinctTitle
}
}
};
}
return codeActionToResolve;
}
static CodeAction? CheckForMatchingAction(CodeAction codeAction, string goalTitle, string currentTitle)
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")
// 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);
if (match != null)
{
return match;
}
return match;
}
return null;
}
return null;
}
}
}
......@@ -17,6 +17,7 @@
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
using CodeAction = Microsoft.CodeAnalysis.CodeActions.CodeAction;
......@@ -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.
codeActions = codeActions.Where(c => !(c.Key is CodeActionWithOptions));
var suppressionActions = codeActions.Where(
a => a.Key is AbstractConfigurationActionWithNestedActions &&
(a.Key as AbstractConfigurationActionWithNestedActions)?.IsBulkConfigurationAction == false);
return GetVSCodeActions(request, codeActions);
var results = new List<VSCodeAction>();
foreach (var codeAction in codeActions)
// Local functions
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
// code action.
if (codeAction.Key is AbstractConfigurationActionWithNestedActions)
var suppressionActions = codeActions.Where(
a => a.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):
// 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();
}
return results.ToArray();
static VSCodeAction GenerateVSCodeAction(
CodeActionParams request,
CodeAction codeAction,
CodeActionKind codeActionKind,
string parentTitle = "")
{
var nestedActions = new List<VSCodeAction>();
using var _ = ArrayBuilder<VSCodeAction>.GetInstance(out var nestedActions);
foreach (var action in codeAction.NestedCodeActions)
{
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 @@
// 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 Newtonsoft.Json.Linq;
using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Xunit;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
......@@ -16,7 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions
public class CodeActionsTests : AbstractLanguageServerProtocolTests
{
[Fact]
public async Task TestGetCodeActionsAsync()
public async Task TestCodeActionHandlerAsync()
{
var markup =
@"class A
......@@ -27,31 +29,68 @@ void M()
}
}";
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 useImplicitTypeResult = results.Single(r => r.Title == CSharpAnalyzersResources.Use_implicit_type);
AssertJsonEquals(expected, useImplicitTypeResult);
var expected = CreateCodeAction(
title: CSharpAnalyzersResources.Use_implicit_type,
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,
CreateCodeActionParams(caret), clientCapabilities, null, CancellationToken.None);
return results.Select(r => (LSP.Command)r).ToArray();
}
var markup =
@"class A
{
void M()
{
int {|caret:|}i = 1;
}
}";
using var workspace = CreateTestWorkspace(markup, out var locations);
private static LSP.ClientCapabilities CreateClientCapabilitiesWithExperimentalValue(string experimentalProperty, JToken value)
=> new LSP.ClientCapabilities()
{
Experimental = new JObject
var expected = CreateCodeAction(
title: string.Format(FeaturesResources.Introduce_constant_for_0, "1"),
kind: CodeActionKind.Refactor,
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()
{
TextDocument = CreateTextDocumentIdentifier(caret.Uri),
......@@ -62,15 +101,31 @@ private static LSP.CodeActionParams CreateCodeActionParams(LSP.Location caret)
}
};
private static LSP.Command CreateCommand(string title, LSP.Location location)
=> new LSP.Command()
internal static LSP.VSCodeAction CreateCodeAction(
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,
CommandIdentifier = "Roslyn.RunCodeAction",
Arguments = new object[]
{
CreateRunCodeActionParams(title, location)
}
Kind = kind,
Children = children,
Data = data,
Diagnostics = diagnostics,
};
if (edit != null)
{
action.Edit = edit;
}
if (command != null)
{
action.Command = command;
}
return action;
}
}
}
......@@ -7,11 +7,9 @@
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.LanguageClient
......
......@@ -41,7 +41,7 @@ public async Task<LSP.TextEdit[]> HandleAsync(RunCodeActionParams request, Reque
request.CodeActionParams.Range,
cancellationToken).ConfigureAwait(false);
var actionToRun = codeActions?.FirstOrDefault(a => a.Title == request.Title);
var actionToRun = codeActions?.FirstOrDefault(a => a.Title == request.DistinctTitle);
if (actionToRun != null)
{
......
......@@ -7,7 +7,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
......@@ -22,8 +21,8 @@
namespace Microsoft.VisualStudio.LanguageServices.LiveShare
{
/// <summary>
/// Run code actions handler. Called when lightbulb invoked.
/// Code actions must be applied from the UI thread in VS.
/// Runs code actions as a command on the server.
/// Commands must be applied from the UI thread in VS.
/// </summary>
[ExportExecuteWorkspaceCommand(CodeActionsHandler.RunCodeActionCommandName)]
internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
......@@ -56,12 +55,14 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
var document = _solutionProvider.GetDocument(runRequest.CodeActionParams.TextDocument);
var codeActions = await CodeActionsHandler.GetCodeActionsAsync(document, _codeFixService, _codeRefactoringService,
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)
{
// add check here
foreach (var operation in await actionToRun.GetOperationsAsync(cancellationToken).ConfigureAwait(false))
{
// TODO - This UI thread dependency should be removed.
......@@ -69,9 +70,11 @@ internal class RunCodeActionsHandler : IExecuteWorkspaceCommandHandler
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(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.
先完成此消息的编辑!
想要评论请 注册