feat: specify custom lang server(s) for :lsp-stop and :lsp-restart (#12578)

Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
This commit is contained in:
Nikita Revenco 2025-01-24 00:14:35 +00:00 committed by GitHub
parent 4ded712dbd
commit a63a2ad281
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 60 deletions

View file

@ -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. |

View file

@ -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) {

View file

@ -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",

View file

@ -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();