From 5dd9082d05e4c9eb13623c1281fb7c4b0071b3a3 Mon Sep 17 00:00:00 2001 From: Xin Zhao Date: Thu, 7 May 2026 06:38:44 +0800 Subject: [PATCH] bash: Add built-in language server support (#52811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces native LSP support for Bash by integrating `bash-language-server`. Combined with the existing Tree-sitter grammar, Zed now provides a complete, out-of-the-box development experience for shell scripting. The implementation is very similar to other npm-managed language servers. With `shellcheck` installed, standard LSP features—including diagnostics, code actions, go-to-definition, find-references, and code completion—work as expected. Since I am not a frequent user of Bash, I have intentionally limited this implementation to a standard, "out-of-the-box" setup. I lack the hands-on experience to identify specific pain points or advanced LSP features that might require custom integration, so I've avoided adding any speculative or specialized configurations, especially within the `LspAdapter` trait. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #51917 Release Notes: - Added built-in language server support for Bash --------- Co-authored-by: Finn Evers --- crates/extension_host/src/extension_host.rs | 2 +- crates/languages/src/bash.rs | 149 ++++++++++++++++++++ crates/languages/src/lib.rs | 2 + docs/src/languages.md | 2 +- docs/src/languages/bash.md | 4 +- 5 files changed, 155 insertions(+), 4 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 8a15148c8f4c3448c2978ab1c05512a4ffaf7704..a59f93610d5bee30fa579bf209341bc5ddbb92ba 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -77,7 +77,7 @@ const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); /// /// These snippets should no longer be downloaded or loaded, because their /// functionality has been integrated into the core editor. -const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright"]; +const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright", "basher"]; /// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. pub fn schema_version_range() -> RangeInclusive { diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs index a947eefd13d2dabe25ba06eaba82d560ee6fbb1a..a002968fa4041b15142774d742811c09d3b7b509 100644 --- a/crates/languages/src/bash.rs +++ b/crates/languages/src/bash.rs @@ -1,5 +1,14 @@ +use anyhow::Result; +use async_trait::async_trait; +use collections::HashMap; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain}; +use lsp::LanguageServerBinary; +use node_runtime::{NodeRuntime, VersionStrategy}; use project::ContextProviderWithTasks; +use semver::Version; +use std::{path::PathBuf, vec}; use task::{TaskTemplate, TaskTemplates, VariableName}; +use util::{ResultExt, maybe}; pub(super) fn bash_task_context() -> ContextProviderWithTasks { ContextProviderWithTasks::new(TaskTemplates(vec![ @@ -17,6 +26,146 @@ pub(super) fn bash_task_context() -> ContextProviderWithTasks { ])) } +pub struct BashLspAdapter { + node: NodeRuntime, +} + +impl BashLspAdapter { + const PACKAGE_NAME: &str = "bash-language-server"; + const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "bash-language-server/out/cli.js"; + + pub fn new(node: NodeRuntime) -> Self { + Self { node } + } + + async fn get_cached_server_binary( + container_dir: PathBuf, + env: HashMap, + node: &NodeRuntime, + ) -> Option { + maybe!(async { + let server_path = container_dir + .join("node_modules") + .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH); + anyhow::ensure!( + server_path.exists(), + "missing executable in directory {server_path:?}" + ); + Ok(LanguageServerBinary { + path: node.binary_path().await?, + env: Some(env), + arguments: vec![server_path.into(), "start".into()], + }) + }) + .await + .log_err() + } +} + +impl LspInstaller for BashLspAdapter { + type BinaryVersion = Version; + + async fn cached_server_binary( + &self, + container_dir: std::path::PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let env = delegate.shell_env().await; + Self::get_cached_server_binary(container_dir, env, &self.node).await + } + + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + _: Option, + _: &gpui::AsyncApp, + ) -> Option { + let path = delegate.which(Self::PACKAGE_NAME.as_ref()).await?; + let env = delegate.shell_env().await; + + Some(LanguageServerBinary { + path, + env: Some(env), + arguments: vec!["start".into()], + }) + } + + async fn check_if_version_installed( + &self, + version: &Self::BinaryVersion, + container_dir: &PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let server_path = container_dir + .join("node_modules") + .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH); + + let should_install_language_server = self + .node + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + container_dir, + VersionStrategy::Latest(version), + ) + .await; + + if should_install_language_server { + None + } else { + let env = delegate.shell_env().await; + Some(LanguageServerBinary { + path: self.node.binary_path().await.ok()?, + env: Some(env), + arguments: vec![server_path.into(), "start".into()], + }) + } + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + _: bool, + _: &mut gpui::AsyncApp, + ) -> Result { + self.node + .npm_package_latest_version(Self::PACKAGE_NAME) + .await + } + + async fn fetch_server_binary( + &self, + latest_version: Self::BinaryVersion, + container_dir: std::path::PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let server_path = container_dir + .join("node_modules") + .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH); + + self.node + .npm_install_packages( + &container_dir, + &[(Self::PACKAGE_NAME, &latest_version.to_string())], + ) + .await?; + + let env = delegate.shell_env().await; + Ok(LanguageServerBinary { + path: self.node.binary_path().await?, + env: Some(env), + arguments: vec![server_path.into(), "start".into()], + }) + } +} + +#[async_trait(?Send)] +impl LspAdapter for BashLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName::new_static(Self::PACKAGE_NAME) + } +} + #[cfg(test)] mod tests { use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext}; diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 9010bbde022e765b53ccceec042a075f85fc102b..fe07a3f998859edba084271c53ed8e01dc78eee3 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -57,6 +57,7 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime #[cfg(feature = "load-grammars")] languages.register_native_grammars(grammars::native_grammars()); + let bash_lsp_adapter = Arc::new(bash::BashLspAdapter::new(node.clone())); let c_lsp_adapter = Arc::new(c::CLspAdapter); let css_lsp_adapter = Arc::new(css::CssLspAdapter::new(node.clone())); let eslint_adapter = Arc::new(eslint::EsLintLspAdapter::new(node.clone(), fs.clone())); @@ -88,6 +89,7 @@ pub fn init(languages: Arc, fs: Arc, node: NodeRuntime LanguageInfo { name: "bash", context: Some(Arc::new(bash::bash_task_context())), + adapters: vec![bash_lsp_adapter], ..Default::default() }, LanguageInfo { diff --git a/docs/src/languages.md b/docs/src/languages.md index 4b96e551cedab47e834697c6f5756936acaea751..b720e725cca816943524d10d990d1e0330bf25e9 100644 --- a/docs/src/languages.md +++ b/docs/src/languages.md @@ -15,7 +15,7 @@ Some work out-of-the box and others rely on 3rd party extensions. - [Ansible](./languages/ansible.md) - [AsciiDoc](./languages/asciidoc.md) - [Astro](./languages/astro.md) -- [Bash](./languages/bash.md) +- [Bash](./languages/bash.md) \* - [Biome](./languages/biome.md) - [C](./languages/c.md) \* - [C++](./languages/cpp.md) \* diff --git a/docs/src/languages/bash.md b/docs/src/languages/bash.md index c801b55054c9939f5e124aca76dc5e6b80f008d4..ce117c87c12b63a87613c9b1978ba6043c5aab1b 100644 --- a/docs/src/languages/bash.md +++ b/docs/src/languages/bash.md @@ -5,14 +5,14 @@ description: "Configure Bash language support in Zed, including language servers # Bash -Bash support is available through the [Bash extension](https://github.com/zed-extensions/bash). +Bash support is available natively in Zed. - Tree-sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash) - Language Server: [bash-lsp/bash-language-server](https://github.com/bash-lsp/bash-language-server) ## Configuration -When `shellcheck` is available `bash-language-server` will use it internally to provide diagnostics. +It is highly recommended to install `shellcheck`, as `bash-language-server` depends on it to provide diagnostics. ### Install `shellcheck`: