languages: Allow installing pre-release of rust-analyzer and clangd (#37530)

Lukas Wirth created

Release Notes:

- Added lsp binary config to allow fetching nightly rust-analyzer and
clangd releases

Change summary

crates/editor/src/editor_tests.rs                      |  4 +
crates/language/src/language.rs                        |  4 +
crates/language_extension/src/extension_lsp_adapter.rs |  1 
crates/languages/src/c.rs                              | 17 ++++++-
crates/languages/src/css.rs                            |  1 
crates/languages/src/go.rs                             |  1 
crates/languages/src/json.rs                           |  2 
crates/languages/src/python.rs                         |  3 +
crates/languages/src/rust.rs                           |  7 ++
crates/languages/src/tailwind.rs                       |  1 
crates/languages/src/typescript.rs                     |  2 
crates/languages/src/vtsls.rs                          |  1 
crates/languages/src/yaml.rs                           |  1 
crates/project/src/lsp_store.rs                        |  1 
crates/project/src/project_settings.rs                 |  8 +++
docs/src/languages/cpp.md                              | 28 +++++++++--
docs/src/languages/rust.md                             | 16 ++++++
17 files changed, 87 insertions(+), 11 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -16783,6 +16783,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
                     "some other init value": false
                 })),
                 enable_lsp_tasks: false,
+                fetch: None,
             },
         );
     });
@@ -16803,6 +16804,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
                     "anotherInitValue": false
                 })),
                 enable_lsp_tasks: false,
+                fetch: None,
             },
         );
     });
@@ -16823,6 +16825,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
                     "anotherInitValue": false
                 })),
                 enable_lsp_tasks: false,
+                fetch: None,
             },
         );
     });
@@ -16841,6 +16844,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
                 settings: None,
                 initialization_options: None,
                 enable_lsp_tasks: false,
+                fetch: None,
             },
         );
     });

crates/language/src/language.rs 🔗

@@ -395,6 +395,7 @@ pub trait LspAdapter: 'static + Send + Sync {
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
+        cx: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>>;
 
     fn will_fetch_server(
@@ -605,7 +606,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
     delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate);
 
     let latest_version = adapter
-        .fetch_latest_server_version(delegate.as_ref())
+        .fetch_latest_server_version(delegate.as_ref(), cx)
         .await?;
 
     if let Some(binary) = adapter
@@ -2222,6 +2223,7 @@ impl LspAdapter for FakeLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         unreachable!();
     }

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -204,6 +204,7 @@ impl LspAdapter for ExtensionLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         unreachable!("get_language_server_command is overridden")
     }

crates/languages/src/c.rs 🔗

@@ -5,8 +5,9 @@ use gpui::{App, AsyncApp};
 use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
 pub use language::*;
 use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
-use project::lsp_store::clangd_ext;
+use project::{lsp_store::clangd_ext, project_settings::ProjectSettings};
 use serde_json::json;
+use settings::Settings as _;
 use smol::fs;
 use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
 use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
@@ -42,9 +43,19 @@ impl super::LspAdapter for CLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
+        cx: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release =
-            latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
+        let release = latest_github_release(
+            "clangd/clangd",
+            true,
+            ProjectSettings::try_read_global(cx, |s| {
+                s.lsp.get(&Self::SERVER_NAME)?.fetch.as_ref()?.pre_release
+            })
+            .flatten()
+            .unwrap_or(false),
+            delegate.http_client(),
+        )
+        .await?;
         let os_suffix = match consts::OS {
             "macos" => "mac",
             "linux" => "linux",

crates/languages/src/css.rs 🔗

@@ -61,6 +61,7 @@ impl LspAdapter for CssLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node

crates/languages/src/go.rs 🔗

@@ -59,6 +59,7 @@ impl super::LspAdapter for GoLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let release =
             latest_github_release("golang/tools", false, false, delegate.http_client()).await?;

crates/languages/src/json.rs 🔗

@@ -321,6 +321,7 @@ impl LspAdapter for JsonLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(
             self.node
@@ -494,6 +495,7 @@ impl LspAdapter for NodeVersionAdapter {
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let release = latest_github_release(
             "zed-industries/package-version-server",

crates/languages/src/python.rs 🔗

@@ -157,6 +157,7 @@ impl LspAdapter for PythonLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node
@@ -1111,6 +1112,7 @@ impl LspAdapter for PyLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(()) as Box<_>)
     }
@@ -1422,6 +1424,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(()) as Box<_>)
     }

crates/languages/src/rust.rs 🔗

@@ -147,11 +147,16 @@ impl LspAdapter for RustLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         delegate: &dyn LspAdapterDelegate,
+        cx: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let release = latest_github_release(
             "rust-lang/rust-analyzer",
             true,
-            false,
+            ProjectSettings::try_read_global(cx, |s| {
+                s.lsp.get(&SERVER_NAME)?.fetch.as_ref()?.pre_release
+            })
+            .flatten()
+            .unwrap_or(false),
             delegate.http_client(),
         )
         .await?;

crates/languages/src/tailwind.rs 🔗

@@ -66,6 +66,7 @@ impl LspAdapter for TailwindLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node

crates/languages/src/typescript.rs 🔗

@@ -563,6 +563,7 @@ impl LspAdapter for TypeScriptLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,
@@ -885,6 +886,7 @@ impl LspAdapter for EsLintLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _delegate: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let url = build_asset_url(
             "zed-industries/vscode-eslint",

crates/languages/src/vtsls.rs 🔗

@@ -73,6 +73,7 @@ impl LspAdapter for VtslsLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,

crates/languages/src/yaml.rs 🔗

@@ -44,6 +44,7 @@ impl LspAdapter for YamlLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node

crates/project/src/lsp_store.rs 🔗

@@ -13070,6 +13070,7 @@ impl LspAdapter for SshLspAdapter {
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
+        _: &AsyncApp,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version")
     }

crates/project/src/project_settings.rs 🔗

@@ -516,6 +516,12 @@ pub struct BinarySettings {
     pub ignore_system_version: Option<bool>,
 }
 
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+pub struct FetchSettings {
+    // Whether to consider pre-releases for fetching
+    pub pre_release: Option<bool>,
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 #[serde(rename_all = "snake_case")]
 pub struct LspSettings {
@@ -527,6 +533,7 @@ pub struct LspSettings {
     /// Default: true
     #[serde(default = "default_true")]
     pub enable_lsp_tasks: bool,
+    pub fetch: Option<FetchSettings>,
 }
 
 impl Default for LspSettings {
@@ -536,6 +543,7 @@ impl Default for LspSettings {
             initialization_options: None,
             settings: None,
             enable_lsp_tasks: true,
+            fetch: None,
         }
     }
 }

docs/src/languages/cpp.md 🔗

@@ -9,22 +9,23 @@ C++ support is available natively in Zed.
 
 You can configure which `clangd` binary Zed should use.
 
-To use a binary in a custom location, add the following to your `settings.json`:
+By default, Zed will try to find a `clangd` in your `$PATH` and try to use that. If that binary successfully executes, it's used. Otherwise, Zed will fall back to installing its own `clangd` version and use that.
+
+If you want to install a pre-release `clangd` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`:
 
 ```json
 {
   "lsp": {
     "clangd": {
-      "binary": {
-        "path": "/path/to/clangd",
-        "arguments": []
+      "fetch": {
+        "pre_release": true
       }
     }
   }
 }
 ```
 
-If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true`:
+If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true` in your `settings.json`:
 
 ```json
 {
@@ -38,6 +39,23 @@ If you want to disable Zed looking for a `clangd` binary, you can set `ignore_sy
 }
 ```
 
+If you want to use a binary in a custom location, you can specify a `path` and optional `arguments`:
+
+```json
+{
+  "lsp": {
+    "cangd": {
+      "binary": {
+        "path": "/path/to/clangd",
+        "arguments": []
+      }
+    }
+  }
+}
+```
+
+This `"path"` has to be an absolute path.
+
 ## Arguments
 
 You can pass any number of arguments to clangd. To see a full set of available options, run `clangd --help` from the command line. For example with `--function-arg-placeholders=0` completions contain only parentheses for function calls, while the default (`--function-arg-placeholders=1`) completions also contain placeholders for method parameters.

docs/src/languages/rust.md 🔗

@@ -63,7 +63,21 @@ A `true` setting will set the target directory to `target/rust-analyzer`. You ca
 
 You can configure which `rust-analyzer` binary Zed should use.
 
-By default, Zed will try to find a `rust-analyzer` in your `$PATH` and try to use that. If that binary successfully executes `rust-analyzer --help`, it's used. Otherwise, Zed will fall back to installing its own `rust-analyzer` version and using that.
+By default, Zed will try to find a `rust-analyzer` in your `$PATH` and try to use that. If that binary successfully executes `rust-analyzer --help`, it's used. Otherwise, Zed will fall back to installing its own stable `rust-analyzer` version and use that.
+
+If you want to install pre-release `rust-analyzer` version instead you can instruct Zed to do so by setting `pre_release` to `true` in your `settings.json`:
+
+```json
+{
+  "lsp": {
+    "rust-analyzer": {
+      "fetch": {
+        "pre_release": true
+      }
+    }
+  }
+}
+```
 
 If you want to disable Zed looking for a `rust-analyzer` binary, you can set `ignore_system_version` to `true` in your `settings.json`: