diff --git a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 497dade34a9f84822be6167617db77061503ee38..11f5a3728c3497a1fcc89355e70893fb64ac28e7 100644 --- a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; @@ -30,10 +31,13 @@ protected virtual ExportProvider GetExportProvider() { var requestHelperTypes = DesktopTestHelpers.GetAllTypesImplementingGivenInterface( typeof(IRequestHandler).Assembly, typeof(IRequestHandler)); + var executeCommandHandlerTypes = DesktopTestHelpers.GetAllTypesImplementingGivenInterface( + typeof(IExecuteWorkspaceCommandHandler).Assembly, typeof(IExecuteWorkspaceCommandHandler)); var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory( TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic .WithPart(typeof(LanguageServerProtocol)) - .WithParts(requestHelperTypes)); + .WithParts(requestHelperTypes) + .WithParts(executeCommandHandlerTypes)); return exportProviderFactory.CreateExportProvider(); } diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs index c114a66c50bd98d07ce222291cd0082b9149a4ef..aa7d93a9a0e9c9afbeeb20817170f5e2d8612721 100644 --- a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandler.cs @@ -28,30 +28,28 @@ public CodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringServic } public async Task HandleRequestAsync(Solution solution, LSP.CodeActionParams request, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var codeActions = await GetCodeActionsAsync(solution, request.TextDocument.Uri, request.Range, - cancellationToken).ConfigureAwait(false); + keepThreadContext, + cancellationToken).ConfigureAwait(keepThreadContext); // 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 is CodeActionWithOptions)); - var commands = new ArrayBuilder(); + var clientSupportsWorkspaceEdits = true; + if (clientCapabilities?.Experimental is JObject clientCapabilitiesExtensions) + { + clientSupportsWorkspaceEdits = clientCapabilitiesExtensions.SelectToken("supportsWorkspaceEdits")?.Value() ?? clientSupportsWorkspaceEdits; + } + var result = new ArrayBuilder(); foreach (var codeAction in codeActions) { - object[] remoteCommandArguments; // If we have a codeaction with a single applychangesoperation, we want to send the codeaction with the edits. - var operations = await codeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false); - - var clientSupportsWorkspaceEdits = true; - if (clientCapabilities?.Experimental is JObject clientCapabilitiesExtensions) - { - clientSupportsWorkspaceEdits = clientCapabilitiesExtensions.SelectToken("supportsWorkspaceEdits")?.Value() ?? clientSupportsWorkspaceEdits; - } - + var operations = await codeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(keepThreadContext); if (clientSupportsWorkspaceEdits && operations.Length == 1 && operations.First() is ApplyChangesOperation applyChangesOperation) { var workspaceEdit = new LSP.WorkspaceEdit { Changes = new Dictionary() }; @@ -62,8 +60,8 @@ public CodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringServic { var newDoc = applyChangesOperation.ChangedSolution.GetDocument(docId); var oldDoc = solution.GetDocument(docId); - var oldText = await oldDoc.GetTextAsync(cancellationToken).ConfigureAwait(false); - var textChanges = await newDoc.GetTextChangesAsync(oldDoc).ConfigureAwait(false); + var oldText = await oldDoc.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); + var textChanges = await newDoc.GetTextChangesAsync(oldDoc).ConfigureAwait(keepThreadContext); var edits = textChanges.Select(tc => new LSP.TextEdit { @@ -74,7 +72,7 @@ public CodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringServic workspaceEdit.Changes.Add(newDoc.FilePath, edits.ToArray()); } - remoteCommandArguments = new object[] { new LSP.CodeAction { Title = codeAction.Title, Edit = workspaceEdit } }; + result.Add(new LSP.CodeAction { Title = codeAction.Title, Edit = workspaceEdit }); } // Otherwise, send the original request to be executed on the host. else @@ -82,8 +80,7 @@ public CodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringServic // Note that we can pass through the params for this // request (like range, filename) because between getcodeaction and runcodeaction there can be no // changes on the IDE side (it will requery for codeactions if there are changes). - remoteCommandArguments = new object[] - { + result.Add( new LSP.Command { CommandIdentifier = RunCodeActionCommandName, @@ -96,24 +93,11 @@ public CodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringServic Title = codeAction.Title } } - } - }; + }); } - - // We need to return a command that is a generic wrapper that VS Code can execute. - // The argument to this wrapper will either be a RunCodeAction command which will carry - // enough information to run the command or a CodeAction with the edits. - var command = new LSP.Command - { - Title = codeAction.Title, - CommandIdentifier = $"{RemoteCommandNamePrefix}.{ProviderName}", - Arguments = remoteCommandArguments - }; - - commands.Add(command); } - return commands.ToArrayAndFree(); + return result.ToArrayAndFree(); } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandlerBase.cs b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandlerBase.cs index 949acd779e12d3b69961fd0e101ebffb670cb2b0..02f42decf10dfcbc909b0072f0c58b476530a115 100644 --- a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandlerBase.cs +++ b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionsHandlerBase.cs @@ -18,10 +18,7 @@ internal abstract class CodeActionsHandlerBase private readonly ICodeFixService _codeFixService; private readonly ICodeRefactoringService _codeRefactoringService; - // TODO - this should not be liveshare. - protected const string RemoteCommandNamePrefix = "_liveshare.remotecommand"; protected const string RunCodeActionCommandName = "Roslyn.RunCodeAction"; - protected const string ProviderName = "Roslyn"; public CodeActionsHandlerBase(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService) { @@ -29,7 +26,7 @@ public CodeActionsHandlerBase(ICodeFixService codeFixService, ICodeRefactoringSe _codeRefactoringService = codeRefactoringService ?? throw new ArgumentNullException(nameof(codeRefactoringService)); } - public async Task> GetCodeActionsAsync(Solution solution, Uri documentUri, LSP.Range selection, CancellationToken cancellationToken) + public async Task> GetCodeActionsAsync(Solution solution, Uri documentUri, LSP.Range selection, bool keepThreadContext, CancellationToken cancellationToken) { var document = solution.GetDocumentFromURI(documentUri); if (document == null) @@ -37,11 +34,11 @@ public async Task> GetCodeActionsAsync(Solution solution return ImmutableArray.Empty; } - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); var textSpan = ProtocolConversions.RangeToTextSpan(selection, text); - var codeFixCollections = await _codeFixService.GetFixesAsync(document, textSpan, true, cancellationToken).ConfigureAwait(false); - var codeRefactorings = await _codeRefactoringService.GetRefactoringsAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + var codeFixCollections = await _codeFixService.GetFixesAsync(document, textSpan, true, cancellationToken).ConfigureAwait(keepThreadContext); + var codeRefactorings = await _codeRefactoringService.GetRefactoringsAsync(document, textSpan, cancellationToken).ConfigureAwait(keepThreadContext); var codeActions = codeFixCollections.SelectMany(c => c.Fixes.Select(f => f.Action)).Concat( codeRefactorings.SelectMany(r => r.Actions)); diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7d179d77f355e23bbd414414039917fc1763a17 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/CodeActions/RunCodeActionsHandler.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands; +using Newtonsoft.Json.Linq; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler +{ + [ExportExecuteWorkspaceCommand(RunCodeActionCommandName)] + internal class RunCodeActionsHandler : CodeActionsHandlerBase, IExecuteWorkspaceCommandHandler + { + [ImportingConstructor] + public RunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService) + : base(codeFixService, codeRefactoringService) + { + } + + public async Task HandleRequestAsync(Solution solution, LSP.ExecuteCommandParams request, LSP.ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) + { + var runRequest = ((JToken)request.Arguments.Single()).ToObject(); + var codeActions = await GetCodeActionsAsync(solution, + runRequest.CodeActionParams.TextDocument.Uri, + runRequest.CodeActionParams.Range, + keepThreadContext, + cancellationToken).ConfigureAwait(keepThreadContext); + + var actionToRun = codeActions?.FirstOrDefault(a => a.Title == runRequest.Title); + + if (actionToRun != null) + { + foreach (var operation in await actionToRun.GetOperationsAsync(cancellationToken).ConfigureAwait(keepThreadContext)) + { + operation.Apply(solution.Workspace, cancellationToken); + } + } + + return true; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Commands/ExecuteWorkspaceCommandHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Commands/ExecuteWorkspaceCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..d4e41939a02729a2d4c38ce66cc7806891770909 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Commands/ExecuteWorkspaceCommandHandler.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler +{ + [Shared] + [ExportLspMethod(LSP.Methods.WorkspaceExecuteCommandName)] + internal class ExecuteWorkspaceCommandHandler : IRequestHandler + { + private readonly ImmutableDictionary> _executeCommandHandlers; + + [ImportingConstructor] + public ExecuteWorkspaceCommandHandler([ImportMany] IEnumerable> executeCommandHandlers) + { + _executeCommandHandlers = CreateMap(executeCommandHandlers); + } + + private static ImmutableDictionary> CreateMap( + IEnumerable> requestHandlers) + { + var requestHandlerDictionary = ImmutableDictionary.CreateBuilder>(); + foreach (var lazyHandler in requestHandlers) + { + requestHandlerDictionary.Add(lazyHandler.Metadata.CommandName, lazyHandler); + } + + return requestHandlerDictionary.ToImmutable(); + } + + /// + /// Handles an + /// by delegating to a handler for the specific command requested. + /// + public Task HandleRequestAsync(Solution solution, LSP.ExecuteCommandParams request, LSP.ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) + { + var commandName = request.Command; + if (string.IsNullOrEmpty(commandName) || !_executeCommandHandlers.TryGetValue(commandName, out var executeCommandHandler)) + { + throw new ArgumentException(string.Format("Command name ({0}) is invalid", commandName)); + } + + return executeCommandHandler.Value.HandleRequestAsync(solution, request, clientCapabilities, cancellationToken, keepThreadContext); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Commands/ExportExecuteWorkspaceCommandAttribute.cs b/src/Features/LanguageServer/Protocol/Handler/Commands/ExportExecuteWorkspaceCommandAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..90b5617203890fdff267624fde481639653a1385 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Commands/ExportExecuteWorkspaceCommandAttribute.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Commands +{ + /// + /// Defines an attribute to export LSP handlers to handle a specific command from the 'workspace/executeCommand' method. + /// + [AttributeUsage(AttributeTargets.Class), MetadataAttribute] + internal class ExportExecuteWorkspaceCommandAttribute : ExportAttribute, IExecuteWorkspaceCommandHandlerMetadata + { + public string CommandName { get; } + + public ExportExecuteWorkspaceCommandAttribute(string commandName) : base(typeof(IExecuteWorkspaceCommandHandler)) + { + if (string.IsNullOrEmpty(commandName)) + { + throw new ArgumentException(nameof(commandName)); + } + + CommandName = commandName; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..1518f79854480a3486be3f4974e1ce361104c94a --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandler.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Commands +{ + internal interface IExecuteWorkspaceCommandHandler + { + /// + /// Handles a specific command from a request. + /// + Task HandleRequestAsync(Solution solution, ExecuteCommandParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandlerMetadata.cs b/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandlerMetadata.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5c87bf51012d9b19bb1f2e6b62d780846954667 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Commands/IExecuteWorkspaceCommandHandlerMetadata.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Commands +{ + internal interface IExecuteWorkspaceCommandHandlerMetadata + { + /// + /// Defines the 'workspace/executeCommand' command name that should be handled. + /// + string CommandName { get; } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs index 6bcad80722b456f81c38c923c35f98683ba99996..96637ab76c3bd5cc6397ca5e68fb1cec12ecd837 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs @@ -21,7 +21,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler [ExportLspMethod(LSP.Methods.TextDocumentCompletionName)] internal class CompletionHandler : IRequestHandler { - public async Task HandleRequestAsync(Solution solution, LSP.CompletionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + public async Task HandleRequestAsync(Solution solution, LSP.CompletionParams request, LSP.ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) { var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document == null) @@ -29,10 +30,10 @@ public async Task HandleRequestAsync(Solution solution, LSP.CompletionPa return Array.Empty(); } - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); var completionService = document.Project.LanguageServices.GetService(); - var list = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false); + var list = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(keepThreadContext); if (list == null) { return Array.Empty(); diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs index 6a268221849618db6036a8b861fa3fad6365c064..d11ee984fdd2d04f682ed9a6298dba5a5b77dff3 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class CompletionResolveHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, LSP.CompletionItem completionItem, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext) { CompletionResolveData data; if (completionItem.Data is CompletionResolveData) @@ -40,10 +40,10 @@ internal class CompletionResolveHandler : IRequestHandler(); - var list = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false); + var list = await completionService.GetCompletionsAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(keepThreadContext); if (list == null) { return completionItem; @@ -55,7 +55,7 @@ internal class CompletionResolveHandler : IRequestHandler HandleRequestAsync(Solution solution, LSP.TextDocumentPositionParams request, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { - return await GetDefinitionAsync(solution, request, false, cancellationToken).ConfigureAwait(false); + return await GetDefinitionAsync(solution, request, typeOnly: false, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext); } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandlerBase.cs b/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandlerBase.cs index 2dddb42ec0f7381c925414f6a794c543ed481702..e103a1422298d1d8d519678d5ab6e222ac76caca 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandlerBase.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToDefinitionHandlerBase.cs @@ -21,7 +21,7 @@ public GoToDefinitionHandlerBase(IMetadataAsSourceFileService metadataAsSourceFi _metadataAsSourceFileService = metadataAsSourceFileService; } - protected async Task GetDefinitionAsync(Solution solution, LSP.TextDocumentPositionParams request, bool typeOnly, CancellationToken cancellationToken) + protected async Task GetDefinitionAsync(Solution solution, LSP.TextDocumentPositionParams request, bool typeOnly, bool keepThreadContext, CancellationToken cancellationToken) { var locations = ArrayBuilder.GetInstance(); @@ -31,10 +31,10 @@ protected async Task GetDefinitionAsync(Solution solution, LSP.T return locations.ToArrayAndFree(); } - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); var definitionService = document.Project.LanguageServices.GetService(); - var definitions = await definitionService.FindDefinitionsAsync(document, position, cancellationToken).ConfigureAwait(false); + var definitions = await definitionService.FindDefinitionsAsync(document, position, cancellationToken).ConfigureAwait(keepThreadContext); if (definitions != null && definitions.Count() > 0) { foreach (var definition in definitions) @@ -44,7 +44,7 @@ protected async Task GetDefinitionAsync(Solution solution, LSP.T continue; } - var definitionText = await definition.Document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var definitionText = await definition.Document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); locations.Add(new LSP.Location { Uri = definition.Document.GetURI(), @@ -55,12 +55,12 @@ protected async Task GetDefinitionAsync(Solution solution, LSP.T else if (document.SupportsSemanticModel && _metadataAsSourceFileService != null) { // No definition found - see if we can get metadata as source but that's only applicable for C#\VB. - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken).ConfigureAwait(false); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken).ConfigureAwait(keepThreadContext); if (symbol != null && symbol.Locations != null && !symbol.Locations.IsEmpty && symbol.Locations.First().IsInMetadata) { if (!typeOnly || symbol is ITypeSymbol) { - var declarationFile = await _metadataAsSourceFileService.GetGeneratedFileAsync(document.Project, symbol, false, cancellationToken).ConfigureAwait(false); + var declarationFile = await _metadataAsSourceFileService.GetGeneratedFileAsync(document.Project, symbol, false, cancellationToken).ConfigureAwait(keepThreadContext); var linePosSpan = declarationFile.IdentifierLocation.GetLineSpan().Span; locations.Add(new LSP.Location diff --git a/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs index 6e6b22b55a9027d51a87562aaf2da7b10647fa33..cf7d13109117b73e3ee093e8708f63ab1b3d1718 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Definitions/GoToTypeDefinitionHandler.cs @@ -18,9 +18,9 @@ public GoToTypeDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFi } public async Task HandleRequestAsync(Solution solution, LSP.TextDocumentPositionParams request, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext) { - return await GetDefinitionAsync(solution, request, true, cancellationToken).ConfigureAwait(false); + return await GetDefinitionAsync(solution, request, typeOnly: true, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext); } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/ExportLspMethodAttribute.cs b/src/Features/LanguageServer/Protocol/Handler/ExportLspMethodAttribute.cs index 7acf17b8b687fec53c4313704267a69574b46e5d..cf4eef111f2d3536a0753ad7594cdffd1f5ff874 100644 --- a/src/Features/LanguageServer/Protocol/Handler/ExportLspMethodAttribute.cs +++ b/src/Features/LanguageServer/Protocol/Handler/ExportLspMethodAttribute.cs @@ -15,6 +15,11 @@ internal class ExportLspMethodAttribute : ExportAttribute, IRequestHandlerMetada public ExportLspMethodAttribute(string methodName) : base(typeof(IRequestHandler)) { + if (string.IsNullOrEmpty(methodName)) + { + throw new ArgumentException(nameof(methodName)); + } + MethodName = methodName; } } diff --git a/src/Features/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs b/src/Features/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs index 23222ad3bc4919ee3532440b1739c008c39bb0b6..9c8429c7d0af8ec5942314ff7da4566655648130 100644 --- a/src/Features/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Composition; using System.Threading; @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class FoldingRangesHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, FoldingRangeParams request, - ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var foldingRanges = ArrayBuilder.GetInstance(); @@ -30,13 +30,13 @@ internal class FoldingRangesHandler : IRequestHandler { - public async Task HandleRequestAsync(Solution solution, LSP.DocumentFormattingParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + public async Task HandleRequestAsync(Solution solution, LSP.DocumentFormattingParams request, LSP.ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) { - return await GetTextEdits(solution, request.TextDocument.Uri, cancellationToken).ConfigureAwait(false); + return await GetTextEdits(solution, request.TextDocument.Uri, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext); } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentHandlerBase.cs b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentHandlerBase.cs index fda38125b46167725881598ccad9288a482efa40..89ec543f12111a559d3bc95c057022c03790c2ba 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentHandlerBase.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentHandlerBase.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler { internal class FormatDocumentHandlerBase { - protected async Task GetTextEdits(Solution solution, Uri documentUri, CancellationToken cancellationToken, LSP.Range range = null) + protected async Task GetTextEdits(Solution solution, Uri documentUri, bool keepThreadContext, CancellationToken cancellationToken, LSP.Range range = null) { var edits = new ArrayBuilder(); var document = solution.GetDocumentFromURI(documentUri); @@ -23,14 +23,14 @@ protected async Task GetTextEdits(Solution solution, Uri documen if (document != null) { var formattingService = document.Project.LanguageServices.GetService(); - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); TextSpan? textSpan = null; if (range != null) { textSpan = ProtocolConversions.RangeToTextSpan(range, text); } - var textChanges = await formattingService.GetFormattingChangesAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + var textChanges = await formattingService.GetFormattingChangesAsync(document, textSpan, cancellationToken).ConfigureAwait(keepThreadContext); edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs index dd774b9428c1d234c11af37c52f31e96335008d7..74ba030f10d306ad8965e3667b0c8ef533f7c2c4 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using System.Composition; @@ -17,14 +17,15 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler [ExportLspMethod(Methods.TextDocumentOnTypeFormattingName)] internal class FormatDocumentOnTypeHandler : IRequestHandler { - public async Task HandleRequestAsync(Solution solution, DocumentOnTypeFormattingParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + public async Task HandleRequestAsync(Solution solution, DocumentOnTypeFormattingParams request, ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) { var edits = new ArrayBuilder(); var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document != null) { var formattingService = document.Project.LanguageServices.GetService(); - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); if (string.IsNullOrEmpty(request.Character)) { @@ -34,14 +35,14 @@ public async Task HandleRequestAsync(Solution solution, DocumentOnTy IList textChanges; if (SyntaxFacts.IsNewLine(request.Character[0])) { - textChanges = await formattingService.GetFormattingChangesOnReturnAsync(document, position, cancellationToken).ConfigureAwait(false); + textChanges = await formattingService.GetFormattingChangesOnReturnAsync(document, position, cancellationToken).ConfigureAwait(keepThreadContext); } else { - textChanges = await formattingService.GetFormattingChangesAsync(document, request.Character[0], position, cancellationToken).ConfigureAwait(false); + textChanges = await formattingService.GetFormattingChangesAsync(document, request.Character[0], position, cancellationToken).ConfigureAwait(keepThreadContext); } - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); if (textChanges != null) { edits.AddRange(textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, text))); diff --git a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentRangeHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentRangeHandler.cs index c5ad891d49495bafe8595df63a6f3a27d3132b58..ed5b80a4154d7738399bc2b723bb94c8fe04dc4c 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentRangeHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentRangeHandler.cs @@ -11,9 +11,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler [ExportLspMethod(Methods.TextDocumentRangeFormattingName)] internal class FormatDocumentRangeHandler : FormatDocumentHandlerBase, IRequestHandler { - public async Task HandleRequestAsync(Solution solution, DocumentRangeFormattingParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + public async Task HandleRequestAsync(Solution solution, DocumentRangeFormattingParams request, ClientCapabilities clientCapabilities, + CancellationToken cancellationToken, bool keepThreadContext = false) { - return await GetTextEdits(solution, request.TextDocument.Uri, cancellationToken, range: request.Range).ConfigureAwait(false); + return await GetTextEdits(solution, request.TextDocument.Uri, keepThreadContext, cancellationToken, range: request.Range).ConfigureAwait(keepThreadContext); } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs index 7b43cb2dc5e9252d6462757553e17e8307190ce8..f99ac8aec66e9b008e91d079372d0e60d82147ce 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Immutable; @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class DocumentHighlightsHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, TextDocumentPositionParams request, - ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext) { var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document == null) @@ -25,19 +25,19 @@ internal class DocumentHighlightsHandler : IRequestHandler(); - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); var highlights = await documentHighlightService.GetDocumentHighlightsAsync( document, position, ImmutableHashSet.Create(document), - cancellationToken).ConfigureAwait(false); + cancellationToken).ConfigureAwait(keepThreadContext); if (!highlights.IsDefaultOrEmpty) { // LSP requests are only for a single document. So just get the highlights for the requested document. var highlightsForDocument = highlights.FirstOrDefault(h => h.Document.Id == document.Id); - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); return highlightsForDocument.HighlightSpans.Select(h => new DocumentHighlight { diff --git a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs index cda877b7c17f42afb853341d2c04a803f7b8bc95..cffe9170d785b456e6374586868660b327053fbd 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Hover/HoverHandler.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class HoverHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, TextDocumentPositionParams request, - ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document == null) @@ -23,16 +23,16 @@ internal class HoverHandler : IRequestHandler return null; } - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); var quickInfoService = document.Project.LanguageServices.GetService(); - var info = await quickInfoService.GetQuickInfoAsync(document, position, cancellationToken).ConfigureAwait(false); + var info = await quickInfoService.GetQuickInfoAsync(document, position, cancellationToken).ConfigureAwait(keepThreadContext); if (info == null) { return null; } - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); return new Hover { Range = ProtocolConversions.TextSpanToRange(info.Span, text), diff --git a/src/Features/LanguageServer/Protocol/Handler/IRequestHandler.cs b/src/Features/LanguageServer/Protocol/Handler/IRequestHandler.cs index 07d822e5463e5a73466ff4412a92581fecdd7d8a..84846ac1434ec6599ccbe94e65683e3df0d28107 100644 --- a/src/Features/LanguageServer/Protocol/Handler/IRequestHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/IRequestHandler.cs @@ -15,6 +15,15 @@ internal interface IRequestHandler internal interface IRequestHandler : IRequestHandler { - Task HandleRequestAsync(Solution solution, RequestType request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken); + /// + /// Handles an LSP request. + /// + /// the solution to apply the request to. + /// the lsp request. + /// the client capabilities for the request. + /// a cancellation token. + /// a value to set if the threading context in the handler should be kept from the caller. + /// the lps response. + Task HandleRequestAsync(Solution solution, RequestType request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Initialize/InitializeHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Initialize/InitializeHandler.cs index 07aba33d2a82b2be461fd88bc8ebe7f23eeab231..9dace61eeecd03cc78f93467bc3e8605805622c5 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Initialize/InitializeHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Initialize/InitializeHandler.cs @@ -33,7 +33,7 @@ internal class InitializeHandler : IRequestHandler HandleRequestAsync(Solution solution, InitializeParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + public Task HandleRequestAsync(Solution solution, InitializeParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) => Task.FromResult(s_initializeResult); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs index 1454a103e18278a84d8d917bc9b033d1e3b58e76..7b3e9ac7967ac2d251613014844c6ce36b2788b1 100644 --- a/src/Features/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class FindImplementationsHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, LSP.TextDocumentPositionParams request, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var locations = ArrayBuilder.GetInstance(); @@ -26,11 +26,11 @@ internal class FindImplementationsHandler : IRequestHandler(); - var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(keepThreadContext); var context = new SimpleFindUsagesContext(cancellationToken); - await findUsagesService.FindImplementationsAsync(document, position, context).ConfigureAwait(false); + await findUsagesService.FindImplementationsAsync(document, position, context).ConfigureAwait(keepThreadContext); foreach (var definition in context.GetDefinitions()) { @@ -39,11 +39,11 @@ internal class FindImplementationsHandler : IRequestHandler HandleRequestAsync(Solution solution, LSP.TextDocumentPositionParams request, - LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document == null) @@ -36,14 +36,14 @@ public SignatureHelpHandler([ImportMany] IEnumerable p.Metadata.Language == document.Project.Language); var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.InvokeSignatureHelpCommand); foreach (var provider in providers) { - var items = await provider.Value.GetItemsAsync(document, position, triggerInfo, cancellationToken).ConfigureAwait(false); + var items = await provider.Value.GetItemsAsync(document, position, triggerInfo, cancellationToken).ConfigureAwait(keepThreadContext); if (items != null) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs index 4a49da1ec7f5a3f32f569078b466867617c9b8cf..ed12d2e70f9aac6b117035f2c94d5dc4a9c654df 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Symbols/DocumentSymbolsHandler.cs @@ -21,7 +21,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class DocumentSymbolsHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, DocumentSymbolParams request, - ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { var document = solution.GetDocumentFromURI(request.TextDocument.Uri); if (document == null) @@ -32,15 +32,15 @@ internal class DocumentSymbolsHandler : IRequestHandler.GetInstance(); var navBarService = document.Project.LanguageServices.GetService(); - var navBarItems = await navBarService.GetItemsAsync(document, cancellationToken).ConfigureAwait(false); + var navBarItems = await navBarService.GetItemsAsync(document, cancellationToken).ConfigureAwait(keepThreadContext); if (navBarItems.Count == 0) { return symbols.ToArrayAndFree(); } - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(keepThreadContext); + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(keepThreadContext); + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(keepThreadContext); // TODO - Return more than 2 levels of symbols. // https://github.com/dotnet/roslyn/projects/45#card-20033869 @@ -49,7 +49,7 @@ internal class DocumentSymbolsHandler : IRequestHandler private static async Task GetDocumentSymbolAsync(NavigationBarItem item, Compilation compilation, SyntaxTree tree, - SourceText text, CancellationToken cancellationToken) + SourceText text, bool keepThreadContext, CancellationToken cancellationToken) { // it is actually symbol location getter. but anyway. var location = GetLocation(item, compilation, tree, cancellationToken); @@ -119,7 +119,7 @@ static SymbolInformation Create(NavigationBarItem item, TextSpan span, string co return null; } - var symbol = await GetSymbolAsync(location, compilation, cancellationToken).ConfigureAwait(false); + var symbol = await GetSymbolAsync(location, compilation, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext); if (symbol == null) { return null; @@ -133,25 +133,25 @@ static SymbolInformation Create(NavigationBarItem item, TextSpan span, string co Deprecated = symbol.GetAttributes().Any(x => x.AttributeClass.MetadataName == "ObsoleteAttribute"), Range = ProtocolConversions.TextSpanToRange(item.Spans.First(), text), SelectionRange = ProtocolConversions.TextSpanToRange(location.SourceSpan, text), - Children = await GetChildrenAsync(item.ChildItems, compilation, tree, text, cancellationToken).ConfigureAwait(false), + Children = await GetChildrenAsync(item.ChildItems, compilation, tree, text, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext), }; static async Task GetChildrenAsync(IEnumerable items, Compilation compilation, SyntaxTree tree, - SourceText text, CancellationToken cancellationToken) + SourceText text, bool keepThreadContext, CancellationToken cancellationToken) { var list = new ArrayBuilder(); foreach (var item in items) { - list.Add(await GetDocumentSymbolAsync(item, compilation, tree, text, cancellationToken).ConfigureAwait(false)); + list.Add(await GetDocumentSymbolAsync(item, compilation, tree, text, keepThreadContext, cancellationToken).ConfigureAwait(keepThreadContext)); } return list.ToArrayAndFree(); } - static async Task GetSymbolAsync(Location location, Compilation compilation, CancellationToken cancellationToken) + static async Task GetSymbolAsync(Location location, Compilation compilation, bool keepThreadContext, CancellationToken cancellationToken) { var model = compilation.GetSemanticModel(location.SourceTree); - var root = await model.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var root = await model.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(keepThreadContext); var node = root.FindNode(location.SourceSpan); while (node != null) diff --git a/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs index 7c9a58402478f98c582451cad6689d0ebf11f6e0..f8309832782a8f7364569531ef48196ab5ca732a 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs @@ -15,13 +15,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler internal class WorkspaceSymbolsHandler : IRequestHandler { public async Task HandleRequestAsync(Solution solution, WorkspaceSymbolParams request, - ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + ClientCapabilities clientCapabilities, CancellationToken cancellationToken, bool keepThreadContext = false) { - var searchTasks = Task.WhenAll(solution.Projects.Select(project => SearchProjectAsync(project, request, cancellationToken))); - return (await searchTasks.ConfigureAwait(false)).SelectMany(s => s).ToArray(); + var searchTasks = Task.WhenAll(solution.Projects.Select(project => SearchProjectAsync(project, request, keepThreadContext, cancellationToken))); + return (await searchTasks.ConfigureAwait(keepThreadContext)).SelectMany(s => s).ToArray(); // local functions - static async Task> SearchProjectAsync(Project project, WorkspaceSymbolParams request, CancellationToken cancellationToken) + static async Task> SearchProjectAsync(Project project, WorkspaceSymbolParams request, bool keepThreadContext, CancellationToken cancellationToken) { var searchService = project.LanguageServices.GetService(); if (searchService != null) @@ -33,20 +33,20 @@ static async Task> SearchProjectAsync(Project ImmutableArray.Empty, request.Query, searchService.KindsProvided, - cancellationToken).ConfigureAwait(false); - var projectSymbolsTasks = Task.WhenAll(items.Select(item => CreateSymbolInformation(item, cancellationToken))); - return (await projectSymbolsTasks.ConfigureAwait(false)).ToImmutableArray(); + cancellationToken).ConfigureAwait(keepThreadContext); + var projectSymbolsTasks = Task.WhenAll(items.Select(item => CreateSymbolInformation(item, keepThreadContext, cancellationToken))); + return (await projectSymbolsTasks.ConfigureAwait(keepThreadContext)).ToImmutableArray(); } return ImmutableArray.Create(); - static async Task CreateSymbolInformation(INavigateToSearchResult result, CancellationToken cancellationToken) + static async Task CreateSymbolInformation(INavigateToSearchResult result, bool keepThreadContext, CancellationToken cancellationToken) { return new SymbolInformation { Name = result.Name, Kind = ProtocolConversions.NavigateToKindToSymbolKind(result.Kind), - Location = await ProtocolConversions.TextSpanToLocationAsync(result.NavigableItem.Document, result.NavigableItem.SourceSpan, cancellationToken).ConfigureAwait(false), + Location = await ProtocolConversions.TextSpanToLocationAsync(result.NavigableItem.Document, result.NavigableItem.SourceSpan, cancellationToken).ConfigureAwait(keepThreadContext), }; } } diff --git a/src/Features/LanguageServer/Protocol/LanguageServerProtocol.cs b/src/Features/LanguageServer/Protocol/LanguageServerProtocol.cs index 8ec734680ff1857ab3c99f124c4871ce4a16b3db..c309b6f28dfe098568f263315f3a06e847363e86 100644 --- a/src/Features/LanguageServer/Protocol/LanguageServerProtocol.cs +++ b/src/Features/LanguageServer/Protocol/LanguageServerProtocol.cs @@ -55,12 +55,25 @@ public LanguageServerProtocol([ImportMany] IEnumerable + /// Answers an execute workspace command request by handling the specified command name. + /// https://microsoft.github.io/language-server-protocol/specification#workspace_executeCommand + /// + /// the solution relevant to the workspace. + /// the request command name and arguments. + /// the client capabilities for the request. + /// a cancellation token. + /// any or null. + public Task ExecuteWorkspaceCommandAsync(Solution solution, LSP.ExecuteCommandParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + => ExecuteRequestAsync(LSP.Methods.WorkspaceExecuteCommandName, solution, request, clientCapabilities, cancellationToken); + /// /// Answers an implementation request by returning the implementation location(s) of a given symbol. /// https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation /// /// the solution containing the request document. /// the request document symbol location. + /// the client capabilities for the request. /// a cancellation token. /// the location(s) of the implementations of the symbol. public Task FindImplementationsAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -72,6 +85,7 @@ public Task FindImplementationsAsync(Solution solution, LSP.TextDocument /// /// the solution containing the document. /// the request document and formatting options. + /// the client capabilities for the request. /// a cancellation token. /// the text edits describing the document modifications. public Task FormatDocumentAsync(Solution solution, LSP.DocumentFormattingParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -83,6 +97,7 @@ public Task FormatDocumentAsync(Solution solution, LSP.DocumentF /// /// the solution containing the document. /// the request document, formatting options, and typing information. + /// the client capabilities for the request. /// a cancellation token. /// the text edits describing the document modifications. public Task FormatDocumentOnTypeAsync(Solution solution, LSP.DocumentOnTypeFormattingParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -94,6 +109,7 @@ public Task FormatDocumentOnTypeAsync(Solution solution, LSP.Doc /// /// the solution containing the document. /// the request document, formatting options, and range to format. + /// the client capabilities for the request. /// a cancellation token. /// the text edits describing the document modifications. public Task FormatDocumentRangeAsync(Solution solution, LSP.DocumentRangeFormattingParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -105,6 +121,7 @@ public Task FormatDocumentRangeAsync(Solution solution, LSP.Docu /// /// the solution containing the document. /// the document and range to get code actions for. + /// the client capabilities for the request. /// a cancellation token. /// a list of commands representing code actions. public Task GetCodeActionsAsync(Solution solution, LSP.CodeActionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -116,6 +133,7 @@ public Task GetCodeActionsAsync(Solution solution, LSP.CodeActionParam /// /// the solution containing the document. /// the document position and completion context. + /// the client capabilities for the request. /// a cancellation token. /// a list of completions. public Task GetCompletionsAsync(Solution solution, LSP.CompletionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -127,6 +145,7 @@ public Task GetCompletionsAsync(Solution solution, LSP.CompletionParams /// /// the solution containing the request document. /// the request document location. + /// the client capabilities for the request. /// a cancellation token. /// the highlights in the document for the given document location. public Task GetDocumentHighlightAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -138,6 +157,7 @@ public Task GetDocumentHighlightAsync(Solution solution /// /// the solution containing the document. /// the document to get symbols from. + /// the client capabilities for the request. /// a cancellation token. /// a list of symbols in the document. public Task GetDocumentSymbolsAsync(Solution solution, LSP.DocumentSymbolParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -149,6 +169,7 @@ public Task GetDocumentSymbolsAsync(Solution solution, LSP.DocumentSym /// /// the solution containing the document. /// the request document. + /// the client capabilities for the request. /// a cancellation token. /// a list of folding ranges in the document. public Task GetFoldingRangeAsync(Solution solution, LSP.FoldingRangeParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -160,6 +181,7 @@ public Task GetFoldingRangeAsync(Solution solution, LSP.Fold /// /// the solution containing any documents in the request. /// the hover requesst. + /// the client capabilities for the request. /// a cancellation token. /// the Hover using MarkupContent. public Task GetHoverAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -171,6 +193,7 @@ public Task GetHoverAsync(Solution solution, LSP.TextDocumentPosition /// /// the solution containing the document. /// the request document position. + /// the client capabilities for the request. /// a cancellation token. /// the signature help at a given location. public Task GetSignatureHelpAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -182,6 +205,7 @@ public Task GetSignatureHelpAsync(Solution solution, LSP.Text /// /// the current solution. /// the workspace request with the query to invoke. + /// the client capabilities for the request. /// a cancellation token. /// a list of symbols in the workspace. public Task GetWorkspaceSymbolsAsync(Solution solution, LSP.WorkspaceSymbolParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -193,6 +217,7 @@ public Task GetWorkspaceSymbolsAsync(Solution solution, /// /// the solution containing the request. /// the document position of the symbol to go to. + /// the client capabilities for the request. /// a cancellation token. /// the location(s) of a given symbol. public Task GoToDefinitionAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -204,6 +229,7 @@ public Task GoToDefinitionAsync(Solution solution, LSP.TextDocumentPosit /// /// the solution containing the request. /// the document position of the type to go to. + /// the client capabilities for the request. /// a cancellation token. /// the location of a type definition. public Task GoToTypeDefinitionAsync(Solution solution, LSP.TextDocumentPositionParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -215,6 +241,7 @@ public Task GoToTypeDefinitionAsync(Solution solution, LSP.TextD /// /// the solution containing the document. /// the initialize parameters. + /// the client capabilities for the request. /// a cancellation token. /// the server cababilities. public Task InitializeAsync(Solution solution, LSP.InitializeParams request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) @@ -226,6 +253,7 @@ public Task InitializeAsync(Solution solution, LSP.Initial /// /// the solution containing the document. /// the completion item to resolve. + /// the client capabilities for the request. /// a cancellation token. /// a resolved completion item. public Task ResolveCompletionItemAsync(Solution solution, LSP.CompletionItem request, LSP.ClientCapabilities clientCapabilities, CancellationToken cancellationToken) diff --git a/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs index e7c71362da7481d6829edc4bead05a7eb53addcf..072058a9f5a149773c53ffbf1b010453245f7b98 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/CodeActionsTests.cs @@ -9,7 +9,7 @@ using Xunit; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions { public class CodeActionsTests : AbstractLanguageServerProtocolTests { @@ -28,13 +28,16 @@ void M() var expected = CreateCommand(CSharpFeaturesResources.Use_implicit_type, locations["caret"].Single()); var clientCapabilities = CreateClientCapabilitiesWithExperimentalValue("supportsWorkspaceEdits", JToken.FromObject(false)); - var results = await RunCodeActionsAsync(solution, locations["caret"].Single(), clientCapabilities); + var results = await RunGetCodeActionsAsync(solution, locations["caret"].Single(), clientCapabilities); var useImplicitTypeResult = results.Single(r => r.Title == CSharpFeaturesResources.Use_implicit_type); AssertJsonEquals(expected, useImplicitTypeResult); } - private static async Task RunCodeActionsAsync(Solution solution, LSP.Location caret, LSP.ClientCapabilities clientCapabilities = null) - => (LSP.Command[])await GetLanguageServer(solution).GetCodeActionsAsync(solution, CreateCodeActionParams(caret), clientCapabilities, CancellationToken.None); + private static async Task RunGetCodeActionsAsync(Solution solution, LSP.Location caret, LSP.ClientCapabilities clientCapabilities = null) + { + var results = await GetLanguageServer(solution).GetCodeActionsAsync(solution, CreateCodeActionParams(caret), clientCapabilities, CancellationToken.None); + return results.Select(r => (LSP.Command)r).ToArray(); + } private static LSP.ClientCapabilities CreateClientCapabilitiesWithExperimentalValue(string experimentalProperty, JToken value) => new LSP.ClientCapabilities() @@ -60,18 +63,10 @@ private static LSP.Command CreateCommand(string title, LSP.Location location) => new LSP.Command() { Title = title, - CommandIdentifier = "_liveshare.remotecommand.Roslyn", + CommandIdentifier = "Roslyn.RunCodeAction", Arguments = new object[] { - new LSP.Command() - { - Title = title, - CommandIdentifier = "Roslyn.RunCodeAction", - Arguments = new object[] - { - CreateRunCodeActionParams(title, location) - } - } + CreateRunCodeActionParams(title, location) } }; } diff --git a/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/RunCodeActionsTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/RunCodeActionsTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..a8f4e92099c24fed402f2c732dc72b336cb60e98 --- /dev/null +++ b/src/Features/LanguageServer/ProtocolUnitTests/CodeActions/RunCodeActionsTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; +using Newtonsoft.Json.Linq; +using Roslyn.Test.Utilities; +using Xunit; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CodeActions +{ + public class RunCodeActionsTests : AbstractLanguageServerProtocolTests + { + [WpfFact] + public async Task TestRunCodeActionsAsync() + { + var markup = +@"class A +{ + void M() + { + {|caret:|}int i = 1; + } +}"; + var (solution, ranges) = CreateTestSolution(markup); + var codeActionLocation = ranges["caret"].First(); + + var results = await RunExecuteWorkspaceCommand(solution, codeActionLocation, CSharpFeaturesResources.Use_implicit_type); + Assert.True(results); + } + + private static async Task RunExecuteWorkspaceCommand(Solution solution, LSP.Location caret, string title, LSP.ClientCapabilities clientCapabilities = null) + => (bool)await GetLanguageServer(solution).ExecuteWorkspaceCommandAsync(solution, CreateExecuteCommandParams(caret, title), + clientCapabilities, CancellationToken.None); + + private static LSP.ExecuteCommandParams CreateExecuteCommandParams(LSP.Location location, string title) + => new LSP.ExecuteCommandParams() + { + Command = "Roslyn.RunCodeAction", + Arguments = new object[] + { + JObject.FromObject(CreateRunCodeActionParams(title, location)) + }, + }; + } +} diff --git a/src/VisualStudio/LiveShare/Impl/PreviewCodeActionsHandler.cs b/src/VisualStudio/LiveShare/Impl/PreviewCodeActionsHandler.cs index c019e5ed5d28e168363f7b4b676bda0457b8096e..55b1c55c335abe538dcb0d978be6d38dc09f91c4 100644 --- a/src/VisualStudio/LiveShare/Impl/PreviewCodeActionsHandler.cs +++ b/src/VisualStudio/LiveShare/Impl/PreviewCodeActionsHandler.cs @@ -30,6 +30,7 @@ public async Task HandleAsync(RunCodeActionParams request, Reque var codeActions = await GetCodeActionsAsync(solution, request.CodeActionParams.TextDocument.Uri, request.CodeActionParams.Range, + keepThreadContext: false, cancellationToken).ConfigureAwait(false); var actionToRun = codeActions?.FirstOrDefault(a => a.Title == request.Title); diff --git a/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.Exports.cs b/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.Exports.cs deleted file mode 100644 index 7183305f24b6d642f27a3080ff0e6267f31da96a..0000000000000000000000000000000000000000 --- a/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.Exports.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CodeRefactorings; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.LiveShare.LanguageServices; - -namespace Microsoft.VisualStudio.LanguageServices.LiveShare -{ - [ExportLspRequestHandler(LiveShareConstants.RoslynContractName, Methods.WorkspaceExecuteCommandName)] - [Obsolete("Used for backwards compatibility with old liveshare clients.")] - internal class RoslynRunCodeActionsHandler : RunCodeActionsHandler - { - [ImportingConstructor] - public RoslynRunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, IThreadingContext threadingContext) - : base(codeFixService, codeRefactoringService, threadingContext) - { - } - } - - [ExportLspRequestHandler(LiveShareConstants.CSharpContractName, Methods.WorkspaceExecuteCommandName)] - internal class CSharpRunCodeActionsHandler : RunCodeActionsHandler - { - [ImportingConstructor] - public CSharpRunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, IThreadingContext threadingContext) - : base(codeFixService, codeRefactoringService, threadingContext) - { - } - } - - [ExportLspRequestHandler(LiveShareConstants.VisualBasicContractName, Methods.WorkspaceExecuteCommandName)] - internal class VisualBasicRunCodeActionsHandler : RunCodeActionsHandler - { - [ImportingConstructor] - public VisualBasicRunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, IThreadingContext threadingContext) - : base(codeFixService, codeRefactoringService, threadingContext) - { - } - } - - [ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, Methods.WorkspaceExecuteCommandName)] - internal class TypeScriptRunCodeActionsHandler : RunCodeActionsHandler - { - [ImportingConstructor] - public TypeScriptRunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, IThreadingContext threadingContext) - : base(codeFixService, codeRefactoringService, threadingContext) - { - } - } -} diff --git a/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.cs b/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.cs deleted file mode 100644 index 2dae0293496cf1c70feffead167c2954d1f88e3b..0000000000000000000000000000000000000000 --- a/src/VisualStudio/LiveShare/Impl/RunCodeActionsHandler.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -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; -using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; -using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.VisualStudio.LiveShare.LanguageServices; -using Newtonsoft.Json.Linq; -using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.VisualStudio.LanguageServices.LiveShare -{ - /// - /// Run code actions handler. Called when lightbulb invoked. - /// TODO - Move to CodeAnalysis.LanguageServer once the UI thread dependency is removed. - /// - internal class RunCodeActionsHandler : CodeActionsHandlerBase, ILspRequestHandler - { - private readonly IThreadingContext _threadingContext; - - public RunCodeActionsHandler(ICodeFixService codeFixService, ICodeRefactoringService codeRefactoringService, IThreadingContext threadingContext) - : base(codeFixService, codeRefactoringService) - { - _threadingContext = threadingContext; - } - - public async Task HandleAsync(LSP.ExecuteCommandParams request, RequestContext requestContext, CancellationToken cancellationToken) - { - var solution = requestContext.Context; - if (request.Command.StartsWith(RemoteCommandNamePrefix)) - { - var command = ((JObject)request.Arguments[0]).ToObject(); - request.Command = command.CommandIdentifier; - request.Arguments = command.Arguments; - } - if (request.Command == RunCodeActionCommandName) - { - var runRequest = ((JToken)request.Arguments.Single()).ToObject(); - var codeActions = await GetCodeActionsAsync(solution, - runRequest.CodeActionParams.TextDocument.Uri, - runRequest.CodeActionParams.Range, - cancellationToken).ConfigureAwait(false); - - var actionToRun = codeActions?.FirstOrDefault(a => a.Title == runRequest.Title); - - if (actionToRun != null) - { - // CodeAction Operation's Apply methods needs to be called on the UI thread. - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - foreach (var operation in await actionToRun.GetOperationsAsync(cancellationToken).ConfigureAwait(true)) - { - operation.Apply(solution.Workspace, cancellationToken); - } - } - } - else - { - // TODO - Modify to our telemetry. - } - - // Return true even in the case that we didn't run the command. Returning false would prompt the guest to tell the host to - // enable command executuion, which wouldn't solve their problem. - return true; - } - } -} diff --git a/src/VisualStudio/LiveShare/Impl/Shims/AbstractLiveShareHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/AbstractLiveShareHandlerShim.cs index 50c43ee296bf3db77b23f99c042baced468566ab..bdf8fbca046eb77de8749f42c54009378a22c9e8 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/AbstractLiveShareHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/AbstractLiveShareHandlerShim.cs @@ -29,6 +29,15 @@ public virtual Task HandleAsync(RequestType param, RequestContext< return ((IRequestHandler)LazyRequestHandler.Value).HandleRequestAsync(requestContext.Context, param, requestContext.ClientCapabilities?.ToObject(), cancellationToken); } + /// + /// Certain implementations require that the processing be done on the UI thread. + /// So allow the handler to specifiy that the thread context should be preserved. + /// + protected Task HandleAsyncPreserveThreadContext(RequestType param, RequestContext requestContext, CancellationToken cancellationToken) + { + return ((IRequestHandler)LazyRequestHandler.Value).HandleRequestAsync(requestContext.Context, param, requestContext.ClientCapabilities?.ToObject(), cancellationToken, keepThreadContext: true); + } + protected Lazy GetRequestHandler(IEnumerable> requestHandlers, string methodName) { return requestHandlers.First(handler => handler.Metadata.MethodName == methodName); diff --git a/src/VisualStudio/LiveShare/Impl/Shims/CodeActionsHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/CodeActionsHandlerShim.cs index f4d2511ed64717254f04c425901d4ad78c32fd19..b9b3fde7d79e524d28cd8b0c98fa4a7910aa8460 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/CodeActionsHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/CodeActionsHandlerShim.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.LiveShare.LanguageServices; @@ -15,14 +15,46 @@ namespace Microsoft.VisualStudio.LanguageServices.LiveShare { internal class CodeActionsHandlerShim : AbstractLiveShareHandlerShim { + public const string RemoteCommandNamePrefix = "_liveshare.remotecommand"; + protected const string ProviderName = "Roslyn"; + public CodeActionsHandlerShim(IEnumerable> requestHandlers) : base(requestHandlers, Methods.TextDocumentCodeActionName) { } + /// + /// Handle a by delegating to the base LSP implementation + /// from . + /// + /// We need to return a command that is a generic wrapper that VS Code can execute. + /// The argument to this wrapper will either be a RunCodeAction command which will carry + /// enough information to run the command or a CodeAction with the edits. + /// There are server and client side dependencies on this shape in liveshare. + /// + /// + /// + /// + /// public async override Task HandleAsync(CodeActionParams param, RequestContext requestContext, CancellationToken cancellationToken) { - return await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + var result = await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + + var commands = new ArrayBuilder(); + foreach (var resultObj in result) + { + var commandArguments = resultObj; + var title = resultObj is CodeAction codeAction ? codeAction.Title : ((Command)resultObj).Title; + commands.Add(new Command + { + Title = title, + // Overwrite the command identifier to match the expected liveshare remote command identifier. + CommandIdentifier = $"{RemoteCommandNamePrefix}.{ProviderName}", + Arguments = new object[] { commandArguments } + }); + } + + return commands.ToArrayAndFree(); } } diff --git a/src/VisualStudio/LiveShare/Impl/Shims/FindImplementationsHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/FindImplementationsHandlerShim.cs index 9db55e70959fb48933663cdca9e253aa78c4045a..ec1b795b80fdfcc718a698ab470e71ceed037333 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/FindImplementationsHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/FindImplementationsHandlerShim.cs @@ -27,7 +27,7 @@ public override async Task HandleAsync(TextDocumentPositionParams param, { // TypeScript requires this call to be on the UI thread. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + return await base.HandleAsyncPreserveThreadContext(param, requestContext, cancellationToken).ConfigureAwait(false); } } diff --git a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentHandlerShim.cs index afd3524411ab7cb1196ecc08626716f32b0a5923..ea93503dbf4b661b71851005d86190ed79d5e753 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentHandlerShim.cs @@ -27,7 +27,7 @@ public override async Task HandleAsync(DocumentFormattingParams para { // To get the formatting options, TypeScript expects to be called on the UI thread. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + return await base.HandleAsyncPreserveThreadContext(param, requestContext, cancellationToken).ConfigureAwait(false); } } diff --git a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentOnTypeHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentOnTypeHandlerShim.cs index 9f002733211c3b5f411e8fefa5dc24b07c263ef7..8519d926032dc9636b585732b09dfd0fe004d427 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentOnTypeHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentOnTypeHandlerShim.cs @@ -27,7 +27,7 @@ public override async Task HandleAsync(DocumentOnTypeFormattingParam { // To get the formatting options, TypeScript expects to be called on the UI thread. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + return await base.HandleAsyncPreserveThreadContext(param, requestContext, cancellationToken).ConfigureAwait(false); } } diff --git a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentRangeHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentRangeHandlerShim.cs index f563e3387e9dd065903ab88ac69d483ed1944167..5133da276884c24cdd0b9199c2e9b986b0f3af17 100644 --- a/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentRangeHandlerShim.cs +++ b/src/VisualStudio/LiveShare/Impl/Shims/FormatDocumentRangeHandlerShim.cs @@ -27,7 +27,7 @@ public override async Task HandleAsync(DocumentRangeFormattingParams { // To get the formatting options, TypeScript expects to be called on the UI thread. await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - return await base.HandleAsync(param, requestContext, cancellationToken).ConfigureAwait(false); + return await base.HandleAsyncPreserveThreadContext(param, requestContext, cancellationToken).ConfigureAwait(false); } } diff --git a/src/VisualStudio/LiveShare/Impl/Shims/RunCodeActionsHandlerShim.cs b/src/VisualStudio/LiveShare/Impl/Shims/RunCodeActionsHandlerShim.cs new file mode 100644 index 0000000000000000000000000000000000000000..9437affeb858823c6d04dcd628ebe59129cc4b05 --- /dev/null +++ b/src/VisualStudio/LiveShare/Impl/Shims/RunCodeActionsHandlerShim.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.VisualStudio.LiveShare.LanguageServices; +using Newtonsoft.Json.Linq; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.LanguageServices.LiveShare +{ + /// + /// Run code actions handler. Called when lightbulb invoked. + /// + internal class RunCodeActionsHandlerShim : AbstractLiveShareHandlerShim + { + private readonly IThreadingContext _threadingContext; + + public RunCodeActionsHandlerShim(IEnumerable> requestHandlers, IThreadingContext threadingContext) + : base(requestHandlers, LSP.Methods.WorkspaceExecuteCommandName) + { + _threadingContext = threadingContext; + } + + /// + /// Unwraps the run code action request from the liveshare shape into the expected shape + /// handled by . This shape is an + /// containing as the argument. + /// + public async override Task HandleAsync(LSP.ExecuteCommandParams request, RequestContext requestContext, CancellationToken cancellationToken) + { + // Unwrap the command to get the RunCodeActions command. + if (request.Command.StartsWith(CodeActionsHandlerShim.RemoteCommandNamePrefix)) + { + var command = ((JObject)request.Arguments[0]).ToObject(); + // The CommandIdentifier should match the exported sub-handler for workspace execute command request. + request.Command = command.CommandIdentifier; + request.Arguments = command.Arguments; + } + + try + { + // Code actions must be applied from the UI thread in the VS context. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + return await base.HandleAsyncPreserveThreadContext(request, requestContext, cancellationToken).ConfigureAwait(false); + } + catch (ArgumentException ex) + { + // We may not have a handler for the command identifier passed in so the base throws. + // This is possible, especially in VSCode scenarios, so rather than blow up, log telemetry. + Logger.Log(FunctionId.Liveshare_UnknownCodeAction, KeyValueLogMessage.Create(m => m["callstack"] = ex.ToString())); + + // Return true even in the case that we didn't run the command. Returning false would prompt the guest to tell the host to + // enable command executuion, which wouldn't solve their problem. + return true; + } + } + } + + [ExportLspRequestHandler(LiveShareConstants.RoslynContractName, LSP.Methods.WorkspaceExecuteCommandName)] + [Obsolete("Used for backwards compatibility with old liveshare clients.")] + internal class RoslynRunCodeActionsHandlerShim : RunCodeActionsHandlerShim + { + [ImportingConstructor] + public RoslynRunCodeActionsHandlerShim([ImportMany] IEnumerable> requestHandlers, IThreadingContext threadingContext) : base(requestHandlers, threadingContext) + { + } + } + + [ExportLspRequestHandler(LiveShareConstants.CSharpContractName, LSP.Methods.WorkspaceExecuteCommandName)] + internal class CSharpRunCodeActionsHandlerShim : RunCodeActionsHandlerShim + { + [ImportingConstructor] + public CSharpRunCodeActionsHandlerShim([ImportMany] IEnumerable> requestHandlers, IThreadingContext threadingContext) : base(requestHandlers, threadingContext) + { + } + } + + [ExportLspRequestHandler(LiveShareConstants.VisualBasicContractName, LSP.Methods.WorkspaceExecuteCommandName)] + internal class VisualBasicRunCodeActionsHandlerShim : RunCodeActionsHandlerShim + { + [ImportingConstructor] + public VisualBasicRunCodeActionsHandlerShim([ImportMany] IEnumerable> requestHandlers, IThreadingContext threadingContext) : base(requestHandlers, threadingContext) + { + } + } + + [ExportLspRequestHandler(LiveShareConstants.TypeScriptContractName, LSP.Methods.WorkspaceExecuteCommandName)] + internal class TypeScriptRunCodeActionsHandlerShim : RunCodeActionsHandlerShim + { + [ImportingConstructor] + public TypeScriptRunCodeActionsHandlerShim([ImportMany] IEnumerable> requestHandlers, IThreadingContext threadingContext) : base(requestHandlers, threadingContext) + { + } + } +} diff --git a/src/VisualStudio/LiveShare/Test/AbstractLiveShareRequestHandlerTests.cs b/src/VisualStudio/LiveShare/Test/AbstractLiveShareRequestHandlerTests.cs index cf03c88418209cc9f1972d89f6618ce4bf7df8b3..c9db8d1a490f51ac1171f76c84dca94bdb138ee0 100644 --- a/src/VisualStudio/LiveShare/Test/AbstractLiveShareRequestHandlerTests.cs +++ b/src/VisualStudio/LiveShare/Test/AbstractLiveShareRequestHandlerTests.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.UnitTests; using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -60,11 +61,15 @@ protected override ExportProvider GetExportProvider() // Get all of the roslyn request helpers in M.CA.LanguageServer var roslynRequestHelperTypes = DesktopTestHelpers.GetAllTypesImplementingGivenInterface( typeof(RoslynHandlers.IRequestHandler).Assembly, typeof(RoslynHandlers.IRequestHandler)); + // Get all of the execute workspace command handlers in M.CA.LanguageServer + var executeCommandHandlerTypes = DesktopTestHelpers.GetAllTypesImplementingGivenInterface( + typeof(IExecuteWorkspaceCommandHandler).Assembly, typeof(IExecuteWorkspaceCommandHandler)); var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory( TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic .WithPart(typeof(MockDocumentNavigationServiceFactory)) .WithParts(liveShareRequestHelperTypes) - .WithParts(roslynRequestHelperTypes)); + .WithParts(roslynRequestHelperTypes) + .WithParts(executeCommandHandlerTypes)); return exportProviderFactory.CreateExportProvider(); } diff --git a/src/VisualStudio/LiveShare/Test/RunCodeActionsHandlerTests.cs b/src/VisualStudio/LiveShare/Test/RunCodeActionsHandlerTests.cs index 1985db5b41fbb5f4cbbb861172d9892e01af72c1..3be7bb8b3df8165b06f05ea053a88477983ac193 100644 --- a/src/VisualStudio/LiveShare/Test/RunCodeActionsHandlerTests.cs +++ b/src/VisualStudio/LiveShare/Test/RunCodeActionsHandlerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.LanguageServer.CustomProtocol; using Newtonsoft.Json.Linq; using Roslyn.Test.Utilities; @@ -26,7 +27,7 @@ void M() var (solution, ranges) = CreateTestSolution(markup); var codeActionLocation = ranges["caret"].First(); - var results = await TestHandleAsync(solution, CreateExecuteCommandParams(codeActionLocation, "Use implicit type")); + var results = await TestHandleAsync(solution, CreateExecuteCommandParams(codeActionLocation, CSharpFeaturesResources.Use_implicit_type)); Assert.True((bool)results); } diff --git a/src/Workspaces/Core/Portable/Log/FunctionId.cs b/src/Workspaces/Core/Portable/Log/FunctionId.cs index 464b1ab364be363e3f3a88e6510b6c2f2455b19a..c0b3951edce801c783fdeb2d1d704a6e97deb0af 100644 --- a/src/Workspaces/Core/Portable/Log/FunctionId.cs +++ b/src/Workspaces/Core/Portable/Log/FunctionId.cs @@ -458,5 +458,7 @@ internal enum FunctionId Intellisense_CompletionProviders_Data, SnapshotService_IsExperimentEnabledAsync, + + Liveshare_UnknownCodeAction, } }