From 0edf3bca7422a5c43a04248f35ebea5aa2221849 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Tue, 20 Feb 2024 20:23:35 -0800 Subject: [PATCH] Allow language specific handlers to use their own types for serialization --- .../AbstractInProcLanguageClient.cs | 4 + .../AbstractLanguageServerProtocolTests.cs | 37 ++- .../LanguageServer/LanguageServerHost.cs | 5 +- .../ExampleLanguageServer.cs | 3 +- .../RequestExecutionQueueTests.cs | 23 +- .../TestExampleLanguageServer.cs | 14 +- .../AbstractLanguageServer.cs | 151 ++++++--- .../IRequestExecutionQueue.cs | 2 +- .../RequestExecutionQueue.cs | 19 +- .../CSharpVisualBasicLanguageServerFactory.cs | 14 +- .../ExecuteWorkspaceCommandHandler.cs | 1 + .../Protocol/ILanguageServerFactory.cs | 2 + .../Protocol/RoslynLanguageServer.cs | 71 ++++- .../Protocol/RoslynRequestExecutionQueue.cs | 49 --- .../ProtocolUnitTests/HandlerTests.cs | 290 ++++++++++++++++++ .../VSTypeScriptHandlerTests.cs | 5 +- ...tractRazorLanguageServerFactoryWrapper.cs} | 9 +- .../RazorLanguageServerFactoryWrapper.cs | 12 +- .../RazorTest/Cohost/RazorCohostTests.cs | 10 +- .../XamlRequestExecutionQueue.cs | 19 +- 20 files changed, 574 insertions(+), 166 deletions(-) create mode 100644 src/Features/LanguageServer/ProtocolUnitTests/HandlerTests.cs rename src/Tools/ExternalAccess/Razor/{IRazorLanguageServerFactory.cs => AbstractRazorLanguageServerFactoryWrapper.cs} (70%) diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs index a93418fe75d85..91d506cc80b48 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs @@ -19,6 +19,7 @@ using Microsoft.VisualStudio.LanguageServer.Client; using Microsoft.VisualStudio.Threading; using Nerdbank.Streams; +using Newtonsoft.Json; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; @@ -213,6 +214,7 @@ internal async Task> CreateAsync> CreateAsync Create( JsonRpc jsonRpc, + JsonSerializer jsonSerializer, ICapabilitiesProvider capabilitiesProvider, WellKnownLspServerKinds serverKind, AbstractLspLogger logger, @@ -232,6 +235,7 @@ public virtual AbstractLanguageServer Create( var server = new RoslynLanguageServer( LspServiceProvider, jsonRpc, + jsonSerializer, capabilitiesProvider, logger, hostServices, diff --git a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 6684a5a8afce1..a7f1921a8adac 100644 --- a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -524,6 +524,13 @@ private static LSP.DidCloseTextDocumentParams CreateDidCloseTextDocumentParams(U } }; + internal static JsonMessageFormatter CreateJsonMessageFormatter() + { + var messageFormatter = new JsonMessageFormatter(); + LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(messageFormatter.JsonSerializer); + return messageFormatter; + } + internal sealed class TestLspServer : IAsyncDisposable { public readonly EditorTestWorkspace TestWorkspace; @@ -569,13 +576,6 @@ private void InitializeClientRpc() Assert.False(workspaceWaiter.HasPendingWork); } - private static JsonMessageFormatter CreateJsonMessageFormatter() - { - var messageFormatter = new JsonMessageFormatter(); - LSP.VSInternalExtensionUtilities.AddVSInternalExtensionConverters(messageFormatter.JsonSerializer); - return messageFormatter; - } - internal static async Task CreateAsync(EditorTestWorkspace testWorkspace, InitializationOptions initializationOptions, AbstractLspLogger logger) { var locations = await GetAnnotatedLocationsAsync(testWorkspace, testWorkspace.CurrentSolution); @@ -617,20 +617,15 @@ internal static async Task CreateAsync(EditorTestWorkspace testWo private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Stream outputStream, EditorTestWorkspace workspace, WellKnownLspServerKinds serverKind, AbstractLspLogger logger) { var capabilitiesProvider = workspace.ExportProvider.GetExportedValue(); - var servicesProvider = workspace.ExportProvider.GetExportedValue(); + var factory = workspace.ExportProvider.GetExportedValue(); - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, CreateJsonMessageFormatter())) + var jsonMessageFormatter = CreateJsonMessageFormatter(); + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, jsonMessageFormatter)) { ExceptionStrategy = ExceptionProcessing.ISerializable, }; - var languageServer = new RoslynLanguageServer( - servicesProvider, jsonRpc, - capabilitiesProvider, - logger, - workspace.Services.HostServices, - ProtocolConstants.RoslynLspLanguages, - serverKind); + var languageServer = (RoslynLanguageServer)factory.Create(jsonRpc, jsonMessageFormatter.JsonSerializer, capabilitiesProvider, serverKind, logger, workspace.Services.HostServices); jsonRpc.StartListening(); return languageServer; @@ -650,6 +645,16 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str return result; } + public Task ExecuteNotificationAsync(string methodName, RequestType request) where RequestType : class + { + return _clientRpc.NotifyWithParameterObjectAsync(methodName, request); + } + + public Task ExecuteNotification0Async(string methodName) + { + return _clientRpc.NotifyWithParameterObjectAsync(methodName); + } + public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "") { if (text == null) diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs index b3681184fb566..013a155b73eb5 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs @@ -28,7 +28,8 @@ internal sealed class LanguageServerHost public LanguageServerHost(Stream inputStream, Stream outputStream, ExportProvider exportProvider, ILogger logger) { - var handler = new HeaderDelimitedMessageHandler(outputStream, inputStream, new JsonMessageFormatter()); + var messageFormatter = new JsonMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(outputStream, inputStream, messageFormatter); // If there is a jsonrpc disconnect or server shutdown, that is handled by the AbstractLanguageServer. No need to do anything here. _jsonRpc = new JsonRpc(handler) @@ -43,7 +44,7 @@ public LanguageServerHost(Stream inputStream, Stream outputStream, ExportProvide var lspLogger = new LspServiceLogger(_logger); var hostServices = exportProvider.GetExportedValue().HostServices; - _roslynLanguageServer = roslynLspFactory.Create(_jsonRpc, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, lspLogger, hostServices); + _roslynLanguageServer = roslynLspFactory.Create(_jsonRpc, messageFormatter.JsonSerializer, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, lspLogger, hostServices); } public void Start() diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs index 5df8580127331..f39f877dbda01 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs @@ -5,6 +5,7 @@ using System; using Microsoft.CommonLanguageServerProtocol.Framework.Handlers; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; @@ -14,7 +15,7 @@ public class ExampleLanguageServer : AbstractLanguageServer? _addExtraHandlers; - public ExampleLanguageServer(JsonRpc jsonRpc, ILspLogger logger, Action? addExtraHandlers) : base(jsonRpc, logger) + public ExampleLanguageServer(JsonRpc jsonRpc, JsonSerializer jsonSerializer, ILspLogger logger, Action? addExtraHandlers) : base(jsonRpc, jsonSerializer, logger) { _addExtraHandlers = addExtraHandlers; // This spins up the queue and ensure the LSP is ready to start receiving requests diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/RequestExecutionQueueTests.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/RequestExecutionQueueTests.cs index 21adf768ace4c..1766f5eeebebc 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/RequestExecutionQueueTests.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/RequestExecutionQueueTests.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Nerdbank.Streams; +using Newtonsoft.Json; using StreamJsonRpc; using Xunit; @@ -15,7 +16,7 @@ public class RequestExecutionQueueTests { private class MockServer : AbstractLanguageServer { - public MockServer() : base(new JsonRpc(new HeaderDelimitedMessageHandler(FullDuplexStream.CreatePair().Item1)), NoOpLspLogger.Instance) + public MockServer() : base(new JsonRpc(new HeaderDelimitedMessageHandler(FullDuplexStream.CreatePair().Item1)), JsonSerializer.CreateDefault(), NoOpLspLogger.Instance) { } @@ -50,7 +51,7 @@ public async Task ExecuteAsync_ThrowCompletes() var lspServices = GetLspServices(); // Act & Assert - await Assert.ThrowsAsync(() => requestExecutionQueue.ExecuteAsync(1, ThrowingHandler.Name, lspServices, CancellationToken.None)); + await Assert.ThrowsAsync(() => requestExecutionQueue.ExecuteAsync(1, ThrowingHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None)); } [Fact] @@ -71,12 +72,12 @@ public async Task ExecuteAsync_WithCancelInProgressWork_CancelsInProgressWorkWhe var cancellingRequestCancellationToken = new CancellationToken(); var completingRequestCancellationToken = new CancellationToken(); - var _ = requestExecutionQueue.ExecuteAsync(1, CancellingHandler.Name, lspServices, cancellingRequestCancellationToken); - var _1 = requestExecutionQueue.ExecuteAsync(1, CompletingHandler.Name, lspServices, completingRequestCancellationToken); + var _ = requestExecutionQueue.ExecuteAsync(1, CancellingHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, cancellingRequestCancellationToken); + var _1 = requestExecutionQueue.ExecuteAsync(1, CompletingHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, completingRequestCancellationToken); // Act & Assert // A Debug.Assert would throw if the tasks hadn't completed when the mutating request is called. - await requestExecutionQueue.ExecuteAsync(1, MutatingHandler.Name, lspServices, CancellationToken.None); + await requestExecutionQueue.ExecuteAsync(1, MutatingHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); } } @@ -99,7 +100,7 @@ public async Task ExecuteAsync_CompletesTask() var requestExecutionQueue = GetRequestExecutionQueue(false, (TestMethodHandler.Metadata, TestMethodHandler.Instance)); var lspServices = GetLspServices(); - var response = await requestExecutionQueue.ExecuteAsync(request: 1, TestMethodHandler.Name, lspServices, CancellationToken.None); + var response = await requestExecutionQueue.ExecuteAsync(request: 1, TestMethodHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); Assert.Equal("stuff", response); } @@ -109,7 +110,7 @@ public async Task ExecuteAsync_CompletesTask_Parameterless() var requestExecutionQueue = GetRequestExecutionQueue(false, (TestParameterlessMethodHandler.Metadata, TestParameterlessMethodHandler.Instance)); var lspServices = GetLspServices(); - var response = await requestExecutionQueue.ExecuteAsync(request: NoValue.Instance, TestParameterlessMethodHandler.Name, lspServices, CancellationToken.None); + var response = await requestExecutionQueue.ExecuteAsync(request: NoValue.Instance, TestParameterlessMethodHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); Assert.True(response); } @@ -119,7 +120,7 @@ public async Task ExecuteAsync_CompletesTask_Notification() var requestExecutionQueue = GetRequestExecutionQueue(false, (TestNotificationHandler.Metadata, TestNotificationHandler.Instance)); var lspServices = GetLspServices(); - var response = await requestExecutionQueue.ExecuteAsync(request: true, TestNotificationHandler.Name, lspServices, CancellationToken.None); + var response = await requestExecutionQueue.ExecuteAsync(request: true, TestNotificationHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); Assert.Same(NoValue.Instance, response); } @@ -129,7 +130,7 @@ public async Task ExecuteAsync_CompletesTask_Notification_Parameterless() var requestExecutionQueue = GetRequestExecutionQueue(false, (TestParameterlessNotificationHandler.Metadata, TestParameterlessNotificationHandler.Instance)); var lspServices = GetLspServices(); - var response = await requestExecutionQueue.ExecuteAsync(request: NoValue.Instance, TestParameterlessNotificationHandler.Name, lspServices, CancellationToken.None); + var response = await requestExecutionQueue.ExecuteAsync(request: NoValue.Instance, TestParameterlessNotificationHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); Assert.Same(NoValue.Instance, response); } @@ -140,8 +141,8 @@ public async Task Queue_DrainsOnShutdown() var request = 1; var lspServices = GetLspServices(); - var task1 = requestExecutionQueue.ExecuteAsync(request, TestMethodHandler.Name, lspServices, CancellationToken.None); - var task2 = requestExecutionQueue.ExecuteAsync(request, TestMethodHandler.Name, lspServices, CancellationToken.None); + var task1 = requestExecutionQueue.ExecuteAsync(request, TestMethodHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); + var task2 = requestExecutionQueue.ExecuteAsync(request, TestMethodHandler.Name, LanguageServerConstants.DefaultLanguageName, lspServices, CancellationToken.None); await requestExecutionQueue.DisposeAsync(); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs index 9d3b3b8734d3c..8754366f4f30a 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs @@ -9,6 +9,7 @@ using Microsoft.CommonLanguageServerProtocol.Framework.Example; using Microsoft.Extensions.DependencyInjection; using Nerdbank.Streams; +using Newtonsoft.Json; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; @@ -18,7 +19,8 @@ internal class TestExampleLanguageServer : ExampleLanguageServer { private readonly JsonRpc _clientRpc; - public TestExampleLanguageServer(Stream clientSteam, JsonRpc jsonRpc, ILspLogger logger, Action? addExtraHandlers) : base(jsonRpc, logger, addExtraHandlers) + public TestExampleLanguageServer(Stream clientSteam, JsonRpc jsonRpc, JsonSerializer jsonSerializer, ILspLogger logger, Action? addExtraHandlers) + : base(jsonRpc, jsonSerializer, logger, addExtraHandlers) { _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientSteam, clientSteam, CreateJsonMessageFormatter())) { @@ -111,14 +113,15 @@ internal static TestExampleLanguageServer CreateBadLanguageServer(ILspLogger log { var (clientStream, serverStream) = FullDuplexStream.CreatePair(); - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverStream, serverStream, CreateJsonMessageFormatter())); + var messageFormatter = CreateJsonMessageFormatter(); + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverStream, serverStream, messageFormatter)); var extraHandlers = (IServiceCollection serviceCollection) => { serviceCollection.AddSingleton(); }; - var server = new TestExampleLanguageServer(clientStream, jsonRpc, logger, extraHandlers); + var server = new TestExampleLanguageServer(clientStream, jsonRpc, messageFormatter.JsonSerializer, logger, extraHandlers); jsonRpc.StartListening(); server.InitializeTest(); @@ -129,9 +132,10 @@ internal static TestExampleLanguageServer CreateLanguageServer(ILspLogger logger { var (clientStream, serverStream) = FullDuplexStream.CreatePair(); - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverStream, serverStream, CreateJsonMessageFormatter())); + var messageFormatter = CreateJsonMessageFormatter(); + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverStream, serverStream, messageFormatter)); - var server = new TestExampleLanguageServer(clientStream, jsonRpc, logger, addExtraHandlers: null); + var server = new TestExampleLanguageServer(clientStream, jsonRpc, messageFormatter.JsonSerializer, logger, addExtraHandlers: null); jsonRpc.StartListening(); server.InitializeTest(); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs index ad8d5077fb960..f4a5b942b49e0 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs @@ -3,10 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using StreamJsonRpc; namespace Microsoft.CommonLanguageServerProtocol.Framework; @@ -16,6 +20,8 @@ public abstract class AbstractLanguageServer private readonly JsonRpc _jsonRpc; protected readonly ILspLogger _logger; + protected readonly JsonSerializer _jsonSerializer; + /// /// These are lazy to allow implementations to define custom variables that are used by /// or @@ -49,17 +55,26 @@ public abstract class AbstractLanguageServer protected AbstractLanguageServer( JsonRpc jsonRpc, + JsonSerializer jsonSerializer, ILspLogger logger) { _logger = logger; - _jsonRpc = jsonRpc; + _jsonSerializer = jsonSerializer; + _jsonRpc.AddLocalRpcTarget(this); _jsonRpc.Disconnected += JsonRpc_Disconnected; _lspServices = new Lazy(() => ConstructLspServices()); _queue = new Lazy>(() => ConstructRequestExecutionQueue()); } + [Obsolete($"Use AbstractLanguageServer(JsonRpc jsonRpc, JsonSerializer jsonSerializer, ILspLogger logger)")] + protected AbstractLanguageServer( + JsonRpc jsonRpc, + ILspLogger logger) : this(jsonRpc, GetJsonSerializerFromJsonRpc(jsonRpc), logger) + { + } + /// /// Initializes the LanguageServer. /// @@ -107,29 +122,37 @@ protected virtual AbstractHandlerProvider HandlerProvider protected virtual void SetupRequestDispatcher(IHandlerProvider handlerProvider) { // Get unique set of methods from the handler provider for the default language. - foreach (var metadata in handlerProvider + foreach (var methodGroup in handlerProvider .GetRegisteredMethods() - .Select(m => new RequestHandlerMetadata(m.MethodName, m.RequestType, m.ResponseType, LanguageServerConstants.DefaultLanguageName)) - .Distinct()) + .GroupBy(m => m.MethodName)) { // Instead of concretely defining methods for each LSP method, we instead dynamically construct the // generic method info from the exported handler types. This allows us to define multiple handlers for - // the same method but different type parameters. This is a key functionality to support TS external - // access as we do not want to couple our LSP protocol version dll to theirs. - // - // We also do not use the StreamJsonRpc support for JToken as the rpc method parameters because we want - // StreamJsonRpc to do the deserialization to handle streaming requests using IProgress. + // the same method but different type parameters. This is a key functionality to support LSP extensibility + // in cases like XAML, TS to allow them to use different LSP type definitions + + // Verify that we are not mixing different numbers of request parameters and responses between different language handlers + // e.g. it is not allowed to have a method have both a parameterless and regular parameter handler. + var requestTypes = methodGroup.Select(m => m.RequestType); + var responseTypes = methodGroup.Select(m => m.ResponseType); + if (!(requestTypes.All(r => r is null) || requestTypes.Any(r => r is not null)) + || !(responseTypes.All(r => r is null) || responseTypes.Any(r => r is not null))) + { + throw new InvalidOperationException($"Language specific handlers for {methodGroup.Key} have mis-matched number of parameters or returns:{Environment.NewLine}{string.Join(Environment.NewLine, methodGroup)}"); + } - var method = DelegatingEntryPoint.GetMethodInstantiation(metadata.RequestType, metadata.ResponseType); + // Pick the kind of streamjsonrpc handling based on the number of request / response arguments + // We use the first since we've validated above that the language specific handlers have similarly shaped requests. + var methodInfo = DelegatingEntryPoint.GetMethodInstantiation(methodGroup.First().RequestType, methodGroup.First().ResponseType); - var delegatingEntryPoint = new DelegatingEntryPoint(metadata.MethodName, this); + var delegatingEntryPoint = new DelegatingEntryPoint(methodGroup.Key, this, handlerProvider); - var methodAttribute = new JsonRpcMethodAttribute(metadata.MethodName) + var methodAttribute = new JsonRpcMethodAttribute(methodGroup.Key) { UseSingleObjectParameterDeserialization = true, }; - _jsonRpc.AddLocalRpcMethod(method, delegatingEntryPoint, methodAttribute); + _jsonRpc.AddLocalRpcMethod(methodInfo, delegatingEntryPoint, methodAttribute); } } @@ -159,13 +182,31 @@ protected IRequestExecutionQueue GetRequestExecutionQueue() return _queue.Value; } + protected virtual string GetLanguageForRequest(string methodName, JToken? parameters) + { + _logger.LogInformation("Using default language handler"); + return LanguageServerConstants.DefaultLanguageName; + } + /// - /// Wrapper class to hold the method and properties from the - /// that the method info passed to StreamJsonRpc is created from. + /// Temporary workaround to avoid requiring a breaking change in CLASP. + /// Consumers of clasp already specify the json serializer they need on the jsonRpc object. + /// We can retrieve that serializer from it via reflection. /// + private static JsonSerializer GetJsonSerializerFromJsonRpc(JsonRpc jsonRpc) + { + var messageHandlerProp = typeof(JsonRpc).GetProperty("MessageHandler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var getter = messageHandlerProp.GetGetMethod(nonPublic: true); + var messageHandler = (IJsonRpcMessageHandler)getter.Invoke(jsonRpc, null); + var formatter = (JsonMessageFormatter)messageHandler.Formatter; + var serializer = formatter.JsonSerializer; + return serializer; + } + private sealed class DelegatingEntryPoint { private readonly string _method; + private readonly Lazy> _languageEntryPoint; private readonly AbstractLanguageServer _target; private static readonly MethodInfo s_entryPointMethod = typeof(DelegatingEntryPoint).GetMethod(nameof(EntryPointAsync)); @@ -173,55 +214,79 @@ private sealed class DelegatingEntryPoint private static readonly MethodInfo s_notificationMethod = typeof(DelegatingEntryPoint).GetMethod(nameof(NotificationEntryPointAsync)); private static readonly MethodInfo s_parameterlessNotificationMethod = typeof(DelegatingEntryPoint).GetMethod(nameof(ParameterlessNotificationEntryPointAsync)); - public DelegatingEntryPoint(string method, AbstractLanguageServer target) + private static readonly MethodInfo s_queueExecuteAsyncMethod = typeof(RequestExecutionQueue).GetMethod(nameof(RequestExecutionQueue.ExecuteAsync)); + + public DelegatingEntryPoint(string method, AbstractLanguageServer target, IHandlerProvider handlerProvider) { _method = method; _target = target; + _languageEntryPoint = new Lazy>(() => + { + var handlerEntryPoints = new Dictionary(); + foreach (var metadata in handlerProvider + .GetRegisteredMethods() + .Where(m => m.MethodName == method)) + { + var requestType = metadata.RequestType ?? NoValue.Instance.GetType(); + var responseType = metadata.ResponseType ?? NoValue.Instance.GetType(); + var methodInfo = s_queueExecuteAsyncMethod.MakeGenericMethod(requestType, responseType); + handlerEntryPoints[metadata.Language] = (methodInfo, metadata); + } + + return handlerEntryPoints.ToImmutableDictionary(); + }); } public static MethodInfo GetMethodInstantiation(Type? requestType, Type? responseType) => (requestType, responseType) switch { - (requestType: not null, responseType: not null) => s_entryPointMethod.MakeGenericMethod(requestType, responseType), - (requestType: null, responseType: not null) => s_parameterlessEntryPointMethod.MakeGenericMethod(responseType), - (requestType: not null, responseType: null) => s_notificationMethod.MakeGenericMethod(requestType), + (requestType: not null, responseType: not null) => s_entryPointMethod, + (requestType: null, responseType: not null) => s_parameterlessEntryPointMethod, + (requestType: not null, responseType: null) => s_notificationMethod, (requestType: null, responseType: null) => s_parameterlessNotificationMethod, }; - public async Task NotificationEntryPointAsync(TRequest request, CancellationToken cancellationToken) where TRequest : class - { - var queue = _target.GetRequestExecutionQueue(); - var lspServices = _target.GetLspServices(); + public Task NotificationEntryPointAsync(JToken request, CancellationToken cancellationToken) => ExecuteRequestAsync(request, cancellationToken); - _ = await queue.ExecuteAsync(request, _method, lspServices, cancellationToken).ConfigureAwait(false); - } + public Task ParameterlessNotificationEntryPointAsync(CancellationToken cancellationToken) => ExecuteRequestAsync(null, cancellationToken); - public async Task ParameterlessNotificationEntryPointAsync(CancellationToken cancellationToken) - { - var queue = _target.GetRequestExecutionQueue(); - var lspServices = _target.GetLspServices(); + public Task EntryPointAsync(JToken request, CancellationToken cancellationToken) => ExecuteRequestAsync(request, cancellationToken); - _ = await queue.ExecuteAsync(NoValue.Instance, _method, lspServices, cancellationToken).ConfigureAwait(false); - } + public Task ParameterlessEntryPointAsync(CancellationToken cancellationToken) => ExecuteRequestAsync(null, cancellationToken); - public async Task EntryPointAsync(TRequest request, CancellationToken cancellationToken) where TRequest : class + private async Task ExecuteRequestAsync(JToken? request, CancellationToken cancellationToken) { var queue = _target.GetRequestExecutionQueue(); var lspServices = _target.GetLspServices(); - var result = await queue.ExecuteAsync(request, _method, lspServices, cancellationToken).ConfigureAwait(false); + // Retrieve the language of the request so we know how to deserialize it. + var language = _target.GetLanguageForRequest(_method, request); - return result; - } + // Find the correct request and response types for the given request and language. + if (!_languageEntryPoint.Value.TryGetValue(language, out var requestInfo) + && !_languageEntryPoint.Value.TryGetValue(LanguageServerConstants.DefaultLanguageName, out requestInfo)) + { + throw new InvalidOperationException($"No default or language specific handler was found for {_method} and document with language {language}"); + } - public async Task ParameterlessEntryPointAsync(CancellationToken cancellationToken) - { - var queue = _target.GetRequestExecutionQueue(); - var lspServices = _target.GetLspServices(); + // Deserialize the request parameters (if any). + object requestObject = NoValue.Instance; + if (request is not null) + { + if (requestInfo.Metadata.RequestType is null) + { + throw new InvalidOperationException($"Handler for {_method} and {language} has no request type defined"); + } - var result = await queue.ExecuteAsync(NoValue.Instance, _method, lspServices, cancellationToken).ConfigureAwait(false); + requestObject = request.ToObject(requestInfo.Metadata.RequestType, _target._jsonSerializer) + ?? throw new InvalidOperationException($"Unable to deserialize {request} into {requestInfo.Metadata.RequestType} for {_method} and language {language}"); + } - return result; + var task = (Task)requestInfo.MethodInfo.Invoke(queue, new[] { requestObject, _method, language, lspServices, cancellationToken }); + await task.ConfigureAwait(false); + var resultProperty = task.GetType().GetProperty("Result"); + var result = resultProperty.GetValue(task); + return result is not null ? JToken.FromObject(result, _target._jsonSerializer) : null; } } @@ -370,9 +435,9 @@ internal TestAccessor(AbstractLanguageServer server) return null; } - internal Task ExecuteRequestAsync(string methodName, TRequest request, CancellationToken cancellationToken) + internal Task ExecuteRequestAsync(string methodName, string languageName, TRequest request, CancellationToken cancellationToken) { - return _server._queue.Value.ExecuteAsync(request, methodName, _server._lspServices.Value, cancellationToken); + return _server._queue.Value.ExecuteAsync(request, methodName, languageName, _server._lspServices.Value, cancellationToken); } internal JsonRpc GetServerRpc() => _server._jsonRpc; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs index 5f36c4e8477b3..67becf57b7184 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs @@ -18,7 +18,7 @@ public interface IRequestExecutionQueue : IAsyncDisposable /// Queue a request. /// /// A task that completes when the handler execution is done. - Task ExecuteAsync(TRequest request, string methodName, ILspServices lspServices, CancellationToken cancellationToken); + Task ExecuteAsync(TRequest request, string methodName, string languageName, ILspServices lspServices, CancellationToken cancellationToken); /// /// Start the queue accepting requests once any event handlers have been attached. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs index 3f982243d2aad..cf43fc7672959 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Threading; +using Newtonsoft.Json.Linq; namespace Microsoft.CommonLanguageServerProtocol.Framework; @@ -33,7 +34,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// Regardless of whether a request is mutating or not, or blocking or not, is an implementation detail of this class /// and any consumers observing the results of the task returned from -/// +/// /// will see the results of the handling of the request, whenever it occurred. /// /// @@ -118,6 +119,7 @@ public void Start() public virtual Task ExecuteAsync( TRequest request, string methodName, + string languageName, ILspServices lspServices, CancellationToken requestCancellationToken) { @@ -129,6 +131,7 @@ public virtual Task ExecuteAsync( var combinedCancellationToken = combinedTokenSource.Token; var (item, resultTask) = CreateQueueItem( methodName, + languageName, request, lspServices, combinedCancellationToken); @@ -150,22 +153,26 @@ public virtual Task ExecuteAsync( internal (IQueueItem, Task) CreateQueueItem( string methodName, + string languageName, TRequest request, ILspServices lspServices, CancellationToken cancellationToken) { - var language = GetLanguageForRequest(methodName, request); - return QueueItem.Create(methodName, - language, + languageName, request, lspServices, _logger, cancellationToken); } - protected virtual string GetLanguageForRequest(string methodName, TRequest request) - => LanguageServerConstants.DefaultLanguageName; + //protected virtual string GetLanguageForRequest(string methodName, TRequest request) + // => LanguageServerConstants.DefaultLanguageName; + + internal virtual string GetLanguageForRequest(string methodName, JObject parameters) + { + return LanguageServerConstants.DefaultLanguageName; + } private async Task ProcessQueueAsync() { diff --git a/src/Features/LanguageServer/Protocol/CSharpVisualBasicLanguageServerFactory.cs b/src/Features/LanguageServer/Protocol/CSharpVisualBasicLanguageServerFactory.cs index 67ecd1ee51d0c..09bc87c661513 100644 --- a/src/Features/LanguageServer/Protocol/CSharpVisualBasicLanguageServerFactory.cs +++ b/src/Features/LanguageServer/Protocol/CSharpVisualBasicLanguageServerFactory.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json; using StreamJsonRpc; namespace Microsoft.CodeAnalysis.LanguageServer @@ -29,6 +30,7 @@ public CSharpVisualBasicLanguageServerFactory( public AbstractLanguageServer Create( JsonRpc jsonRpc, + JsonSerializer jsonSerializer, ICapabilitiesProvider capabilitiesProvider, WellKnownLspServerKinds serverKind, AbstractLspLogger logger, @@ -37,6 +39,7 @@ public AbstractLanguageServer Create( var server = new RoslynLanguageServer( _lspServiceProvider, jsonRpc, + jsonSerializer, capabilitiesProvider, logger, hostServices, @@ -46,10 +49,11 @@ public AbstractLanguageServer Create( return server; } - public AbstractLanguageServer Create(Stream input, Stream output, ICapabilitiesProvider capabilitiesProvider, AbstractLspLogger logger, HostServices hostServices) - { - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(output, input)); - return Create(jsonRpc, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, logger, hostServices); - } + //public AbstractLanguageServer Create(Stream input, Stream output, ICapabilitiesProvider capabilitiesProvider, AbstractLspLogger logger, HostServices hostServices) + //{ + + // var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(output, input)); + // return Create(jsonRpc, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, logger, hostServices); + //} } } diff --git a/src/Features/LanguageServer/Protocol/Handler/WorkspaceCommand/ExecuteWorkspaceCommandHandler.cs b/src/Features/LanguageServer/Protocol/Handler/WorkspaceCommand/ExecuteWorkspaceCommandHandler.cs index 761da70bf4244..04397fc1bf4d4 100644 --- a/src/Features/LanguageServer/Protocol/Handler/WorkspaceCommand/ExecuteWorkspaceCommandHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/WorkspaceCommand/ExecuteWorkspaceCommandHandler.cs @@ -36,6 +36,7 @@ public ExecuteWorkspaceCommandHandler() var result = await requestExecutionQueue.ExecuteAsync( request, + LanguageServerConstants.DefaultLanguageName, requestMethod, lspServices, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/LanguageServer/Protocol/ILanguageServerFactory.cs b/src/Features/LanguageServer/Protocol/ILanguageServerFactory.cs index 243190649afe8..c2145c42bf68c 100644 --- a/src/Features/LanguageServer/Protocol/ILanguageServerFactory.cs +++ b/src/Features/LanguageServer/Protocol/ILanguageServerFactory.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json; using StreamJsonRpc; namespace Microsoft.CodeAnalysis.LanguageServer @@ -13,6 +14,7 @@ internal interface ILanguageServerFactory { public AbstractLanguageServer Create( JsonRpc jsonRpc, + JsonSerializer jsonSerializer, ICapabilitiesProvider capabilitiesProvider, WellKnownLspServerKinds serverKind, AbstractLspLogger logger, diff --git a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs index 3303971dcb38d..a3238b06da3d8 100644 --- a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -11,6 +11,8 @@ using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.ServerLifetime; using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; using StreamJsonRpc; @@ -26,12 +28,13 @@ internal sealed class RoslynLanguageServer : AbstractLanguageServer supportedLanguages, WellKnownLspServerKinds serverKind) - : base(jsonRpc, logger) + : base(jsonRpc, serializer, logger) { _lspServiceProvider = lspServiceProvider; _serverKind = serverKind; @@ -107,5 +110,71 @@ public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestCon OnInitialized(); return Task.CompletedTask; } + + protected override string GetLanguageForRequest(string methodName, JToken? parameters) + { + if (parameters == null) + { + _logger.LogInformation("No request parameters given, using default language handler"); + return LanguageServerConstants.DefaultLanguageName; + } + + // For certain requests like text syncing we'll always use the default language handler + // as we do not want languages to be able to override them. + if (ShouldUseDefaultLanguage(methodName)) + { + return LanguageServerConstants.DefaultLanguageName; + } + + var lspWorkspaceManager = GetLspServices().GetRequiredService(); + + // All general LSP spec document params have the following json structure + // { "textDocument": { "uri": "" ... } ... } + // + // We can easily identify the URI for the request by looking for this structure + var textDocumentToken = parameters["textDocument"]; + if (textDocumentToken is not null) + { + var uriToken = textDocumentToken["uri"]; + Contract.ThrowIfNull(uriToken, "textDocument does not have a uri property"); + var uri = uriToken.ToObject(_jsonSerializer); + Contract.ThrowIfNull(uri, "Failed to deserialize uri property"); + var language = lspWorkspaceManager.GetLanguageForUri(uri); + _logger.LogInformation($"Using {language} from request text document"); + return language; + } + + // All the LSP resolve params have the following known json structure + // { "data": { "TextDocument": { "uri": "" ... } ... } ... } + // + // We can deserialize the data object using our unified DocumentResolveData. + var dataToken = parameters["data"]; + if (dataToken is not null) + { + var data = dataToken.ToObject(_jsonSerializer); + Contract.ThrowIfNull(data, "Failed to document resolve data object"); + var language = lspWorkspaceManager.GetLanguageForUri(data.TextDocument.Uri); + _logger.LogInformation($"Using {language} from data text document"); + return language; + } + + // This request is not for a textDocument and is not a resolve request. + _logger.LogInformation("Request did not contain a textDocument, using default language handler"); + return LanguageServerConstants.DefaultLanguageName; + + static bool ShouldUseDefaultLanguage(string methodName) + => methodName switch + { + Methods.InitializeName => true, + Methods.InitializedName => true, + Methods.TextDocumentDidOpenName => true, + Methods.TextDocumentDidChangeName => true, + Methods.TextDocumentDidCloseName => true, + Methods.TextDocumentDidSaveName => true, + Methods.ShutdownName => true, + Methods.ExitName => true, + _ => false, + }; + } } } diff --git a/src/Features/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs b/src/Features/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs index 1944086af9b1e..291c908c88242 100644 --- a/src/Features/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Protocol/RoslynRequestExecutionQueue.cs @@ -2,13 +2,10 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CommonLanguageServerProtocol.Framework; -using Roslyn.LanguageServer.Protocol; -using Newtonsoft.Json.Linq; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer @@ -16,7 +13,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer internal sealed class RoslynRequestExecutionQueue : RequestExecutionQueue { private readonly IInitializeManager _initializeManager; - private readonly LspWorkspaceManager _lspWorkspaceManager; /// /// Serial access is guaranteed by the queue. @@ -27,7 +23,6 @@ public RoslynRequestExecutionQueue(AbstractLanguageServer langua : base(languageServer, logger, handlerProvider) { _initializeManager = languageServer.GetLspServices().GetRequiredService(); - _lspWorkspaceManager = languageServer.GetLspServices().GetRequiredService(); } public override Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask, bool rethrowExceptions) @@ -44,50 +39,6 @@ public override Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask, bool } } - protected override string GetLanguageForRequest(string methodName, TRequest request) - { - var uri = GetUriForRequest(methodName, request); - if (uri is not null) - { - return _lspWorkspaceManager.GetLanguageForUri(uri); - } - - return base.GetLanguageForRequest(methodName, request); - } - - private static Uri? GetUriForRequest(string methodName, TRequest request) - { - if (request is ITextDocumentParams textDocumentParams) - { - return textDocumentParams.TextDocument.Uri; - } - - if (IsDocumentResolveMethod(methodName)) - { - var dataToken = (JToken?)request?.GetType().GetProperty("Data")?.GetValue(request); - var resolveData = dataToken?.ToObject(); - if (resolveData is null) - { - throw new InvalidOperationException($"{methodName} requires resolve data object to derive from {nameof(DocumentResolveData)}."); - } - - return resolveData.TextDocument.Uri; - } - - return null; - - static bool IsDocumentResolveMethod(string methodName) - => methodName switch - { - Methods.CodeActionResolveName => true, - Methods.CodeLensResolveName => true, - Methods.DocumentLinkResolveName => true, - Methods.InlayHintResolveName => true, - Methods.TextDocumentCompletionResolveName => true, - _ => false, - }; - } - /// /// Serial access is guaranteed by the queue. /// diff --git a/src/Features/LanguageServer/ProtocolUnitTests/HandlerTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/HandlerTests.cs new file mode 100644 index 0000000000000..648a151286885 --- /dev/null +++ b/src/Features/LanguageServer/ProtocolUnitTests/HandlerTests.cs @@ -0,0 +1,290 @@ +// 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.Composition; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests +{ + [UseExportProvider] + public class HandlerTests : AbstractLanguageServerProtocolTests + { + public HandlerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + protected override TestComposition Composition => base.Composition.AddParts( + typeof(DocumentHandler), + typeof(RequestHandlerWithNoParams), + typeof(NotificationHandler), + typeof(NotificationWithoutParamsHandler), + typeof(LanguageSpecificHandler), + typeof(LanguageSpecificHandlerWithDifferentParams)); + + [Theory, CombinatorialData] + public async Task CanExecuteRequestHandler(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + var request = new TestRequestTypeOne(new TextDocumentIdentifier + { + Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.cs") + }); + var response = await server.ExecuteRequestAsync(DocumentHandler.MethodName, request, CancellationToken.None); + Assert.Equal(typeof(DocumentHandler).Name, response); + } + + [Theory, CombinatorialData] + public async Task CanExecuteRequestHandlerWithNoParams(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + var response = await server.ExecuteRequest0Async(RequestHandlerWithNoParams.MethodName, CancellationToken.None); + Assert.Equal(typeof(RequestHandlerWithNoParams).Name, response); + } + + [Theory, CombinatorialData] + public async Task CanExecuteNotificationHandler(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + await server.ExecuteNotificationAsync(NotificationHandler.MethodName, "hello"); + var response = await NotificationHandler.ResultSource.Task; + Assert.Equal(typeof(NotificationHandler).Name, response); + } + + [Theory, CombinatorialData] + public async Task CanExecuteNotificationHandlerWithNoParams(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + await server.ExecuteNotification0Async(NotificationWithoutParamsHandler.MethodName); + var response = await NotificationWithoutParamsHandler.ResultSource.Task; + Assert.Equal(typeof(NotificationWithoutParamsHandler).Name, response); + } + + [Theory, CombinatorialData] + public async Task CanExecuteLanguageSpecificHandler(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + var request = new TestRequestTypeOne(new TextDocumentIdentifier + { + Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.fs") + }); + var response = await server.ExecuteRequestAsync(DocumentHandler.MethodName, request, CancellationToken.None); + Assert.Equal(typeof(LanguageSpecificHandler).Name, response); + } + + [Theory, CombinatorialData] + public async Task CanExecuteLanguageSpecificHandlerWithDifferentRequestTypes(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + + var request = new TestRequestTypeTwo(new TextDocumentIdentifier + { + Uri = ProtocolConversions.CreateAbsoluteUri(@"C:\test.vb") + }); + var response = await server.ExecuteRequestAsync(DocumentHandler.MethodName, request, CancellationToken.None); + Assert.Equal(typeof(LanguageSpecificHandlerWithDifferentParams).Name, response); + } + + [Theory, CombinatorialData] + public async Task ThrowsOnInvalidLanguageSpecificHandler(bool mutatingLspWorkspace) + { + // Arrange + await Assert.ThrowsAsync(async () => await CreateTestLspServerAsync("", mutatingLspWorkspace, extraExportedTypes: [typeof(DuplicateLanguageSpecificHandler)])); + } + + [DataContract] + internal record TestRequestTypeOne([property: DataMember(Name = "textDocument")] TextDocumentIdentifier TextDocumentIdentifier); + + [DataContract] + internal record TestRequestTypeTwo([property: DataMember(Name = "textDocument")] TextDocumentIdentifier TextDocumentIdentifier); + + [ExportCSharpVisualBasicStatelessLspService(typeof(DocumentHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] + internal class DocumentHandler : ILspServiceDocumentRequestHandler + { + public const string MethodName = nameof(DocumentHandler); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DocumentHandler() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequestTypeOne request) + { + return request.TextDocumentIdentifier; + } + + public Task HandleRequestAsync(TestRequestTypeOne request, RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(this.GetType().Name); + } + } + + [ExportCSharpVisualBasicStatelessLspService(typeof(RequestHandlerWithNoParams)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] + internal class RequestHandlerWithNoParams : ILspServiceRequestHandler + { + public const string MethodName = nameof(RequestHandlerWithNoParams); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RequestHandlerWithNoParams() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public Task HandleRequestAsync(RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(this.GetType().Name); + } + } + + [ExportCSharpVisualBasicStatelessLspService(typeof(NotificationHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] + internal class NotificationHandler : ILspServiceNotificationHandler + { + public const string MethodName = nameof(NotificationHandler); + public static readonly TaskCompletionSource ResultSource = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public NotificationHandler() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public Task HandleNotificationAsync(string request, RequestContext context, CancellationToken cancellationToken) + { + ResultSource.SetResult(this.GetType().Name); + return ResultSource.Task; + } + } + + [ExportCSharpVisualBasicStatelessLspService(typeof(NotificationWithoutParamsHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] + internal class NotificationWithoutParamsHandler : ILspServiceNotificationHandler + { + public const string MethodName = nameof(NotificationWithoutParamsHandler); + public static readonly TaskCompletionSource ResultSource = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public NotificationWithoutParamsHandler() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public Task HandleNotificationAsync(RequestContext context, CancellationToken cancellationToken) + { + ResultSource.SetResult(this.GetType().Name); + return ResultSource.Task; + } + } + + /// + /// Defines a language specific handler with the same method as + /// + [ExportCSharpVisualBasicStatelessLspService(typeof(LanguageSpecificHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(DocumentHandler.MethodName, LanguageNames.FSharp)] + internal class LanguageSpecificHandler : ILspServiceDocumentRequestHandler + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LanguageSpecificHandler() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequestTypeOne request) + { + return request.TextDocumentIdentifier; + } + + public Task HandleRequestAsync(TestRequestTypeOne request, RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(this.GetType().Name); + } + } + + /// + /// Defines a language specific handler with the same method as + /// but using different request and response types. + /// + [ExportCSharpVisualBasicStatelessLspService(typeof(LanguageSpecificHandlerWithDifferentParams)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(DocumentHandler.MethodName, LanguageNames.VisualBasic)] + internal class LanguageSpecificHandlerWithDifferentParams : ILspServiceDocumentRequestHandler + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LanguageSpecificHandlerWithDifferentParams() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(TestRequestTypeTwo request) + { + return request.TextDocumentIdentifier; + } + + public Task HandleRequestAsync(TestRequestTypeTwo request, RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(this.GetType().Name); + } + } + + /// + /// Defines a language specific handler with the same method and language as + /// but with different params (an error) + /// + [ExportCSharpVisualBasicStatelessLspService(typeof(DuplicateLanguageSpecificHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(DocumentHandler.MethodName, LanguageNames.FSharp)] + internal class DuplicateLanguageSpecificHandler : ILspServiceRequestHandler + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DuplicateLanguageSpecificHandler() + { + } + + public bool MutatesSolutionState => true; + public bool RequiresLSPSolution => true; + + public Task HandleRequestAsync(RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(this.GetType().Name); + } + } + } +} diff --git a/src/Features/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs index cc39ced87c907..bf29073777e65 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/VSTypeScriptHandlerTests.cs @@ -116,7 +116,8 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str var capabilitiesProvider = workspace.ExportProvider.GetExportedValue(); var servicesProvider = workspace.ExportProvider.GetExportedValue(); - var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream)) + var messageFormatter = CreateJsonMessageFormatter(); + var jsonRpc = new JsonRpc(new HeaderDelimitedMessageHandler(outputStream, inputStream, messageFormatter)) { ExceptionStrategy = ExceptionProcessing.ISerializable, }; @@ -124,7 +125,7 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str var logger = NoOpLspLogger.Instance; var languageServer = new RoslynLanguageServer( - servicesProvider, jsonRpc, + servicesProvider, jsonRpc, messageFormatter.JsonSerializer, capabilitiesProvider, logger, workspace.Services.HostServices, diff --git a/src/Tools/ExternalAccess/Razor/IRazorLanguageServerFactory.cs b/src/Tools/ExternalAccess/Razor/AbstractRazorLanguageServerFactoryWrapper.cs similarity index 70% rename from src/Tools/ExternalAccess/Razor/IRazorLanguageServerFactory.cs rename to src/Tools/ExternalAccess/Razor/AbstractRazorLanguageServerFactoryWrapper.cs index 0569cc2b90546..93dc75dd8acda 100644 --- a/src/Tools/ExternalAccess/Razor/IRazorLanguageServerFactory.cs +++ b/src/Tools/ExternalAccess/Razor/AbstractRazorLanguageServerFactoryWrapper.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using Microsoft.CodeAnalysis.Host; using Newtonsoft.Json; @@ -12,11 +13,11 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor /// /// NOTE: For Razor test usage only /// - internal interface IRazorLanguageServerFactoryWrapper + internal abstract class AbstractRazorLanguageServerFactoryWrapper { - IRazorLanguageServerTarget CreateLanguageServer(JsonRpc jsonRpc, IRazorTestCapabilitiesProvider capabilitiesProvider, HostServices hostServices); + internal abstract IRazorLanguageServerTarget CreateLanguageServer(JsonRpc jsonRpc, JsonSerializer jsonSerializer, IRazorTestCapabilitiesProvider capabilitiesProvider, HostServices hostServices); - DocumentInfo CreateDocumentInfo( + internal abstract DocumentInfo CreateDocumentInfo( DocumentId id, string name, IReadOnlyList? folders = null, @@ -30,6 +31,6 @@ DocumentInfo CreateDocumentInfo( /// /// Supports the creation of a Roslyn LSP server for functional tests /// - void AddJsonConverters(JsonSerializer jsonSerializer); + internal abstract void AddJsonConverters(JsonSerializer jsonSerializer); } } diff --git a/src/Tools/ExternalAccess/Razor/RazorLanguageServerFactoryWrapper.cs b/src/Tools/ExternalAccess/Razor/RazorLanguageServerFactoryWrapper.cs index e99bb674778a6..989fc0e911cfb 100644 --- a/src/Tools/ExternalAccess/Razor/RazorLanguageServerFactoryWrapper.cs +++ b/src/Tools/ExternalAccess/Razor/RazorLanguageServerFactoryWrapper.cs @@ -15,9 +15,9 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor { - [Export(typeof(IRazorLanguageServerFactoryWrapper))] + [Export(typeof(AbstractRazorLanguageServerFactoryWrapper))] [Shared] - internal class RazorLanguageServerFactoryWrapper : IRazorLanguageServerFactoryWrapper + internal class RazorLanguageServerFactoryWrapper : AbstractRazorLanguageServerFactoryWrapper { private readonly ILanguageServerFactory _languageServerFactory; @@ -33,15 +33,15 @@ public RazorLanguageServerFactoryWrapper(ILanguageServerFactory languageServerFa _languageServerFactory = languageServerFactory; } - public IRazorLanguageServerTarget CreateLanguageServer(JsonRpc jsonRpc, IRazorTestCapabilitiesProvider razorCapabilitiesProvider, HostServices hostServices) + internal override IRazorLanguageServerTarget CreateLanguageServer(JsonRpc jsonRpc, JsonSerializer jsonSerializer, IRazorTestCapabilitiesProvider razorCapabilitiesProvider, HostServices hostServices) { var capabilitiesProvider = new RazorCapabilitiesProvider(razorCapabilitiesProvider); - var languageServer = _languageServerFactory.Create(jsonRpc, capabilitiesProvider, WellKnownLspServerKinds.RazorLspServer, NoOpLspLogger.Instance, hostServices); + var languageServer = _languageServerFactory.Create(jsonRpc, jsonSerializer, capabilitiesProvider, WellKnownLspServerKinds.RazorLspServer, NoOpLspLogger.Instance, hostServices); return new RazorLanguageServerTargetWrapper(languageServer); } - public DocumentInfo CreateDocumentInfo( + internal override DocumentInfo CreateDocumentInfo( DocumentId id, string name, IReadOnlyList? folders = null, @@ -65,7 +65,7 @@ public DocumentInfo CreateDocumentInfo( .WithDocumentServiceProvider(documentServiceProvider); } - public void AddJsonConverters(JsonSerializer jsonSerializer) + internal override void AddJsonConverters(JsonSerializer jsonSerializer) { VSInternalExtensionUtilities.AddVSInternalExtensionConverters(jsonSerializer); } diff --git a/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs b/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs index 3ffcfff8da827..0ee0abd4523d6 100644 --- a/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs +++ b/src/Tools/ExternalAccess/RazorTest/Cohost/RazorCohostTests.cs @@ -59,7 +59,7 @@ public async Task TestExternalAccessRazorHandlerInvoked() } }; - var response = await server.GetTestAccessor().ExecuteRequestAsync(RazorHandler.MethodName, request, CancellationToken.None); + var response = await server.GetTestAccessor().ExecuteRequestAsync(RazorHandler.MethodName, LanguageServerConstants.DefaultLanguageName, request, CancellationToken.None); Assert.NotNull(response); Assert.Equal(document.GetURI(), response.DocumentUri); @@ -88,7 +88,7 @@ public async Task TestProjectContextHandler() } }; - var response = await server.GetTestAccessor().ExecuteRequestAsync(VSMethods.GetProjectContextsName, request, CancellationToken.None); + var response = await server.GetTestAccessor().ExecuteRequestAsync(VSMethods.GetProjectContextsName, LanguageServerConstants.DefaultLanguageName, request, CancellationToken.None); Assert.NotNull(response); var projectContext = Assert.Single(response?.ProjectContexts); @@ -118,7 +118,7 @@ public async Task TestDocumentSync() } }; - await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidOpenName, didOpenRequest, CancellationToken.None); + await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidOpenName, LanguageServerConstants.DefaultLanguageName, didOpenRequest, CancellationToken.None); var workspaceManager = server.GetLspServices().GetRequiredService(); Assert.True(workspaceManager.GetTrackedLspText().TryGetValue(document.GetURI(), out var trackedText)); @@ -144,7 +144,7 @@ public async Task TestDocumentSync() ] }; - await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidChangeName, didChangeRequest, CancellationToken.None); + await server.GetTestAccessor().ExecuteRequestAsync(Methods.TextDocumentDidChangeName, LanguageServerConstants.DefaultLanguageName, didChangeRequest, CancellationToken.None); Assert.True(workspaceManager.GetTrackedLspText().TryGetValue(document.GetURI(), out trackedText)); Assert.Equal("Not The Original text", trackedText.Text.ToString()); @@ -167,7 +167,7 @@ private static async Task> InitializeLang var serverAccessor = server!.GetTestAccessor(); - await serverAccessor.ExecuteRequestAsync(Methods.InitializeName, new InitializeParams { Capabilities = new() }, CancellationToken.None); + await serverAccessor.ExecuteRequestAsync(Methods.InitializeName, LanguageServerConstants.DefaultLanguageName, new InitializeParams { Capabilities = new() }, CancellationToken.None); return server; } diff --git a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs index 95ee7a5f8acd7..97b2a28ea79da 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/XamlRequestExecutionQueue.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CommonLanguageServerProtocol.Framework; +using Newtonsoft.Json.Linq; using Roslyn.LanguageServer.Protocol; namespace Microsoft.VisualStudio.LanguageServices.Xaml.LanguageServer @@ -23,15 +24,15 @@ public XamlRequestExecutionQueue( _projectService = projectService; } - protected override string GetLanguageForRequest(string methodName, TRequest request) - { - if (request is ITextDocumentParams textDocumentParams && - textDocumentParams.TextDocument is { Uri: { IsAbsoluteUri: true } documentUri }) - { - _projectService.TrackOpenDocument(documentUri.LocalPath); - } + //internal override string GetLanguageForRequest(string methodName, JObject request) + //{ + // if (request is ITextDocumentParams textDocumentParams && + // textDocumentParams.TextDocument is { Uri: { IsAbsoluteUri: true } documentUri }) + // { + // _projectService.TrackOpenDocument(documentUri.LocalPath); + // } - return base.GetLanguageForRequest(methodName, request); - } + // return base.GetLanguageForRequest(methodName, request); + //} } }