Unnamed repository; edit this file 'description' to name the repository.
feat: specify custom lang server(s) for `:lsp-stop` and `:lsp-restart` (#12578)
Co-authored-by: Nikita Revenco <[email protected]>
Nikita Revenco 2025-01-24
parent 4ded712 · commit a63a2ad
-rw-r--r--book/src/generated/typable-cmd.md4
-rw-r--r--helix-lsp/src/lib.rs70
-rw-r--r--helix-term/src/commands/typed.rs70
-rw-r--r--helix-term/src/ui/mod.rs9
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<Vec<Arc<Client>>> {
- 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<Result<Arc<Client>>> {
+ 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<str>]) -> anyhow::Result<Vec<String>> {
+ 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<str>],
+ args: &[Cow<str>],
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<DocumentId> = 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<str>],
+ args: &[Cow<str>],
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::<Vec<_>>();
+ 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<Completion> {
+ 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<Completion> {
static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
let mut keys = Vec::new();