From 0c308f67f55a97c435283364cc25b1280968670d Mon Sep 17 00:00:00 2001 From: tinger Date: Wed, 12 Nov 2025 13:24:39 +0100 Subject: [PATCH] feat: lsp: Add :lsp-toggle --- Cargo.lock | 94 +++++++++++++++++++++++++++++++ helix-lsp/src/lib.rs | 49 ++++++++++++++++ helix-term/src/commands/typed.rs | 96 ++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c3f6925e..f137266c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,15 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -176,7 +185,9 @@ version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ + "iana-time-zone", "num-traits", + "windows-link", ] [[package]] @@ -252,6 +263,12 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1811,6 +1828,30 @@ dependencies = [ "typenum", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -3246,12 +3287,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index d99014e0..469413e3 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -702,6 +702,55 @@ impl Registry { } } + pub fn toggle( + &mut self, + name: &str, + language_config: &LanguageConfiguration, + doc_path: Option<&std::path::Path>, + root_dirs: &[PathBuf], + enable_snippets: bool, + ) -> Option>> { + let restart = if let Some(clients) = self.inner_by_name.get_mut(name) { + let restart = if clients.is_empty() { + log::info!("restarting client for '{name}' which was manually stopped"); + true + } else { + log::info!("stopping existing clients for '{name}'"); + false + }; + for client in clients.drain(..) { + self.file_event_handler.remove_client(client.id()); + self.inner.remove(client.id()); + tokio::spawn(async move { + let _ = client.force_shutdown(); + }); + } + restart + } else { + true + }; + + if restart { + let client = match self.start_client( + name.to_string(), + language_config, + doc_path, + root_dirs, + enable_snippets, + ) { + Ok(client) => client, + Err(StartupError::NoRequiredRootFound) => return None, + Err(StartupError::Error(err)) => return Some(Err(err)), + }; + self.inner_by_name + .insert(name.to_owned(), vec![client.clone()]); + + Some(Ok(client)) + } else { + None + } + } + pub fn get<'a>( &'a mut self, language_config: &'a LanguageConfiguration, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 9a5bf971..5bf70c03 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1923,6 +1923,91 @@ fn lsp_stop(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> any Ok(()) } +fn lsp_toggle(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let editor_config = cx.editor.config.load(); + let doc = doc!(cx.editor); + let config = doc + .language_config() + .context("LSP not defined for the current document")?; + + let language_servers: Vec<_> = config + .language_servers + .iter() + .map(|ls| ls.name.as_str()) + .collect(); + let language_servers = if args.is_empty() { + language_servers + } else { + let (valid, invalid): (Vec<_>, Vec<_>) = args + .iter() + .map(|arg| arg.as_ref()) + .partition(|name| language_servers.contains(name)); + if !invalid.is_empty() { + let s = if invalid.len() == 1 { "" } else { "s" }; + bail!("Unknown language server{s}: {}", invalid.join(", ")); + } + valid + }; + + let mut errors = Vec::new(); + for server in language_servers.iter() { + match cx + .editor + .language_servers + .toggle( + server, + config, + doc.path(), + &editor_config.workspace_lsp_roots, + editor_config.lsp.snippets, + ) + .transpose() + { + // Ignore the executable-not-found error unless the server was explicitly requested + // in the arguments. + Err(helix_lsp::Error::ExecutableNotFound(_)) + if !args.iter().any(|arg| arg == server) => {} + Err(err) => errors.push(err.to_string()), + _ => (), + } + } + + // This collect is needed because refresh_language_server would need to re-borrow editor. + let document_ids_to_refresh: Vec = cx + .editor + .documents() + .filter_map(|doc| match doc.language_config() { + Some(config) + if config.language_servers.iter().any(|ls| { + language_servers + .iter() + .any(|restarted_ls| restarted_ls == &ls.name) + }) => + { + Some(doc.id()) + } + _ => None, + }) + .collect(); + + for document_id in document_ids_to_refresh { + cx.editor.refresh_language_servers(document_id); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Error restarting language servers: {}", + errors.join(", ") + )) + } +} + fn tree_sitter_scopes( cx: &mut compositor::Context, _args: Args, @@ -3664,6 +3749,17 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ ..Signature::DEFAULT }, }, + TypableCommand { + name: "lsp-toggle", + aliases: &[], + doc: "Toggles the given language servers, or all language servers that are used by the current file if no arguments are supplied", + fun: lsp_toggle, + completer: CommandCompleter::all(completers::configured_language_servers), + signature: Signature { + positionals: (0, None), + ..Signature::DEFAULT + }, + }, TypableCommand { name: "tree-sitter-scopes", aliases: &[], -- 2.51.2