diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 643aa9a26..1b4a8426a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1549,4 +1549,63 @@ impl Client { changes, }) } + + // Everything below is explicitly extensions used for handling non standard lsp commands + pub fn non_standard_extension( + &self, + method_name: String, + params: Option, + ) -> Option>> { + Some(self.call_non_standard(DynamicLspRequest { + method_name, + params, + })) + } + + fn call_non_standard(&self, request: DynamicLspRequest) -> impl Future> { + self.call_non_standard_with_timeout(request, self.req_timeout) + } + + fn call_non_standard_with_timeout( + &self, + request: DynamicLspRequest, + timeout_secs: u64, + ) -> impl Future> { + let server_tx = self.server_tx.clone(); + let id = self.next_request_id(); + + let params = serde_json::to_value(&request.params); + async move { + use std::time::Duration; + use tokio::time::timeout; + + let request = jsonrpc::MethodCall { + jsonrpc: Some(jsonrpc::Version::V2), + id: id.clone(), + method: (&request.method_name).to_string(), + params: Self::value_into_params(params?), + }; + + let (tx, mut rx) = channel::>(1); + + server_tx + .send(Payload::Request { + chan: tx, + value: request, + }) + .map_err(|e| Error::Other(e.into()))?; + + // TODO: delay other calls until initialize success + timeout(Duration::from_secs(timeout_secs), rx.recv()) + .await + .map_err(|_| Error::Timeout(id))? // return Timeout + .ok_or(Error::StreamClosed)? + } + } +} + +#[derive(serde::Serialize, Deserialize)] +pub struct DynamicLspRequest { + method_name: String, + params: Option, } diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs index 70a071891..227b0ab75 100644 --- a/helix-term/src/commands/engine/steel.rs +++ b/helix-term/src/commands/engine/steel.rs @@ -1924,6 +1924,33 @@ fn load_misc_api(engine: &mut Engine, generate_sources: bool) { template_function_arity_1("push-component!"); template_function_arity_1("enqueue-thread-local-callback"); + module.register_fn("send-lsp-command", send_arbitrary_lsp_command); + if generate_sources { + builtin_misc_module.push_str( + r#" + (provide send-lsp-command) + ;;@doc + ;; Send an lsp command. The `lsp-name` must correspond to an active lsp. + ;; The method name corresponds to the method name that you'd expect to see + ;; with the lsp, and the params can be passed as a hash table. The callback + ;; provided will be called with whatever result is returned from the LSP, + ;; deserialized from json to a steel value. + ;; + ;; # Example + ;; ```scheme + ;; (define (view-crate-graph) + ;; (send-lsp-command "rust-analyzer" + ;; "rust-analyzer/viewCrateGraph" + ;; (hash "full" #f) + ;; ;; Callback to run with the result + ;; (lambda (result) (displayln result)))) + ;; ``` + (define (send-lsp-command lsp-name method-name params callback) + (helix.send-lsp-command *helix.cx* lsp-name method-name params callback)) + "#, + ); + } + let mut template_function_arity_2 = |name: &str| { if generate_sources { builtin_misc_module.push_str(&format!( @@ -2829,3 +2856,95 @@ fn move_window_to_the_right(cx: &mut Context) { .is_some() {} } + +// TODO: Get LSP id correctly, and then format a json blob (or something) +// that can be serialized by serde +fn send_arbitrary_lsp_command( + cx: &mut Context, + name: SteelString, + command: SteelString, + // Arguments - these will be converted to some json stuff + json_argument: Option, + callback_fn: SteelVal, +) -> anyhow::Result<()> { + let argument = json_argument.map(|x| serde_json::Value::try_from(x).unwrap()); + + let (_view, doc) = current!(cx.editor); + + let language_server_id = anyhow::Context::context( + doc.language_servers().find(|x| x.name() == name.as_str()), + "Unable to find the language server specified!", + )? + .id(); + + let future = match cx + .editor + .language_server_by_id(language_server_id) + .and_then(|language_server| { + language_server.non_standard_extension(command.to_string(), argument) + }) { + Some(future) => future, + None => { + // TODO: Come up with a better message once we check the capabilities for + // the arbitrary thing you're trying to do, since for now the above actually + // always returns a `Some` + cx.editor.set_error( + "Language server does not support whatever command you just tried to do", + ); + return Ok(()); + } + }; + + let rooted = callback_fn.as_rooted(); + + let callback = async move { + // Result of the future - this will be whatever we get back + // from the lsp call + let res = future.await?; + + let call: Box = Box::new( + move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| { + let mut ctx = Context { + register: None, + count: None, + editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs, + }; + + let cloned_func = rooted.value(); + + ENGINE.with(move |x| { + let mut guard = x.borrow_mut(); + + match SteelVal::try_from(res) { + Ok(result) => { + if let Err(e) = guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, args| { + let context = args[0].clone(); + engine.update_value("*helix.cx*", context); + + engine.call_function_with_args( + cloned_func.clone(), + vec![result.clone()], + ) + }) + { + present_error_inside_engine_context(&mut ctx, &mut guard, e); + } + } + Err(e) => { + present_error_inside_engine_context(&mut ctx, &mut guard, e); + } + } + }) + }, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); + + Ok(()) +}