Skip to content

[LSP] Use runtime type arguments for streamjsonrpc request handling#59510

Merged
dibarbet merged 17 commits intodotnet:mainfrom
dibarbet:ts_lsp_externalaccess_2
Feb 16, 2022
Merged

[LSP] Use runtime type arguments for streamjsonrpc request handling#59510
dibarbet merged 17 commits intodotnet:mainfrom
dibarbet:ts_lsp_externalaccess_2

Conversation

@dibarbet
Copy link
Member

@dibarbet dibarbet commented Feb 12, 2022

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

  1. Derive the LSP method handler types at runtime to send to streamjsonrpc. This will allow TS to use their own set of types to create request handlers while still allowing streamjsonrpc to do the deserialization. Allows us to remove the jsonrpcmethod method lists. Plus did some cleanup around duplicate method name specification
  2. In order to test 1), I modified our LSP tests to actually send requests through streamjsonrpc instead of directly adding them to the request queue. This should improve coverage but required a few changes to tests

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)

@ghost ghost added the Area-IDE label Feb 12, 2022
@dibarbet dibarbet force-pushed the ts_lsp_externalaccess_2 branch from 4884d27 to e558a5b Compare February 12, 2022 03:25
/// </summary>
protected Task<TestLspServer> CreateTestLspServerAsync(string markup)
=> CreateTestLspServerAsync(new string[] { markup }, LanguageNames.CSharp);
protected Task<TestLspServer> CreateTestLspServerAsync(string markup, LSP.ClientCapabilities? clientCapabilities = null)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member Author

@dibarbet dibarbet Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; }
Copy link
Member Author

@dibarbet dibarbet Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where we register the method + request and response types with streamjsonrpc.

@dibarbet dibarbet marked this pull request as ready for review February 15, 2022 19:59
@dibarbet dibarbet requested a review from a team as a code owner February 15, 2022 19:59
@dibarbet dibarbet added the LSP issues related to the roslyn language server protocol implementation label Feb 15, 2022
@dibarbet dibarbet changed the title Ts lsp externalaccess 2 [LSP] Use runtime type arguments for streamjsonrpc request handling Feb 15, 2022
/// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: do we really need this, or can we jsut pass around a tuple of these values...

Copy link
Member Author

@dibarbet dibarbet Feb 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for my edification, why ddo we still have this method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't love that for testing it seems like we expose all of thsee things. how come?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nice. :)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm skipping the tests

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +376 to +380
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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's behavior changes happening here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is caused by actually doing serialization - the client extension converters add the whitespace classified text runs

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Member Author

@dibarbet dibarbet Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gives me nullable warnings because GetCustomAttribute returns a nullable Attribute, so this way seemed clearer than bang

return messageFormatter;
}

internal static async Task<TestLspServer> CreateAsync(TestWorkspace testWorkspace, LSP.ClientCapabilities clientCapabilities, WellKnownLspServerKinds serverKind)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really good. 😄

@dibarbet
Copy link
Member Author

thanks all for the reviews

Will address feedback from @jasonmalinowski in a separate PR to avoid dealing with more merge conflicts here.

@dibarbet dibarbet merged commit 83389ee into dotnet:main Feb 16, 2022
@ghost ghost added this to the Next milestone Feb 16, 2022
@dibarbet dibarbet deleted the ts_lsp_externalaccess_2 branch February 16, 2022 23:47
dibarbet added a commit to dibarbet/roslyn that referenced this pull request Feb 17, 2022
@RikkiGibson RikkiGibson modified the milestones: Next, 17.2.P2 Mar 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-IDE LSP issues related to the roslyn language server protocol implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants