From a63a2ad281b5f651effd29efa4e34f504507d0da Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+nik-rev@users.noreply.github.com> Date: Fri, 24 Jan 2025 00:14:35 +0000 Subject: [PATCH] feat: specify custom lang server(s) for `:lsp-stop` and `:lsp-restart` (#12578) Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> --- book/src/generated/typable-cmd.md | 4 +- helix-lsp/src/lib.rs | 70 ++++++++++++++----------------- helix-term/src/commands/typed.rs | 70 ++++++++++++++++++++++--------- helix-term/src/ui/mod.rs | 9 ++++ 4 files changed, 93 insertions(+), 60 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index f0d9a0f4..55820e08 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -52,8 +52,8 @@ | `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. | | `:update`, `:u` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | -| `:lsp-restart` | Restarts the language servers used by the current doc | -| `:lsp-stop` | Stops the language servers that are used by the current doc | +| `:lsp-restart` | Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied | +| `:lsp-stop` | Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index fd5cdb8b..7ece350a 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -618,51 +618,45 @@ impl Registry { Ok(self.inner[id].clone()) } - /// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers, - /// as it could be that language servers of these documents were stopped by this method. + /// If this method is called, all documents that have a reference to the language server have to refresh their language servers, /// See helix_view::editor::Editor::refresh_language_servers - pub fn restart( + pub fn restart_server( &mut self, + name: &str, language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], enable_snippets: bool, - ) -> Result>> { - language_config - .language_servers - .iter() - .filter_map(|LanguageServerFeatures { name, .. }| { - if let Some(old_clients) = self.inner_by_name.remove(name) { - if old_clients.is_empty() { - log::info!("restarting client for '{name}' which was manually stopped"); - } else { - log::info!("stopping existing clients for '{name}'"); - } - for old_client in old_clients { - self.file_event_handler.remove_client(old_client.id()); - self.inner.remove(old_client.id()); - tokio::spawn(async move { - let _ = old_client.force_shutdown().await; - }); - } - } - let client = match self.start_client( - name.clone(), - 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()]); + ) -> Option>> { + if let Some(old_clients) = self.inner_by_name.remove(name) { + if old_clients.is_empty() { + log::info!("restarting client for '{name}' which was manually stopped"); + } else { + log::info!("stopping existing clients for '{name}'"); + } + for old_client in old_clients { + self.file_event_handler.remove_client(old_client.id()); + self.inner.remove(old_client.id()); + tokio::spawn(async move { + let _ = old_client.force_shutdown().await; + }); + } + } + 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)) - }) - .collect() + Some(Ok(client)) } pub fn stop(&mut self, name: &str) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 4a2546d7..8a335674 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1476,9 +1476,34 @@ fn lsp_workspace_command( Ok(()) } +/// Returns all language servers used by the current document if no servers are supplied +/// If servers are supplied, do a check to make sure that all of the servers exist +fn valid_lang_servers(doc: &Document, servers: &[Cow]) -> anyhow::Result> { + let valid_ls_names = doc + .language_servers() + .map(|ls| ls.name().to_string()) + .collect(); + + if servers.is_empty() { + Ok(valid_ls_names) + } else { + let (valid, invalid): (Vec<_>, Vec<_>) = servers + .iter() + .map(|m| m.to_string()) + .partition(|ls| valid_ls_names.contains(ls)); + + if !invalid.is_empty() { + let s = if invalid.len() == 1 { "" } else { "s" }; + bail!("Unknown language server{s}: {}", invalid.join(", ")); + }; + + Ok(valid) + } +} + fn lsp_restart( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1486,17 +1511,25 @@ fn lsp_restart( } let editor_config = cx.editor.config.load(); - let (_view, doc) = current!(cx.editor); + let doc = doc!(cx.editor); let config = doc .language_config() .context("LSP not defined for the current document")?; - cx.editor.language_servers.restart( - config, - doc.path(), - &editor_config.workspace_lsp_roots, - editor_config.lsp.snippets, - )?; + let ls_restart_names = valid_lang_servers(doc, args)?; + + for server in ls_restart_names.iter() { + cx.editor + .language_servers + .restart_server( + server, + config, + doc.path(), + &editor_config.workspace_lsp_roots, + editor_config.lsp.snippets, + ) + .transpose()?; + } // This collect is needed because refresh_language_server would need to re-borrow editor. let document_ids_to_refresh: Vec = cx @@ -1505,10 +1538,9 @@ fn lsp_restart( .filter_map(|doc| match doc.language_config() { Some(config) if config.language_servers.iter().any(|ls| { - config - .language_servers + ls_restart_names .iter() - .any(|restarted_ls| restarted_ls.name == ls.name) + .any(|restarted_ls| restarted_ls == &ls.name) }) => { Some(doc.id()) @@ -1526,17 +1558,15 @@ fn lsp_restart( fn lsp_stop( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + let doc = doc!(cx.editor); - let ls_shutdown_names = doc!(cx.editor) - .language_servers() - .map(|ls| ls.name().to_string()) - .collect::>(); + let ls_shutdown_names = valid_lang_servers(doc, args)?; for ls_name in &ls_shutdown_names { cx.editor.language_servers.stop(ls_name); @@ -2910,16 +2940,16 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "lsp-restart", aliases: &[], - doc: "Restarts the language servers used by the current doc", + doc: "Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied", fun: lsp_restart, - signature: CommandSignature::none(), + signature: CommandSignature::all(completers::language_servers), }, TypableCommand { name: "lsp-stop", aliases: &[], - doc: "Stops the language servers that are used by the current doc", + doc: "Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied", fun: lsp_stop, - signature: CommandSignature::none(), + signature: CommandSignature::all(completers::language_servers), }, TypableCommand { name: "tree-sitter-scopes", diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d5eaaefc..a7779be2 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -410,6 +410,15 @@ pub mod completers { } } + pub fn language_servers(editor: &Editor, input: &str) -> Vec { + let language_servers = doc!(editor).language_servers().map(|ls| ls.name()); + + fuzzy_match(input, language_servers, false) + .into_iter() + .map(|(name, _)| ((0..), Span::raw(name.to_string()))) + .collect() + } + pub fn setting(_editor: &Editor, input: &str) -> Vec { static KEYS: Lazy> = Lazy::new(|| { let mut keys = Vec::new();