diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 567e8a702..c8a1691ab 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -512,6 +512,8 @@ pub enum Notification { ShowMessage(lsp::ShowMessageParams), LogMessage(lsp::LogMessageParams), ProgressMessage(lsp::ProgressParams), + // Other kind specifically for extensions + Other(String, jsonrpc::Params), } impl Notification { @@ -538,9 +540,7 @@ impl Notification { let params: lsp::ProgressParams = params.parse()?; Self::ProgressMessage(params) } - _ => { - return Err(Error::Unhandled); - } + _ => Self::Other(method.to_owned(), params), }; Ok(notification) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 46766504b..1ec99357b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -21,6 +21,7 @@ use tui::backend::Backend; use crate::{ args::Args, + commands::ScriptingEngine, compositor::{Compositor, Event}, config::Config, handlers, @@ -881,6 +882,19 @@ impl Application { // Remove the language server from the registry. self.editor.language_servers.remove_by_id(server_id); } + Notification::Other(event_name, params) => { + let server_id = server_id; + + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + scroll: None, + jobs: &mut self.jobs, + }; + + ScriptingEngine::handle_lsp_notification( + &mut cx, server_id, event_name, params, + ); + } } } Call::MethodCall(helix_lsp::jsonrpc::MethodCall { diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index 6a5abbfd4..2bfb767b3 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -1,5 +1,6 @@ use arc_swap::{ArcSwap, ArcSwapAny}; use helix_core::syntax; +use helix_lsp::{jsonrpc, LanguageServerId}; use helix_view::{document::Mode, input::KeyEvent}; use std::{borrow::Cow, sync::Arc}; @@ -140,6 +141,23 @@ impl ScriptingEngine { .collect() } + pub fn handle_lsp_notification( + cx: &mut compositor::Context, + server_id: LanguageServerId, + event_name: String, + params: jsonrpc::Params, + ) { + for kind in PLUGIN_PRECEDENCE { + if manual_dispatch!( + kind, + // TODO: Get rid of these clones! + handle_lsp_notification(cx, server_id, event_name.clone(), params.clone()) + ) { + return; + } + } + } + pub fn generate_sources() { for kind in PLUGIN_PRECEDENCE { manual_dispatch!(kind, generate_sources()) @@ -160,6 +178,7 @@ pub trait PluginSystem { /// this is done here. This is run before the context is available. fn initialize(&self) {} + #[allow(unused)] fn engine_name(&self) -> PluginSystemKind; /// Post initialization, once the context is available. This means you should be able to @@ -207,6 +226,19 @@ pub trait PluginSystem { false } + /// Call into the scripting engine to handle an unhandled LSP notification, sent from the server + /// to the client. + #[inline(always)] + fn handle_lsp_notification( + &self, + _cx: &mut compositor::Context, + _server_id: LanguageServerId, + _event_name: String, + _params: jsonrpc::Params, + ) -> bool { + false + } + /// Given an identifier, extract the documentation from the engine. #[inline(always)] fn get_doc_for_identifier(&self, _ident: &str) -> Option { diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs index 82aa0823f..87638f1af 100644 --- a/helix-term/src/commands/engine/steel.rs +++ b/helix-term/src/commands/engine/steel.rs @@ -6,8 +6,8 @@ use helix_core::{ extensions::steel_implementations::{rope_module, SteelRopeSlice}, find_workspace, graphemes, syntax::config::{ - default_timeout, AutoPairConfig, IndentationConfiguration, LanguageConfiguration, - LanguageServerConfiguration, SoftWrap, + default_timeout, AutoPairConfig, LanguageConfiguration, LanguageServerConfiguration, + SoftWrap, }, syntax::{self}, text_annotations::InlineAnnotation, @@ -33,11 +33,12 @@ use once_cell::sync::{Lazy, OnceCell}; use steel::{ compiler::modules::steel_home, gc::{unsafe_erased_pointers::CustomReference, ShareableMut}, + rerrs::ErrorKind, rvals::{as_underlying_type, AsRefMutSteelVal, FromSteelVal, IntoSteelVal, SteelString}, steel_vm::{ engine::Engine, mutex_lock, mutex_unlock, register_fn::RegisterFn, ThreadStateController, }, - steelerr, SteelErr, SteelVal, + steelerr, RootedSteelVal, SteelErr, SteelVal, }; use std::{ @@ -194,6 +195,18 @@ pub static BUFFER_EXTENSION_KEYMAP: Lazy> = Lazy:: }) }); +pub static LSP_NOTIFICATION_REGISTRY: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + +fn register_lsp_notification_callback(lsp: String, kind: String, function: SteelVal) { + let rooted = function.as_rooted(); + + LSP_NOTIFICATION_REGISTRY + .write() + .unwrap() + .insert((lsp, kind), rooted); +} + pub struct BufferExtensionKeyMap { map: HashMap, reverse: HashMap, @@ -676,6 +689,11 @@ fn dynamic_set_option( fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { let mut module = BuiltInModule::new("helix/core/configuration"); + module.register_fn( + "register-lsp-notification-handler", + register_lsp_notification_callback, + ); + module.register_fn("update-configuration!", |ctx: &mut Context| { ctx.editor .config_events @@ -852,6 +870,28 @@ fn load_configuration_api(engine: &mut Engine, generate_sources: bool) { let mut builtin_configuration_module = r#"(require-builtin helix/core/configuration as helix.) +(provide register-lsp-notification-handler) + +;;@doc +;; Register a callback to be called on LSP notifications sent from the server -> client +;; that aren't currently handled by Helix as a built in. +;; +;; ```scheme +;; (register-lsp-notification-handler lsp-name event-name handler) +;; ``` +;; +;; * lsp-name : string? +;; * event-name : string? +;; * function : (-> hash? any?) ;; Function where the first argument is the parameters +;; +;; # Examples +;; ``` +;; (register-lsp-notification-handler "dart" +;; "dart/textDocument/publishClosingLabels" +;; (lambda (args) (displayln args))) +;; ``` +(define register-lsp-notification-handler helix.register-lsp-notification-handler) + (provide set-option!) (define (set-option! key value) (helix.set-option! *helix.config* key value)) @@ -1817,7 +1857,7 @@ impl super::PluginSystem for SteelScriptingEngine { fn call_function_by_name(&self, cx: &mut Context, name: &str, args: &[Cow]) -> bool { if enter_engine(|x| x.global_exists(name)) { - let args = args + let mut args = args .iter() .map(|x| x.clone().into_steelval().unwrap()) .collect::>(); @@ -1831,9 +1871,8 @@ impl super::PluginSystem for SteelScriptingEngine { move |engine, arguments| { let context = arguments[0].clone(); engine.update_value("*helix.cx*", context); - - // TODO: Get rid of this clone - engine.call_function_by_name_with_args(name, args.clone()) + engine + .call_function_by_name_with_args_from_mut_slice(name, &mut args) }, ) }) @@ -1947,6 +1986,74 @@ impl super::PluginSystem for SteelScriptingEngine { steel_doc::walk_dir(&mut writer, target, &mut engine).unwrap(); } } + + // TODO: Should this just be a hook / event instead of a function like this? + // Handle an LSP notification, assuming its been sent through + fn handle_lsp_notification( + &self, + cx: &mut compositor::Context, + server_id: helix_lsp::LanguageServerId, + event_name: String, + params: helix_lsp::jsonrpc::Params, + ) -> bool { + if let Err(e) = enter_engine(|guard| { + { + let mut ctx = Context { + register: None, + count: None, + editor: &mut cx.editor, + callback: Vec::new(), + on_next_key_callback: None, + jobs: &mut cx.jobs, + }; + + let language_server_name = ctx + .editor + .language_servers + .get_by_id(server_id) + .map(|x| x.name().to_owned()); + + if language_server_name.is_none() { + ctx.editor.set_error("Unable to find language server"); + } + + let language_server_name = language_server_name.unwrap(); + + let function = LSP_NOTIFICATION_REGISTRY + .read() + .unwrap() + .get(&(language_server_name, event_name)) + .map(|x| x.value()) + .cloned(); + + if let Some(function) = function { + // Install the interrupt handler, in the event this thing + // is blocking for too long. + with_interrupt_handler(|| { + guard + .with_mut_reference::(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + let params = serde_json::to_value(¶ms) + .map_err(|e| SteelErr::new(ErrorKind::Generic, e.to_string())) + .and_then(|x| x.into_steelval())?; + + let args = vec![params]; + + engine.call_function_with_args(function.clone(), args) + }) + }) + } else { + Ok(SteelVal::Void) + } + } + }) { + cx.editor.set_error(format!("{}", e)); + } + true + } } impl SteelScriptingEngine { @@ -2959,7 +3066,6 @@ fn register_hook(event_kind: String, callback_fn: SteelVal) -> steel::UnRecovera let context = args[0].clone(); engine.update_value("*helix.cx*", context); let mut args = [minimized_event.into_steelval().unwrap()]; - // engine.call_function_by_name_with_args(&function_name, args) engine.call_function_with_args_from_mut_slice( rooted.value().clone(), &mut args,