ebkalderon / tower-lsp

Language Server Protocol implementation written in Rust
Apache License 2.0
1.01k stars 57 forks source link

custom methods for notification without params not called #409

Open Kryx-Ikyr opened 8 months ago

Kryx-Ikyr commented 8 months ago

Hello,

I'm working on a language server that is using custom notifications. I can't manage to make them work, and I think it could be coming from tower-lsp.

Here is my code:

use backend::Backend;
use tower_lsp::{LspService, Server};

#[derive(Debug)]
pub struct Backend {
    pub client: Client,
}

#[tower_lsp::async_trait]
impl LanguageServer for Backend {
    async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
        Ok(InitializeResult::default())
    }

    async fn initialized(&self, _: InitializedParams) {
        let options = DidChangeWatchedFilesRegistrationOptions {
            watchers: vec![
                FileSystemWatcher {
                    glob_pattern: GlobPattern::String("**".to_string()),
                    kind: Some(WatchKind::Change),
                },
            ],
        };
        match self.client.register_capability(vec![
            Registration {
                id: "workspace/didChangeWatchedFiles".to_string(),
                method: "workspace/didChangeWatchedFiles".to_string(),
                register_options: Some(to_value(options).unwrap()),
            },
            Registration {
                id: "workspace/didChangeConfiguration".to_string(),
                method: "workspace/didChangeConfiguration".to_string(),
                register_options: None,
            },
        ]).await {
            Ok(_) => (),
            Err(e) => self.client.log_message(MessageType::ERROR, format!("Error registering capabilities: {:?}", e)).await,
        }
        self.client.log_message(MessageType::INFO, "server initialized!").await;
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }
}

impl Backend {
    pub async fn client_config_changed(&self) {

    }

    pub async fn client_ready(&self) {   // <------- Implementation of my custom method
        self.client.log_message(MessageType::INFO, format!("Client ready !")).await
    }
}

#[tokio::main]
async fn main() {
    println!("starting server");
    let debug = true;
    if debug {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:2087").await.unwrap();

        loop {
            let (stream, _) = listener.accept().await.unwrap();
            let (reader, writer) = tokio::io::split(stream);
            let (service, messages) = LspService::build(|client| Backend { client })
                .custom_method("custom/configurationChanged", Backend::client_config_changed)
                .custom_method("custom/clientReady", Backend::client_ready)  //<-------- Here I register my custom method
                .finish();
            let server = Server::new(reader, writer, messages);
            tokio::spawn(async move {
                server.serve(service).await;
            });
        }
    } else {
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();

        let (service, socket) = LspService::new(|client| Backend { client, odoo:None });
        Server::new(stdin, stdout, socket).serve(service).await;
    }
}

In this example, client_ready is never called, but if I put breakpoints in tower-lsp, it seems that the event is catched, the related method is registered the Router and should be called, but failed to do it. I'm quite new to Rust and I can't understand why my function isn't called. Is it something from my code?

From what I see, the call is stopped here:

C:\Users\XX.cargo\registry\src\index.crates.io-6f17d22bba15001f\tower-lsp-0.20.0\src\jsonrpc\router.rs

impl<P, R, E> Service<Request> for MethodHandler<P, R, E>
where
    P: FromParams,
    R: IntoResponse,
    E: Send + 'static,
{
    type Response = Option<Response>;
    type Error = E;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request) -> Self::Future {
        let (_, id, params) = req.into_parts();

        match id {
            Some(_) if R::is_notification() => return future::ok(().into_response(id)).boxed(),
            None if !R::is_notification() => return future::ok(None).boxed(),
            _ => {}
        }

        let params = match P::from_params(params) {
            Ok(params) => params,
            Err(err) => return future::ok(id.map(|id| Response::from_error(id, err))).boxed(),
        };

        (self.f)(params)
            .map(move |r| Ok(r.into_response(id)))
            .boxed()
    }
}

Thank you for your help !

EDIT: To give more information, the notification is sent from vscode, from a custom extension that is calling

client.sendNotification(
    "custom/clientReady",
);

in typescript. This extension is used with another language server in Python that is working. Moreover I can see tower-lsp catching the message, so I don't think that the issue could come from the client.

ratmice commented 8 months ago

It has been long enough since I implemented custom methods in my lsp, that I forget most of the implementation details. However I can at the very least say that it did work at that time (I appear to be a few releases behind and still on 0.18 though) https://github.com/ratmice/nimbleparse_lsp/blob/main/server/src/main.rs#L703-L706

Sorry if I can't be more helpful at the moment.

Kryx-Ikyr commented 8 months ago

It's weird, I don't see what is different in my code. However I'm wondering if it could be related to the params. In my case it is a notification, without params or return value. And from what I see tower-lsp is stopping the propagation on the build of the parameters, so it is maybe related. I will try later (I'm not on my computer) to add a fake parameter to test it.

Kryx-Ikyr commented 8 months ago

Unfortunately, I tested everything, but can't manage to make it work. It seems that the json sent from vscode for notification contains an array of 1-length as params with a null value as first param. From what I see, tower-lsp struggle to deserialize it, but I can totally be wrong, I'm new to Rust :) Some help would be really appreciated ;)

Kryx-Ikyr commented 8 months ago

If I change my method in Rust to:

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ReadyParams {
    value1: u32,
}

pub async fn client_ready(&self, params: ReadyParams) {
    self.client.log_message(MessageType::INFO, format!("Client ready !")).await
}

To add a custom parameter, and edit the typescript to send this parameter:

client.sendNotification(
     "Odoo/clientReady",
      {
       "value1": 5
      }
);

my custom method is called. I think tower-lsp doesn't accept notification (and maybe requests) without parameters. From what I see, it happens here:

https://github.com/ebkalderon/tower-lsp/blob/49e1ce54549d5efc53b75510517c2f0b86f5c827/src/jsonrpc/router.rs#L210

Where let Some(p) = params is valid because params is not empty but contains a null value, so it is raising an error

blmarket commented 7 months ago

custom_method is to define custom method, not custom notification handler. I don't think tower-lsp currently handles custom notifications...

Simply speaking, how about just work with custom methods for now, and ignore the response? it will just do the work for now.

Kryx-Ikyr commented 7 months ago

As a notification is a method without parameter, custom_method should handle them too, and the documentation of tower-lsp states it clearly: "Defines a custom JSON-RPC request or notification with the given method name and handler." "Handlers may optionally include a single params argument."

But I managed to solve my issue, and I think it is coming from vscode, and not really tower-lsp. When you use the LSP API of vscode and call the "send_notification("custom_name"); " method, their are building the parameters to send with the selected method. As there is no parameter here, javascript is setting it to the default value: null.

It results in a call of the method "custom_name" with the parameter 'null' in tower-lsp, which doesn't exist. For me it's the vscode API that doesn't follow their own documentation... I solved my issue by adding a dummy parameter to my notifications and they are working like a charm now :)