openlawlibrary / pygls

A pythonic generic language server
https://pygls.readthedocs.io/en/latest/
Apache License 2.0
588 stars 105 forks source link

How to register a command and run it in one step? #476

Open noklam opened 4 months ago

noklam commented 4 months ago

I found this section in the docs: https://pygls.readthedocs.io/en/v0.10.2/pages/advanced_usage.html#commands The entrypoint is registered in the playground: https://github.com/openlawlibrary/pygls/blob/a40a3c3ad18943503e29cbd61586545974d4dbc8/.vscode/extensions/pygls-playground/package.json#L29-L32

While this works fine, it involves two steps to execute a command:

  1. Execute pygls command
  2. Select the corresponding one

Is there a way to combine these into 1 step? The alternative is using the VSCode API directly https://code.visualstudio.com/api/extension-guides/command, but it has some downside:

  1. I have to write TS, I am much more familiar with Python
  2. It cannot access information on the server side.

It seems like this is possible by modifying https://github.com/openlawlibrary/pygls/blob/a40a3c3ad18943503e29cbd61586545974d4dbc8/.vscode/extensions/pygls-playground/src/extension.ts#L244-L248, is there any tradeoff implementing this on the server side instead of client?

alcarney commented 4 months ago

The 2-step process is just a quirk from the fact the playground tries to make it possible to call a command from any server, without knowing up front what those commands are called.

Assuming that your command does not require any arguments, you should be able to replace the value of command in https://github.com/openlawlibrary/pygls/blob/a40a3c3ad18943503e29cbd61586545974d4dbc8/.vscode/extensions/pygls-playground/package.json#L29-L32

with the id of your custom command (the string you pass to @server.command()). It should then be possible to call your command directly from the VSCode command palette.

However, if your command does take arguments or you need to process the return value in any way then I don't think you can avoid writing some TS! 😅

noklam commented 3 months ago

Coming back to this after one month! I haven't tried this yet. We are working on a visual component and currently exploring if it's possible to add interaction between a webview and the LSP.

I have been thinking there is two possible ways:

  1. Send request directly through the language client, but I cannot find any API that I can use.
  2. Use VSCode command, basically when I click on a React component, it triggers the VSCode action (argument will be extracted from the react component), and trigger the LSP response.
alcarney commented 3 months ago

Send request directly through the language client, but I cannot find any API that I can use.

There are generic sendRequest and sendNotification methods on the VSCode language client.

I use them to send a few custom messages to esbonio, but I imagine they would work fine for standard LSP messages also

add interaction between a webview and the LSP

It's also possible to setup a web socket connection with a pygls powered server

Yes, it's more complicated than routing the message via VSCode's Webview API, but it does open the door to the webview working with editors other than VSCode.

noklam commented 3 months ago

There are generic sendRequest and sendNotification methods on the VSCode language client.

Thanks for giving some pointer. I tried to implement this today and using the lsClient in Typescript directly to sendRequest. It seems like the the server is communicating properly but VSCode UI does not respond to it at all.

The request looks like this in the client (it's hard coded now as I just want to test if this mechanism works)

    lsClient?.sendRequest("textDocument/definition",
        {
            "textDocument": {
                "uri": "file:///Users/Nok_Lam_Chan/dev/kedro/tmp/spaceflights/src/spaceflights/pipelines/data_science/pipeline.py"
            },
            "position": {
                "line": 11,
                "character": 29
            }
        }
    );

I register this function in a command so I can invoke it with the UI.

export async function sendDefinitionRequest(lsClient: LanguageClient | undefined) {
    lsClient?.sendRequest("textDocument/definition",
  ...

After that, the server respond with the correct response but the cursor didn't move.

2024-08-13 14:35:54.996 [info] [Trace - 14:35:54] Received response 'textDocument/definition - (47)' in 22ms. 2024-08-13 14:35:54.996 [info] Result: [ { "uri": "file:///Users/Nok_Lam_Chan/dev/kedro/tmp/spaceflights/conf/base/catalog.yml", "range": { "start": { "line": 43, "character": 0 }, "end": { "line": 44, "character": 0 } } } ]

Do I need to handle the response from the client side or is that something VSCode take care of automatically?

noklam commented 3 months ago

command-request

The command is mainly for testing, it mimics when I do cmd+click on a string. I manage to reproduce the navigation function, but I wonder is this necessary. Since in the case of using pygls, I only need to declare @LSP_SERVER.feature(TEXT_DOCUMENT_DEFINITION) and the UI know how to respond to the server.

For now I am using the vscode.window api on the client side.

alcarney commented 3 months ago

Do I need to handle the response from the client side or is that something VSCode take care of automatically?

Yes, you would need to handle the response as using sendRequest is going to bypass the "internal wiring" setup by VSCode's language client. Here for example is how the client usually handles TEXT_DOCUMENT_DEFINITION.

To be honest... I don't know what the "right" way to call TEXT_DOCUMENT_DEFINITION programmatically so that the result is automatically handled. Since the example above is implementing an API that allows VSCode to decide when TEXT_DOCUMENT_DEFINITION should be called.

The command is mainly for testing,

Maybe I'm reading too much into your example, but from the gif it looks like you might want to implement the workspace/symbol request?

noklam commented 3 months ago

@alcarney Adding symbol would be interesting but I don't think it's suitable in this case. The idea in mind is to embedded this into webview and triggering the Go to Definition from the graph. So the command is temporary for testing just because it is handy to trigger it manually. It won't be needed once we integrate it properly.

bypass the "internal wiring" setup by VSCode's language client.

This is the logic that I was looking for, but at the end I implemented a simple one just to get started. It roughly looks like this.

    if (result && result.length > 0) {
        const location = result[0];
        const uri: vscode.Uri = vscode.Uri.parse(location.uri);
        const range = location.range;

        vscode.window.showTextDocument(uri,
            {
                selection: range,
                viewColumn: vscode.ViewColumn.One,
            }
        );

Maybe I'm reading too much into your example, but from the gif it looks like you might want to implement the https://github.com/openlawlibrary/pygls/pull/470 request?

Since you mentioned symbol, is there any example that I can look into. Right now the LSP do the analysis on the demand, I think I will need to build these symbols with cache.

alcarney commented 3 months ago

Since you mentioned symbol, is there any example that I can look into. Right now the LSP do the analysis on the demand, I think I will need to build these symbols with cache.

There is the example symbols.py server which maintains an internal index of all the known symbols.

Other than that you could go through some of the servers in Implementations.md and see if/how they handle it

noklam commented 2 months ago

We actually implemented a custom command that that a word as an argument. But seems like we could just use vscode.executeDefinitionProvider, since our own LSP is already registered as a definition provider, it's not necessary to call the LSP directly. Our LSP is build around a Python DSL with YAML.

vscode.commands.executeCommand<vscode.Location[]>(
                'vscode.executeDefinitionProvider',
                document.uri,
                position,
            );

This is what it looks like now. viz-flow