[LSP] Use runtime type arguments for streamjsonrpc request handling#59510
[LSP] Use runtime type arguments for streamjsonrpc request handling#59510dibarbet merged 17 commits intodotnet:mainfrom
Conversation
serialization at runtime and modify tests to use streamjsonrpc instead of directly accessing the queue.
4884d27 to
e558a5b
Compare
… handler types only for the specified contract
| /// </summary> | ||
| protected Task<TestLspServer> CreateTestLspServerAsync(string markup) | ||
| => CreateTestLspServerAsync(new string[] { markup }, LanguageNames.CSharp); | ||
| protected Task<TestLspServer> CreateTestLspServerAsync(string markup, LSP.ClientCapabilities? clientCapabilities = null) |
There was a problem hiding this comment.
Since we're not directly going through the request queue, we need to provide the client capabilities to the server upfront. This better matches how VS actually works.
| jsonRpc, | ||
| capabilitiesProvider, | ||
| _lspWorkspaceRegistrationService, | ||
| lspMiscellaneousFilesWorkspace: null, |
There was a problem hiding this comment.
matches previous behavior, have #57243 filed for me to followup on this
| /// Implementation of <see cref="LanguageServerTarget"/> that also supports | ||
| /// VS LSP extension methods. | ||
| /// </summary> | ||
| internal class VisualStudioInProcLanguageServer : LanguageServerTarget |
There was a problem hiding this comment.
no longer needed since we don't have LSP push diagnostics and we don't need to specify jsonrpcmethods explicitly anymore 🎉
| return messageFormatter; | ||
| } | ||
|
|
||
| internal static async Task<TestLspServer> CreateAsync(TestWorkspace testWorkspace, LSP.ClientCapabilities clientCapabilities, WellKnownLspServerKinds serverKind) |
There was a problem hiding this comment.
we now directly create the jsonrpc stream and LanguageServerTarget (previously we just manually sent requests to the RequestDispatcher). Tests will now go through serialization / deserialization as well as the full functionality of the LanguageServerTarget (yay more test coverage)
There was a problem hiding this comment.
This seems really good. 😄
| /// Creates a new request dispatcher every time to ensure handlers are not shared | ||
| /// and cleaned up appropriately on server restart. | ||
| /// </summary> | ||
| public virtual RequestDispatcher CreateRequestDispatcher(ImmutableArray<string> supportedLanguages, WellKnownLspServerKinds serverKind) |
There was a problem hiding this comment.
We don't need the supported languages here to filter out handlers for other servers - instead we only import the handler providers that match the desired contract name
| /// Additional <see cref="IRequestHandler"/> if <see cref="AbstractRequestHandlerProvider.CreateRequestHandlers(WellKnownLspServerKinds)"/> | ||
| /// provides more than one handler at once. | ||
| /// </param> | ||
| public ExportLspRequestHandlerProviderAttribute(string contractName, Type firstHandlerType, params Type[] additionalHandlerTypes) : base(contractName, typeof(AbstractRequestHandlerProvider)) |
There was a problem hiding this comment.
the firstHandler and additionalHandlerTypes is so we get a compile error if you don't pass in at least one handler type
| /// <summary> | ||
| /// The LSP method that this <see cref="IRequestHandler"/> implements. | ||
| /// </summary> | ||
| string Method { get; } |
There was a problem hiding this comment.
Duplicate information - we now use the method attribute on the handler to get the method name (doesn't require instantiating the handler)
| serverKind); | ||
| Queue.RequestServerShutdown += RequestExecutionQueue_Errored; | ||
|
|
||
| foreach (var metadata in RequestDispatcher.GetRegisteredMethods()) |
There was a problem hiding this comment.
this is where we register the method + request and response types with streamjsonrpc.
| /// Wrapper class to hold the method and properties from the <see cref="LanguageServerTarget"/> | ||
| /// that the method info passed to streamjsonrpc is created from. | ||
| /// </summary> | ||
| private class DelegatingEntryPoint |
There was a problem hiding this comment.
todo: do we really need this, or can we jsut pass around a tuple of these values...
There was a problem hiding this comment.
this type is mainly to hold onto the method name associated with the streamjsonrpc entrypoint. Since streamjsonrpc doesn't pass the method name to us when they call our entry point.
I don't think a tuple would work, as the target needs to have both the actual entry point method (for streamjsonrpc to call) and save the method so we can pass it to the queue when streamjsonrpc calls us
| return RequestDispatcher.ExecuteRequestAsync<TextDocumentPositionParams, LSP.SignatureHelp?>(Queue, Methods.TextDocumentSignatureHelpName, textDocumentPositionParams, _clientCapabilities, ClientName, cancellationToken); | ||
| } | ||
|
|
||
| /// <summary> |
There was a problem hiding this comment.
thank you for breasking up all the red with a little green. it's festive.
| public async Task<object?> ExecuteWorkspaceCommandAsync(LSP.ExecuteCommandParams request, CancellationToken cancellationToken) | ||
| { | ||
| Contract.ThrowIfNull(_clientCapabilities, $"{nameof(InitializeAsync)} has not been called."); | ||
| var requestMethod = AbstractExecuteWorkspaceCommandHandler.GetRequestNameForCommandName(request.Command); |
There was a problem hiding this comment.
for my edification, why ddo we still have this method?
There was a problem hiding this comment.
workspace/ExecuteCommand contains a command name string and an object typed payload. The server is expected to know what to do with the payload based on the command name (since commands are originally created by the server).
In roslyn, we model this by creating IRequestHandler for each command name that we support. So in order to know which IRequestHandler to delegate the command request to, we need to deserialize the ExecuteCommandParams to retrieve the command name. Then we can pass it to whatever handler says it handles that command.
And since the types for workspace/ExecuteCommand are all always object anyway, it doesn't matter if we deserialize first
| => _server.Queue.GetTestAccessor().GetLspWorkspaceManager().GetTestAccessor(); | ||
|
|
||
| internal RequestDispatcher.TestAccessor GetDispatcherAccessor() | ||
| => _server.RequestDispatcher.GetTestAccessor(); |
There was a problem hiding this comment.
i don't love that for testing it seems like we expose all of thsee things. how come?
There was a problem hiding this comment.
90% of the tests don't look at this, but there are specific tests that test implementation details of the queue, request dispatcher, or handlers.
For example, a few code action tests look at the handler instance to verify the cached contents match expectations. And the queue is looked at to verify that the tracked text at each point matches the DidChange requests, or that when we shutdown all the tasks in queue are cancelled.
Since these are TestAccessors the API surface they have access to is still limited, so the tests aren't actually able to arbitrarily access these internal parts.
| // Using the lazy set of handlers, create a lazy instance that will resolve the set of handlers for the provider | ||
| // and then lookup the correct handler for the specified method. | ||
| requestHandlerDictionary.Add(method, new Lazy<IRequestHandler>(() => lazyProviders.Value[method])); | ||
| requestHandlerDictionary.Add(new RequestHandlerMetadata(method, requestType, responseType), new Lazy<IRequestHandler>(() => lazyProviders.Value[method])); |
| var results = await RunGetCodeActionsAsync(testLspServer, caretLocation); | ||
| var introduceConstant = results[0].Children.FirstOrDefault( | ||
| r => ((CodeActionResolveData)r.Data).UniqueIdentifier == FeaturesResources.Introduce_constant | ||
| r => ((JObject)r.Data).ToObject<CodeActionResolveData>().UniqueIdentifier == FeaturesResources.Introduce_constant |
There was a problem hiding this comment.
i'm skipping the tests
jasonmalinowski
left a comment
There was a problem hiding this comment.
Some questions and confusion, but those largely are things that predate this specific PR. Confused me, but could be fixed separately (or not at all).
...itorFeatures/Core/Implementation/LanguageServer/Handlers/CodeActions/RunCodeActionHandler.cs
Show resolved
Hide resolved
| new ClassifiedTextRun("whitespace", string.Empty), | ||
| new ClassifiedTextRun("keyword", "class"), | ||
| new ClassifiedTextRun("whitespace", " "), | ||
| new ClassifiedTextRun("class name", className) | ||
| new ClassifiedTextRun("class name", className), | ||
| new ClassifiedTextRun("whitespace", string.Empty), |
There was a problem hiding this comment.
There's behavior changes happening here?
There was a problem hiding this comment.
This is caused by actually doing serialization - the client extension converters add the whitespace classified text runs
src/Features/LanguageServer/Protocol/Handler/Formatting/FormatDocumentOnTypeHandler.cs
Show resolved
Hide resolved
| static string GetRequestHandlerMethod(Type handlerType) | ||
| { | ||
| // Get the LSP method name from the handler's method name attribute. | ||
| var methodAttribute = Attribute.GetCustomAttribute(handlerType, typeof(MethodAttribute)) as MethodAttribute; |
There was a problem hiding this comment.
| var methodAttribute = Attribute.GetCustomAttribute(handlerType, typeof(MethodAttribute)) as MethodAttribute; | |
| var methodAttribute = (MethodAttribute?)Attribute.GetCustomAttribute(handlerType, typeof(MethodAttribute)); |
That way any type mismatch is a useful throw instead of this which might be more confusing.
There was a problem hiding this comment.
this gives me nullable warnings because GetCustomAttribute returns a nullable Attribute, so this way seemed clearer than bang
src/Features/LanguageServer/ProtocolUnitTests/SemanticTokens/AbstractSemanticTokensTests.cs
Show resolved
Hide resolved
| return messageFormatter; | ||
| } | ||
|
|
||
| internal static async Task<TestLspServer> CreateAsync(TestWorkspace testWorkspace, LSP.ClientCapabilities clientCapabilities, WellKnownLspServerKinds serverKind) |
There was a problem hiding this comment.
This seems really good. 😄
|
thanks all for the reviews Will address feedback from @jasonmalinowski in a separate PR to avoid dealing with more merge conflicts here. |
First part of exposing request handlers to TS. There are a lot of file changes, but the majority are mechanical.
This PR does two things generally
PR validation - https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/380294
Hitting test failure that is the same as the one we're currently hitting in main (bug https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1483775)