lsp: Remove reinstall, update config (#18318)

Conrad Irwin and Mikayla created

Release Notes:

- Fixed overriding the path of a language server binary for all language
servers. `{"lsp":{"<lsp-name>":{"binary":{"path": "_"}}}}` will now work
for all language servers including those defined by extensions.
- (breaking change) To disable finding lsp adapters in your path, you
must now specify
`{"lsp":{"<lsp-name>":{"binary":{"ignore_system_version": true}}}}`.
Previously this was `{"lsp":{"<lsp-name>":{"binary":{"path_lookup":
false}}}}`. Note that this setting still does not apply to extensions.
- Removed automatic reinstallation of language servers. (It mostly
didn't work)

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

assets/settings/default.json                        |   2 
crates/activity_indicator/src/activity_indicator.rs |   2 
crates/extension/src/extension_lsp_adapter.rs       |  15 
crates/language/src/language.rs                     |  73 -
crates/language/src/language_registry.rs            | 190 +---
crates/languages/src/c.rs                           |  54 -
crates/languages/src/css.rs                         |   7 
crates/languages/src/go.rs                          |  54 -
crates/languages/src/json.rs                        |  19 
crates/languages/src/python.rs                      |   7 
crates/languages/src/rust.rs                        | 104 -
crates/languages/src/tailwind.rs                    |  39 
crates/languages/src/typescript.rs                  |  20 
crates/languages/src/vtsls.rs                       |  50 
crates/languages/src/yaml.rs                        |  38 
crates/lsp/src/lsp.rs                               |   9 
crates/project/src/lsp_store.rs                     | 628 ++++++--------
crates/project/src/project.rs                       |   8 
crates/project/src/project_settings.rs              |   4 
crates/zed/src/main.rs                              |   2 
docs/src/languages/rust.md                          |   4 
21 files changed, 397 insertions(+), 932 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -783,7 +783,7 @@
   /// or to ensure Zed always downloads and installs an isolated version of node:
   /// {
   ///   "node": {
-  ///     "disable_path_lookup": true
+  ///     "ignore_system_version": true,
   ///   }
   /// NOTE: changing this setting currently requires restarting Zed.
   "node": {},

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -299,7 +299,7 @@ impl ActivityIndicator {
                         .into_any_element(),
                 ),
                 message: format!(
-                    "Failed to download {}. Click to show error.",
+                    "Failed to run {}. Click to show error.",
                     failed
                         .iter()
                         .map(|name| name.0.as_ref())

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -10,16 +10,11 @@ use gpui::AsyncAppContext;
 use language::{
     CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
 };
-use lsp::{CodeActionKind, LanguageServerBinary};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
 use serde::Serialize;
 use serde_json::Value;
 use std::ops::Range;
-use std::{
-    any::Any,
-    path::{Path, PathBuf},
-    pin::Pin,
-    sync::Arc,
-};
+use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
 use util::{maybe, ResultExt};
 use wasmtime_wasi::WasiView as _;
 
@@ -38,8 +33,8 @@ impl LspAdapter for ExtensionLspAdapter {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        _: Option<Arc<Path>>,
         delegate: Arc<dyn LspAdapterDelegate>,
+        _: LanguageServerBinaryOptions,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         _: &'a mut AsyncAppContext,
     ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
@@ -124,10 +119,6 @@ impl LspAdapter for ExtensionLspAdapter {
         unreachable!("get_language_server_command is overridden")
     }
 
-    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
-        None
-    }
-
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
         let code_action_kinds = self
             .extension

crates/language/src/language.rs 🔗

@@ -29,7 +29,7 @@ use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
 pub use highlight_map::HighlightMap;
 use http_client::HttpClient;
 pub use language_registry::LanguageName;
-use lsp::{CodeActionKind, LanguageServerBinary};
+use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
 use parking_lot::Mutex;
 use regex::Regex;
 use schemars::{
@@ -69,7 +69,7 @@ pub use buffer::*;
 pub use diagnostic_set::DiagnosticEntry;
 pub use language_registry::{
     AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry,
-    LanguageServerBinaryStatus, PendingLanguageServer, QUERY_FILENAME_PREFIXES,
+    LanguageServerBinaryStatus, QUERY_FILENAME_PREFIXES,
 };
 pub use lsp::LanguageServerId;
 pub use outline::*;
@@ -249,28 +249,17 @@ impl CachedLspAdapter {
 
     pub async fn get_language_server_command(
         self: Arc<Self>,
-        container_dir: Option<Arc<Path>>,
         delegate: Arc<dyn LspAdapterDelegate>,
+        binary_options: LanguageServerBinaryOptions,
         cx: &mut AsyncAppContext,
     ) -> Result<LanguageServerBinary> {
         let cached_binary = self.cached_binary.lock().await;
         self.adapter
             .clone()
-            .get_language_server_command(container_dir, delegate, cached_binary, cx)
+            .get_language_server_command(delegate, binary_options, cached_binary, cx)
             .await
     }
 
-    pub fn can_be_reinstalled(&self) -> bool {
-        self.adapter.can_be_reinstalled()
-    }
-
-    pub async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        self.adapter.installation_test_binary(container_dir).await
-    }
-
     pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
         self.adapter.code_action_kinds()
     }
@@ -322,6 +311,7 @@ pub trait LspAdapterDelegate: Send + Sync {
     fn worktree_id(&self) -> WorktreeId;
     fn worktree_root_path(&self) -> &Path;
     fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
+    async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
 
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> HashMap<String, String>;
@@ -335,8 +325,8 @@ pub trait LspAdapter: 'static + Send + Sync {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        container_dir: Option<Arc<Path>>,
         delegate: Arc<dyn LspAdapterDelegate>,
+        binary_options: LanguageServerBinaryOptions,
         mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         cx: &'a mut AsyncAppContext,
     ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
@@ -352,30 +342,30 @@ pub trait LspAdapter: 'static + Send + Sync {
             // We only want to cache when we fall back to the global one,
             // because we don't want to download and overwrite our global one
             // for each worktree we might have open.
-            if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await {
-                log::info!(
-                    "found user-installed language server for {}. path: {:?}, arguments: {:?}",
-                    self.name().0,
-                    binary.path,
-                    binary.arguments
-                );
-                return Ok(binary);
+            if binary_options.allow_path_lookup {
+                if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await {
+                    log::info!(
+                        "found user-installed language server for {}. path: {:?}, arguments: {:?}",
+                        self.name().0,
+                        binary.path,
+                        binary.arguments
+                    );
+                    return Ok(binary);
+                }
+            }
+
+            if !binary_options.allow_binary_download {
+                return Err(anyhow!("downloading language servers disabled"));
             }
 
             if let Some(cached_binary) = cached_binary.as_ref() {
                 return Ok(cached_binary.clone());
             }
 
-            let Some(container_dir) = container_dir else {
+            let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else {
                 anyhow::bail!("cannot download language servers for remotes (yet)")
             };
 
-            if !container_dir.exists() {
-                smol::fs::create_dir_all(&container_dir)
-                    .await
-                    .context("failed to create container directory")?;
-            }
-
             let mut binary = try_fetch_server_binary(self.as_ref(), &delegate, container_dir.to_path_buf(), cx).await;
 
             if let Err(error) = binary.as_ref() {
@@ -443,21 +433,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         delegate: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary>;
 
-    /// Returns `true` if a language server can be reinstalled.
-    ///
-    /// If language server initialization fails, a reinstallation will be attempted unless the value returned from this method is `false`.
-    ///
-    /// Implementations that rely on software already installed on user's system
-    /// should have [`can_be_reinstalled`](Self::can_be_reinstalled) return `false`.
-    fn can_be_reinstalled(&self) -> bool {
-        true
-    }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary>;
-
     fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     /// Post-processes completions provided by the language server.
@@ -1711,8 +1686,8 @@ impl LspAdapter for FakeLspAdapter {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        _: Option<Arc<Path>>,
         _: Arc<dyn LspAdapterDelegate>,
+        _: LanguageServerBinaryOptions,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
         _: &'a mut AsyncAppContext,
     ) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
@@ -1743,10 +1718,6 @@ impl LspAdapter for FakeLspAdapter {
         unreachable!();
     }
 
-    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
-        unreachable!();
-    }
-
     fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
     fn disk_based_diagnostic_sources(&self) -> Vec<String> {

crates/language/src/language_registry.rs 🔗

@@ -4,18 +4,17 @@ use crate::{
     },
     task_context::ContextProvider,
     with_parser, CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
-    LanguageServerName, LspAdapter, LspAdapterDelegate, PLAIN_TEXT,
+    LanguageServerName, LspAdapter, PLAIN_TEXT,
 };
 use anyhow::{anyhow, Context, Result};
 use collections::{hash_map, HashMap, HashSet};
 
 use futures::{
     channel::{mpsc, oneshot},
-    future::Shared,
     Future,
 };
 use globset::GlobSet;
-use gpui::{AppContext, BackgroundExecutor, Task};
+use gpui::{AppContext, BackgroundExecutor};
 use lsp::LanguageServerId;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
@@ -118,12 +117,6 @@ pub enum LanguageServerBinaryStatus {
     Failed { error: String },
 }
 
-pub struct PendingLanguageServer {
-    pub server_id: LanguageServerId,
-    pub task: Task<Result<(lsp::LanguageServer, Option<serde_json::Value>)>>,
-    pub container_dir: Option<Arc<Path>>,
-}
-
 #[derive(Clone)]
 pub struct AvailableLanguage {
     id: LanguageId,
@@ -882,123 +875,53 @@ impl LanguageRegistry {
         self.lsp_binary_status_tx.send(server_name, status);
     }
 
-    #[allow(clippy::too_many_arguments)]
-    pub fn create_pending_language_server(
-        self: &Arc<Self>,
-        stderr_capture: Arc<Mutex<Option<String>>>,
-        _language_name_for_tests: LanguageName,
-        adapter: Arc<CachedLspAdapter>,
-        root_path: Arc<Path>,
-        delegate: Arc<dyn LspAdapterDelegate>,
-        project_environment: Shared<Task<Option<HashMap<String, String>>>>,
-        cx: &mut AppContext,
-    ) -> Option<PendingLanguageServer> {
-        let server_id = self.state.write().next_language_server_id();
-        log::info!(
-            "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
-            adapter.name.0
-        );
+    pub fn next_language_server_id(&self) -> LanguageServerId {
+        self.state.write().next_language_server_id()
+    }
 
-        let container_dir: Option<Arc<Path>> = self
-            .language_server_download_dir
+    pub fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>> {
+        self.language_server_download_dir
             .as_ref()
-            .map(|dir| Arc::from(dir.join(adapter.name.0.as_ref())));
-        let root_path = root_path.clone();
-        let this = Arc::downgrade(self);
-
-        let task = cx.spawn({
-            let container_dir = container_dir.clone();
-            move |mut cx| async move {
-                let project_environment = project_environment.await;
-
-                let binary_result = adapter
-                    .clone()
-                    .get_language_server_command(container_dir, delegate.clone(), &mut cx)
-                    .await;
-
-                delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);
-
-                let mut binary = binary_result?;
-
-                // If we do have a project environment (either by spawning a shell in in the project directory
-                // or by getting it from the CLI) and the language server command itself
-                // doesn't have an environment (which it would have, if it was found in $PATH), then
-                // we use the project environment.
-                if binary.env.is_none() && project_environment.is_some() {
-                    log::info!(
-                        "using project environment for language server {:?}, id: {server_id}",
-                        adapter.name.0
-                    );
-                    binary.env = project_environment.clone();
-                }
-
-                let options = adapter
-                    .adapter
-                    .clone()
-                    .initialization_options(&delegate)
-                    .await?;
+            .map(|dir| Arc::from(dir.join(name.0.as_ref())))
+    }
 
-                #[cfg(any(test, feature = "test-support"))]
-                if true {
-                    if let Some(this) = this.upgrade() {
-                        if let Some(fake_entry) = this
-                            .state
-                            .write()
-                            .fake_server_entries
-                            .get_mut(&adapter.name)
-                        {
-                            let (server, mut fake_server) = lsp::FakeLanguageServer::new(
-                                server_id,
-                                binary,
-                                adapter.name.0.to_string(),
-                                fake_entry.capabilities.clone(),
-                                cx.clone(),
-                            );
-                            fake_entry._server = Some(fake_server.clone());
-
-                            if let Some(initializer) = &fake_entry.initializer {
-                                initializer(&mut fake_server);
-                            }
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn create_fake_language_server(
+        &self,
+        server_id: LanguageServerId,
+        name: &LanguageServerName,
+        binary: lsp::LanguageServerBinary,
+        cx: gpui::AsyncAppContext,
+    ) -> Option<lsp::LanguageServer> {
+        let mut state = self.state.write();
+        let fake_entry = state.fake_server_entries.get_mut(&name)?;
+        let (server, mut fake_server) = lsp::FakeLanguageServer::new(
+            server_id,
+            binary,
+            name.0.to_string(),
+            fake_entry.capabilities.clone(),
+            cx.clone(),
+        );
+        fake_entry._server = Some(fake_server.clone());
 
-                            let tx = fake_entry.tx.clone();
-                            cx.background_executor()
-                                .spawn(async move {
-                                    if fake_server
-                                        .try_receive_notification::<lsp::notification::Initialized>(
-                                        )
-                                        .await
-                                        .is_some()
-                                    {
-                                        tx.unbounded_send(fake_server.clone()).ok();
-                                    }
-                                })
-                                .detach();
+        if let Some(initializer) = &fake_entry.initializer {
+            initializer(&mut fake_server);
+        }
 
-                            return Ok((server, options));
-                        }
-                    }
+        let tx = fake_entry.tx.clone();
+        cx.background_executor()
+            .spawn(async move {
+                if fake_server
+                    .try_receive_notification::<lsp::notification::Initialized>()
+                    .await
+                    .is_some()
+                {
+                    tx.unbounded_send(fake_server.clone()).ok();
                 }
+            })
+            .detach();
 
-                drop(this);
-                Ok((
-                    lsp::LanguageServer::new(
-                        stderr_capture,
-                        server_id,
-                        binary,
-                        &root_path,
-                        adapter.code_action_kinds(),
-                        cx,
-                    )?,
-                    options,
-                ))
-            }
-        });
-
-        Some(PendingLanguageServer {
-            server_id,
-            task,
-            container_dir,
-        })
+        Some(server)
     }
 
     pub fn language_server_binary_statuses(
@@ -1007,29 +930,16 @@ impl LanguageRegistry {
         self.lsp_binary_status_tx.subscribe()
     }
 
-    pub fn delete_server_container(
-        &self,
-        adapter: Arc<CachedLspAdapter>,
-        cx: &mut AppContext,
-    ) -> Task<()> {
+    pub async fn delete_server_container(&self, name: LanguageServerName) {
         log::info!("deleting server container");
+        let Some(dir) = self.language_server_download_dir(&name) else {
+            return;
+        };
 
-        let download_dir = self
-            .language_server_download_dir
-            .clone()
-            .expect("language server download directory has not been assigned before deleting server container");
-
-        cx.spawn(|_| async move {
-            let container_dir = download_dir.join(adapter.name.0.as_ref());
-            smol::fs::remove_dir_all(container_dir)
-                .await
-                .context("server container removal")
-                .log_err();
-        })
-    }
-
-    pub fn next_language_server_id(&self) -> LanguageServerId {
-        self.state.write().next_language_server_id()
+        smol::fs::remove_dir_all(dir)
+            .await
+            .context("server container removal")
+            .log_err();
     }
 }
 

crates/languages/src/c.rs 🔗

@@ -5,7 +5,6 @@ use gpui::AsyncAppContext;
 use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
 pub use language::*;
 use lsp::LanguageServerBinary;
-use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
 use smol::fs::{self, File};
 use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
 use util::{fs::remove_matching, maybe, ResultExt};
@@ -25,41 +24,14 @@ impl super::LspAdapter for CLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
+        _: &AsyncAppContext,
     ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx.update(|cx| {
-            language_server_settings(delegate, &Self::SERVER_NAME, cx)
-                .and_then(|s| s.binary.clone())
-        });
-
-        match configured_binary {
-            Ok(Some(BinarySettings {
-                path: Some(path),
-                arguments,
-                ..
-            })) => Some(LanguageServerBinary {
-                path: path.into(),
-                arguments: arguments
-                    .unwrap_or_default()
-                    .iter()
-                    .map(|arg| arg.into())
-                    .collect(),
-                env: None,
-            }),
-            Ok(Some(BinarySettings {
-                path_lookup: Some(false),
-                ..
-            })) => None,
-            _ => {
-                let env = delegate.shell_env().await;
-                let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
-                Some(LanguageServerBinary {
-                    path,
-                    arguments: vec![],
-                    env: Some(env),
-                })
-            }
-        }
+        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
+        Some(LanguageServerBinary {
+            path,
+            arguments: vec![],
+            env: None,
+        })
     }
 
     async fn fetch_latest_server_version(
@@ -141,18 +113,6 @@ impl super::LspAdapter for CLspAdapter {
         get_cached_server_binary(container_dir).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--help".into()];
-                binary
-            })
-    }
-
     async fn label_for_completion(
         &self,
         completion: &lsp::CompletionItem,

crates/languages/src/css.rs 🔗

@@ -84,13 +84,6 @@ impl LspAdapter for CssLspAdapter {
         get_cached_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
-    }
-
     async fn initialization_options(
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,

crates/languages/src/go.rs 🔗

@@ -6,7 +6,6 @@ use gpui::{AppContext, AsyncAppContext, Task};
 use http_client::github::latest_github_release;
 pub use language::*;
 use lsp::LanguageServerBinary;
-use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
 use regex::Regex;
 use serde_json::json;
 use smol::{fs, process};
@@ -68,41 +67,14 @@ impl super::LspAdapter for GoLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
+        _: &AsyncAppContext,
     ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx.update(|cx| {
-            language_server_settings(delegate, &Self::SERVER_NAME, cx)
-                .and_then(|s| s.binary.clone())
-        });
-
-        match configured_binary {
-            Ok(Some(BinarySettings {
-                path: Some(path),
-                arguments,
-                ..
-            })) => Some(LanguageServerBinary {
-                path: path.into(),
-                arguments: arguments
-                    .unwrap_or_default()
-                    .iter()
-                    .map(|arg| arg.into())
-                    .collect(),
-                env: None,
-            }),
-            Ok(Some(BinarySettings {
-                path_lookup: Some(false),
-                ..
-            })) => None,
-            _ => {
-                let env = delegate.shell_env().await;
-                let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
-                Some(LanguageServerBinary {
-                    path,
-                    arguments: server_binary_arguments(),
-                    env: Some(env),
-                })
-            }
-        }
+        let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
+        Some(LanguageServerBinary {
+            path,
+            arguments: server_binary_arguments(),
+            env: None,
+        })
     }
 
     fn will_fetch_server(
@@ -214,18 +186,6 @@ impl super::LspAdapter for GoLspAdapter {
         get_cached_server_binary(container_dir).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--help".into()];
-                binary
-            })
-    }
-
     async fn initialization_options(
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,

crates/languages/src/json.rs 🔗

@@ -186,13 +186,6 @@ impl LspAdapter for JsonLspAdapter {
         get_cached_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
-    }
-
     async fn initialization_options(
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,
@@ -374,18 +367,6 @@ impl LspAdapter for NodeVersionAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_version_server_binary(container_dir).await
     }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_version_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--version".into()];
-                binary
-            })
-    }
 }
 
 async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {

crates/languages/src/python.rs 🔗

@@ -97,13 +97,6 @@ impl LspAdapter for PythonLspAdapter {
         get_cached_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
-    }
-
     async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
         // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
         // Where `XX` is the sorting category, `YYYY` is based on most recent usage,

crates/languages/src/rust.rs 🔗

@@ -8,7 +8,6 @@ use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
 pub use language::*;
 use language_settings::all_language_settings;
 use lsp::LanguageServerBinary;
-use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
 use regex::Regex;
 use smol::fs::{self, File};
 use std::{
@@ -37,77 +36,34 @@ impl LspAdapter for RustLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
+        _: &AsyncAppContext,
     ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx
-            .update(|cx| {
-                language_server_settings(delegate, &Self::SERVER_NAME, cx)
-                    .and_then(|s| s.binary.clone())
+        let path = delegate.which("rust-analyzer".as_ref()).await?;
+        let env = delegate.shell_env().await;
+
+        // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
+        // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
+        log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
+        let result = delegate
+            .try_exec(LanguageServerBinary {
+                path: path.clone(),
+                arguments: vec!["--help".into()],
+                env: Some(env.clone()),
             })
-            .ok()?;
-
-        let (path, env, arguments) = match configured_binary {
-            // If nothing is configured, or path_lookup explicitly enabled,
-            // we lookup the binary in the path.
-            None
-            | Some(BinarySettings {
-                path: None,
-                path_lookup: Some(true),
-                ..
-            })
-            | Some(BinarySettings {
-                path: None,
-                path_lookup: None,
-                ..
-            }) => {
-                let path = delegate.which("rust-analyzer".as_ref()).await;
-                let env = delegate.shell_env().await;
-
-                if let Some(path) = path {
-                    // It is surprisingly common for ~/.cargo/bin/rust-analyzer to be a symlink to
-                    // /usr/bin/rust-analyzer that fails when you run it; so we need to test it.
-                    log::info!("found rust-analyzer in PATH. trying to run `rust-analyzer --help`");
-                    match delegate
-                        .try_exec(LanguageServerBinary {
-                            path: path.clone(),
-                            arguments: vec!["--help".into()],
-                            env: Some(env.clone()),
-                        })
-                        .await
-                    {
-                        Ok(()) => (Some(path), Some(env), None),
-                        Err(err) => {
-                            log::error!("failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}", path, err);
-                            (None, None, None)
-                        }
-                    }
-                } else {
-                    (None, None, None)
-                }
-            }
-            // Otherwise, we use the configured binary.
-            Some(BinarySettings {
-                path: Some(path),
-                arguments,
-                path_lookup,
-            }) => {
-                if path_lookup.is_some() {
-                    log::warn!("Both `path` and `path_lookup` are set, ignoring `path_lookup`");
-                }
-                (Some(path.into()), None, arguments)
-            }
-
-            _ => (None, None, None),
-        };
+            .await;
+        if let Err(err) = result {
+            log::error!(
+                "failed to run rust-analyzer after detecting it in PATH: binary: {:?}: {}",
+                path,
+                err
+            );
+            return None;
+        }
 
-        path.map(|path| LanguageServerBinary {
+        Some(LanguageServerBinary {
             path,
-            env,
-            arguments: arguments
-                .unwrap_or_default()
-                .iter()
-                .map(|arg| arg.into())
-                .collect(),
+            env: Some(env),
+            arguments: vec![],
         })
     }
 
@@ -186,18 +142,6 @@ impl LspAdapter for RustLspAdapter {
         get_cached_server_binary(container_dir).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--help".into()];
-                binary
-            })
-    }
-
     fn disk_based_diagnostic_sources(&self) -> Vec<String> {
         vec!["rustc".into()]
     }

crates/languages/src/tailwind.rs 🔗

@@ -46,38 +46,6 @@ impl LspAdapter for TailwindLspAdapter {
         Self::SERVER_NAME.clone()
     }
 
-    async fn check_if_user_installed(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
-    ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx
-            .update(|cx| {
-                language_server_settings(delegate, &Self::SERVER_NAME, cx)
-                    .and_then(|s| s.binary.clone())
-            })
-            .ok()??;
-
-        let path = if let Some(configured_path) = configured_binary.path.map(PathBuf::from) {
-            configured_path
-        } else {
-            self.node.binary_path().await.ok()?
-        };
-
-        let arguments = configured_binary
-            .arguments
-            .unwrap_or_default()
-            .iter()
-            .map(|arg| arg.into())
-            .collect();
-
-        Some(LanguageServerBinary {
-            path,
-            arguments,
-            env: None,
-        })
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
@@ -125,13 +93,6 @@ impl LspAdapter for TailwindLspAdapter {
         get_cached_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
-    }
-
     async fn initialization_options(
         self: Arc<Self>,
         _: &Arc<dyn LspAdapterDelegate>,

crates/languages/src/typescript.rs 🔗

@@ -164,13 +164,6 @@ impl LspAdapter for TypeScriptLspAdapter {
         get_cached_ts_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_ts_server_binary(container_dir, &self.node).await
-    }
-
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
         Some(vec![
             CodeActionKind::QUICKFIX,
@@ -509,19 +502,6 @@ impl LspAdapter for EsLintLspAdapter {
             arguments: eslint_server_binary_arguments(&server_path),
         })
     }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        let server_path =
-            Self::build_destination_path(&container_dir).join(EsLintLspAdapter::SERVER_PATH);
-        Some(LanguageServerBinary {
-            path: self.node.binary_path().await.ok()?,
-            env: None,
-            arguments: eslint_server_binary_arguments(&server_path),
-        })
-    }
 }
 
 #[cfg(target_os = "windows")]

crates/languages/src/vtsls.rs 🔗

@@ -5,7 +5,7 @@ use gpui::AsyncAppContext;
 use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::{CodeActionKind, LanguageServerBinary};
 use node_runtime::NodeRuntime;
-use project::{lsp_store::language_server_settings, project_settings::BinarySettings};
+use project::lsp_store::language_server_settings;
 use serde_json::Value;
 use std::{
     any::Any,
@@ -71,40 +71,15 @@ impl LspAdapter for VtslsLspAdapter {
     async fn check_if_user_installed(
         &self,
         delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
+        _: &AsyncAppContext,
     ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx.update(|cx| {
-            language_server_settings(delegate, &SERVER_NAME, cx).and_then(|s| s.binary.clone())
-        });
-
-        match configured_binary {
-            Ok(Some(BinarySettings {
-                path: Some(path),
-                arguments,
-                ..
-            })) => Some(LanguageServerBinary {
-                path: path.into(),
-                arguments: arguments
-                    .unwrap_or_default()
-                    .iter()
-                    .map(|arg| arg.into())
-                    .collect(),
-                env: None,
-            }),
-            Ok(Some(BinarySettings {
-                path_lookup: Some(false),
-                ..
-            })) => None,
-            _ => {
-                let env = delegate.shell_env().await;
-                let path = delegate.which(SERVER_NAME.as_ref()).await?;
-                Some(LanguageServerBinary {
-                    path: path.clone(),
-                    arguments: typescript_server_binary_arguments(&path),
-                    env: Some(env),
-                })
-            }
-        }
+        let env = delegate.shell_env().await;
+        let path = delegate.which(SERVER_NAME.as_ref()).await?;
+        Some(LanguageServerBinary {
+            path: path.clone(),
+            arguments: typescript_server_binary_arguments(&path),
+            env: Some(env),
+        })
     }
 
     async fn fetch_server_binary(
@@ -157,13 +132,6 @@ impl LspAdapter for VtslsLspAdapter {
         get_cached_ts_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_ts_server_binary(container_dir, &self.node).await
-    }
-
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
         Some(vec![
             CodeActionKind::QUICKFIX,

crates/languages/src/yaml.rs 🔗

@@ -42,37 +42,6 @@ impl LspAdapter for YamlLspAdapter {
         Self::SERVER_NAME.clone()
     }
 
-    async fn check_if_user_installed(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-        cx: &AsyncAppContext,
-    ) -> Option<LanguageServerBinary> {
-        let configured_binary = cx
-            .update(|cx| {
-                language_server_settings(delegate, &Self::SERVER_NAME, cx)
-                    .and_then(|s| s.binary.clone())
-            })
-            .ok()??;
-
-        let path = if let Some(configured_path) = configured_binary.path.map(PathBuf::from) {
-            configured_path
-        } else {
-            self.node.binary_path().await.ok()?
-        };
-
-        let arguments = configured_binary
-            .arguments
-            .unwrap_or_default()
-            .iter()
-            .map(|arg| arg.into())
-            .collect();
-        Some(LanguageServerBinary {
-            path,
-            arguments,
-            env: None,
-        })
-    }
-
     async fn fetch_latest_server_version(
         &self,
         _: &dyn LspAdapterDelegate,
@@ -120,13 +89,6 @@ impl LspAdapter for YamlLspAdapter {
         get_cached_server_binary(container_dir, &self.node).await
     }
 
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir, &self.node).await
-    }
-
     async fn workspace_configuration(
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,

crates/lsp/src/lsp.rs 🔗

@@ -64,6 +64,15 @@ pub struct LanguageServerBinary {
     pub env: Option<HashMap<String, String>>,
 }
 
+/// Configures the search (and installation) of language servers.
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageServerBinaryOptions {
+    /// Whether the adapter should look at the users system
+    pub allow_path_lookup: bool,
+    /// Whether the adapter should download its own version
+    pub allow_binary_download: bool,
+}
+
 /// A running language server process.
 pub struct LanguageServer {
     server_id: LanguageServerId,

crates/project/src/lsp_store.rs 🔗

@@ -37,16 +37,16 @@ use language::{
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
     DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, LanguageConfig,
-    LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerName, LocalFile, LspAdapter,
-    LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
-    ToPointUtf16, Transaction, Unclipped,
+    LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus,
+    LanguageServerName, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
+    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use lsp::{
     CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag,
     DidChangeWatchedFilesRegistrationOptions, Edit, FileSystemWatcher, InsertTextFormat,
-    LanguageServer, LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem,
-    MessageType, OneOf, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url,
-    WorkDoneProgressCancelParams, WorkspaceFolder,
+    LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId,
+    LspRequestFuture, MessageActionItem, MessageType, OneOf, ServerHealthStatus, ServerStatus,
+    SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams, WorkspaceFolder,
 };
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
@@ -67,9 +67,8 @@ use std::{
     iter, mem,
     ops::{ControlFlow, Range},
     path::{self, Path, PathBuf},
-    process::Stdio,
     str,
-    sync::{atomic::Ordering::SeqCst, Arc},
+    sync::Arc,
     time::{Duration, Instant},
 };
 use text::{Anchor, BufferId, LineEnding};
@@ -87,8 +86,6 @@ pub use worktree::{
     FS_WATCH_LATENCY,
 };
 
-const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
-const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
 const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
 pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);
 
@@ -157,6 +154,7 @@ impl LocalLspStore {
             futures::future::join_all(shutdown_futures).await;
         }
     }
+
     async fn format_locally(
         lsp_store: WeakModel<LspStore>,
         mut buffers_with_paths: Vec<(Model<Buffer>, Option<PathBuf>)>,
@@ -1471,7 +1469,7 @@ impl LspStore {
         }
 
         for (worktree_id, adapter_name) in language_servers_to_stop {
-            self.stop_language_server(worktree_id, adapter_name, cx)
+            self.stop_local_language_server(worktree_id, adapter_name, cx)
                 .detach();
         }
 
@@ -1488,7 +1486,7 @@ impl LspStore {
 
         // Restart all language servers with changed initialization options.
         for (worktree, language) in language_servers_to_restart {
-            self.restart_language_servers(worktree, language, cx);
+            self.restart_local_language_servers(worktree, language, cx);
         }
 
         cx.notify();
@@ -3028,7 +3026,7 @@ impl LspStore {
         })
     }
 
-    pub fn primary_language_server_for_buffer<'a>(
+    fn primary_language_server_for_buffer<'a>(
         &'a self,
         buffer: &'a Buffer,
         cx: &'a AppContext,
@@ -3328,7 +3326,7 @@ impl LspStore {
         Ok(())
     }
 
-    pub fn update_worktree_diagnostics(
+    fn update_worktree_diagnostics(
         &mut self,
         worktree_id: WorktreeId,
         server_id: LanguageServerId,
@@ -5405,9 +5403,6 @@ impl LspStore {
             language_registry: self.languages.clone(),
         }) as Arc<dyn LspAdapterDelegate>;
 
-        // TODO: We should use `adapter` here instead of reaching through the `CachedLspAdapter`.
-        let lsp_adapter = adapter.adapter.clone();
-
         let Some((upstream_client, project_id)) = self.upstream_client() else {
             return;
         };
@@ -5419,17 +5414,11 @@ impl LspStore {
             return;
         };
 
-        let task = cx.spawn(|_, cx| async move {
-            let user_binary_task = lsp_adapter.check_if_user_installed(delegate.as_ref(), &cx);
-            let binary = match user_binary_task.await {
-                Some(binary) => binary,
-                None => {
-                    return Err(anyhow!(
-                        "Downloading language server for ssh host is not supported yet"
-                    ))
-                }
-            };
+        let user_binary_task =
+            self.get_language_server_binary(adapter.clone(), delegate.clone(), false, cx);
 
+        let task = cx.spawn(|_, _| async move {
+            let binary = user_binary_task.await?;
             let name = adapter.name();
             let code_action_kinds = adapter
                 .adapter
@@ -5481,6 +5470,73 @@ impl LspStore {
         .detach();
     }
 
+    fn get_language_server_binary(
+        &self,
+        adapter: Arc<CachedLspAdapter>,
+        delegate: Arc<dyn LspAdapterDelegate>,
+        allow_binary_download: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<LanguageServerBinary>> {
+        let settings = ProjectSettings::get(
+            Some(SettingsLocation {
+                worktree_id: delegate.worktree_id(),
+                path: Path::new(""),
+            }),
+            cx,
+        )
+        .lsp
+        .get(&adapter.name)
+        .and_then(|s| s.binary.clone());
+
+        if settings.as_ref().is_some_and(|b| b.path.is_some()) {
+            let settings = settings.unwrap();
+            return cx.spawn(|_, _| async move {
+                Ok(LanguageServerBinary {
+                    path: PathBuf::from(&settings.path.unwrap()),
+                    env: Some(delegate.shell_env().await),
+                    arguments: settings
+                        .arguments
+                        .unwrap_or_default()
+                        .iter()
+                        .map(Into::into)
+                        .collect(),
+                })
+            });
+        }
+        let lsp_binary_options = LanguageServerBinaryOptions {
+            allow_path_lookup: !settings
+                .as_ref()
+                .and_then(|b| b.ignore_system_version)
+                .unwrap_or_default(),
+            allow_binary_download,
+        };
+        cx.spawn(|_, mut cx| async move {
+            let binary_result = adapter
+                .clone()
+                .get_language_server_command(delegate.clone(), lsp_binary_options, &mut cx)
+                .await;
+
+            delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);
+
+            let mut binary = binary_result?;
+            if let Some(arguments) = settings.and_then(|b| b.arguments) {
+                binary.arguments = arguments.into_iter().map(Into::into).collect();
+            }
+
+            // If we do have a project environment (either by spawning a shell in in the project directory
+            // or by getting it from the CLI) and the language server command itself
+            // doesn't have an environment, then we use the project environment.
+            if binary.env.is_none() {
+                log::info!(
+                    "using project environment for language server {:?}",
+                    adapter.name()
+                );
+                binary.env = Some(delegate.shell_env().await);
+            }
+            Ok(binary)
+        })
+    }
+
     fn start_language_server(
         &mut self,
         worktree_handle: &Model<Worktree>,
@@ -5496,6 +5552,7 @@ impl LspStore {
         let worktree_id = worktree.id();
         let worktree_path = worktree.abs_path();
         let key = (worktree_id, adapter.name.clone());
+
         if self.language_server_ids.contains_key(&key) {
             return;
         }
@@ -5505,31 +5562,6 @@ impl LspStore {
             return;
         }
 
-        if adapter.reinstall_attempt_count.load(SeqCst) > MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
-            return;
-        }
-
-        let local = self.as_local().unwrap();
-
-        let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
-        let lsp_adapter_delegate = LocalLspAdapterDelegate::for_local(self, worktree_handle, cx);
-        let project_environment = local.environment.update(cx, |environment, cx| {
-            environment.get_environment(Some(worktree_id), Some(worktree_path.clone()), cx)
-        });
-
-        let pending_server = match self.languages.create_pending_language_server(
-            stderr_capture.clone(),
-            language.clone(),
-            adapter.clone(),
-            Arc::clone(&worktree_path),
-            lsp_adapter_delegate.clone(),
-            project_environment,
-            cx,
-        ) {
-            Some(pending_server) => pending_server,
-            None => return,
-        };
-
         let project_settings = ProjectSettings::get(
             Some(SettingsLocation {
                 worktree_id,
@@ -5537,76 +5569,146 @@ impl LspStore {
             }),
             cx,
         );
-
-        // We need some on the SSH client, and some on SSH host
         let lsp = project_settings.lsp.get(&adapter.name);
         let override_options = lsp.and_then(|s| s.initialization_options.clone());
 
-        let server_id = pending_server.server_id;
-        let container_dir = pending_server.container_dir.clone();
-        let state = LanguageServerState::Starting({
+        let stderr_capture = Arc::new(Mutex::new(Some(String::new())));
+        let delegate = LocalLspAdapterDelegate::for_local(self, worktree_handle, cx)
+            as Arc<dyn LspAdapterDelegate>;
+
+        let server_id = self.languages.next_language_server_id();
+        let root_path = worktree_path.clone();
+        log::info!(
+            "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}",
+            adapter.name.0
+        );
+
+        let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx);
+
+        let pending_server = cx.spawn({
             let adapter = adapter.clone();
+            let stderr_capture = stderr_capture.clone();
+
+            move |_lsp_store, cx| async move {
+                let binary = binary.await?;
+
+                #[cfg(any(test, feature = "test-support"))]
+                if let Some(server) = _lsp_store
+                    .update(&mut cx.clone(), |this, cx| {
+                        this.languages.create_fake_language_server(
+                            server_id,
+                            &adapter.name,
+                            binary.clone(),
+                            cx.to_async(),
+                        )
+                    })
+                    .ok()
+                    .flatten()
+                {
+                    return Ok(server);
+                }
+
+                lsp::LanguageServer::new(
+                    stderr_capture,
+                    server_id,
+                    binary,
+                    &root_path,
+                    adapter.code_action_kinds(),
+                    cx,
+                )
+            }
+        });
+
+        let state = LanguageServerState::Starting({
             let server_name = adapter.name.0.clone();
+            let delegate = delegate as Arc<dyn LspAdapterDelegate>;
             let language = language.clone();
             let key = key.clone();
+            let adapter = adapter.clone();
 
             cx.spawn(move |this, mut cx| async move {
-                let result = Self::setup_and_insert_language_server(
-                    this.clone(),
-                    lsp_adapter_delegate,
-                    override_options,
-                    pending_server,
-                    adapter.clone(),
-                    language.clone(),
-                    server_id,
-                    key,
-                    &mut cx,
-                )
-                .await;
+                let result = {
+                    let delegate = delegate.clone();
+                    let adapter = adapter.clone();
+                    let this = this.clone();
+                    let mut cx = cx.clone();
+                    async move {
+                        let language_server = pending_server.await?;
 
-                match result {
-                    Ok(server) => {
-                        stderr_capture.lock().take();
-                        server
-                    }
+                        let workspace_config = adapter
+                            .adapter
+                            .clone()
+                            .workspace_configuration(&delegate, &mut cx)
+                            .await?;
 
-                    Err(err) => {
-                        log::error!("failed to start language server {server_name:?}: {err}");
-                        log::error!("server stderr: {:?}", stderr_capture.lock().take());
+                        let mut initialization_options = adapter
+                            .adapter
+                            .clone()
+                            .initialization_options(&(delegate))
+                            .await?;
 
-                        let this = this.upgrade()?;
-                        let container_dir = container_dir?;
+                        Self::setup_lsp_messages(this.clone(), &language_server, delegate, adapter);
 
-                        let attempt_count = adapter.reinstall_attempt_count.fetch_add(1, SeqCst);
-                        if attempt_count >= MAX_SERVER_REINSTALL_ATTEMPT_COUNT {
-                            let max = MAX_SERVER_REINSTALL_ATTEMPT_COUNT;
-                            log::error!("Hit {max} reinstallation attempts for {server_name:?}");
-                            return None;
+                        match (&mut initialization_options, override_options) {
+                            (Some(initialization_options), Some(override_options)) => {
+                                merge_json_value_into(override_options, initialization_options);
+                            }
+                            (None, override_options) => initialization_options = override_options,
+                            _ => {}
                         }
 
-                        log::info!(
-                            "retrying installation of language server {server_name:?} in {}s",
-                            SERVER_REINSTALL_DEBOUNCE_TIMEOUT.as_secs()
-                        );
-                        cx.background_executor()
-                            .timer(SERVER_REINSTALL_DEBOUNCE_TIMEOUT)
-                            .await;
+                        let language_server = cx
+                            .update(|cx| language_server.initialize(initialization_options, cx))?
+                            .await
+                            .inspect_err(|_| {
+                                if let Some(this) = this.upgrade() {
+                                    this.update(&mut cx, |_, cx| {
+                                        cx.emit(LspStoreEvent::LanguageServerRemoved(server_id))
+                                    })
+                                    .ok();
+                                }
+                            })?;
 
-                        let installation_test_binary = adapter
-                            .installation_test_binary(container_dir.to_path_buf())
-                            .await;
+                        language_server
+                            .notify::<lsp::notification::DidChangeConfiguration>(
+                                lsp::DidChangeConfigurationParams {
+                                    settings: workspace_config,
+                                },
+                            )
+                            .ok();
 
-                        this.update(&mut cx, |_, cx| {
-                            Self::check_errored_server(
+                        anyhow::Ok(language_server)
+                    }
+                }
+                .await;
+
+                match result {
+                    Ok(server) => {
+                        this.update(&mut cx, |this, mut cx| {
+                            this.insert_newly_running_language_server(
                                 language,
                                 adapter,
+                                server.clone(),
                                 server_id,
-                                installation_test_binary,
-                                cx,
-                            )
+                                key,
+                                &mut cx,
+                            );
                         })
                         .ok();
+                        stderr_capture.lock().take();
+                        Some(server)
+                    }
 
+                    Err(err) => {
+                        let log = stderr_capture.lock().take().unwrap_or_default();
+                        delegate.update_status(
+                            adapter.name(),
+                            LanguageServerBinaryStatus::Failed {
+                                error: format!("{err}\n-- stderr--\n{}", log),
+                            },
+                        );
+                        log::error!("Failed to start language server {server_name:?}: {err}");
+                        log::error!("server stderr: {:?}", log);
                         None
                     }
                 }
@@ -5620,109 +5722,6 @@ impl LspStore {
         self.language_server_ids.insert(key, server_id);
     }
 
-    #[allow(clippy::too_many_arguments)]
-    async fn setup_and_insert_language_server(
-        this: WeakModel<Self>,
-        delegate: Arc<dyn LspAdapterDelegate>,
-        override_initialization_options: Option<serde_json::Value>,
-        pending_server: PendingLanguageServer,
-        adapter: Arc<CachedLspAdapter>,
-        language: LanguageName,
-        server_id: LanguageServerId,
-        key: (WorktreeId, LanguageServerName),
-        cx: &mut AsyncAppContext,
-    ) -> Result<Option<Arc<LanguageServer>>> {
-        let language_server = Self::setup_pending_language_server(
-            this.clone(),
-            override_initialization_options,
-            pending_server,
-            delegate,
-            adapter.clone(),
-            server_id,
-            cx,
-        )
-        .await?;
-
-        let this = match this.upgrade() {
-            Some(this) => this,
-            None => return Err(anyhow!("failed to upgrade project handle")),
-        };
-
-        this.update(cx, |this, cx| {
-            this.insert_newly_running_language_server(
-                language,
-                adapter,
-                language_server.clone(),
-                server_id,
-                key,
-                cx,
-            )
-        })??;
-
-        Ok(Some(language_server))
-    }
-
-    fn reinstall_language_server(
-        &mut self,
-        language: LanguageName,
-        adapter: Arc<CachedLspAdapter>,
-        server_id: LanguageServerId,
-        cx: &mut ModelContext<Self>,
-    ) -> Option<Task<()>> {
-        log::info!("beginning to reinstall server");
-
-        if let Some(local) = self.as_local_mut() {
-            let existing_server = match local.language_servers.remove(&server_id) {
-                Some(LanguageServerState::Running { server, .. }) => Some(server),
-                _ => None,
-            };
-
-            self.worktree_store.update(cx, |store, cx| {
-                for worktree in store.worktrees() {
-                    let key = (worktree.read(cx).id(), adapter.name.clone());
-                    self.language_server_ids.remove(&key);
-                }
-            });
-
-            Some(cx.spawn(move |this, mut cx| async move {
-                if let Some(task) = existing_server.and_then(|server| server.shutdown()) {
-                    log::info!("shutting down existing server");
-                    task.await;
-                }
-
-                // TODO: This is race-safe with regards to preventing new instances from
-                // starting while deleting, but existing instances in other projects are going
-                // to be very confused and messed up
-                let Some(task) = this
-                    .update(&mut cx, |this, cx| {
-                        this.languages.delete_server_container(adapter.clone(), cx)
-                    })
-                    .log_err()
-                else {
-                    return;
-                };
-                task.await;
-
-                this.update(&mut cx, |this, cx| {
-                    for worktree in this.worktree_store.read(cx).worktrees().collect::<Vec<_>>() {
-                        this.start_language_server(
-                            &worktree,
-                            adapter.clone(),
-                            language.clone(),
-                            cx,
-                        );
-                    }
-                })
-                .ok();
-            }))
-        } else if let Some(_ssh_store) = self.as_ssh() {
-            // TODO
-            None
-        } else {
-            None
-        }
-    }
-
     async fn shutdown_language_server(
         server_state: Option<LanguageServerState>,
         name: LanguageServerName,
@@ -5761,7 +5760,7 @@ impl LspStore {
 
     // Returns a list of all of the worktrees which no longer have a language server and the root path
     // for the stopped server
-    pub fn stop_language_server(
+    fn stop_local_language_server(
         &mut self,
         worktree_id: WorktreeId,
         adapter_name: LanguageServerName,
@@ -5877,7 +5876,6 @@ impl LspStore {
                 .spawn(request)
                 .detach_and_log_err(cx);
         } else {
-            #[allow(clippy::mutable_key_type)]
             let language_server_lookup_info: HashSet<(Model<Worktree>, LanguageName)> = buffers
                 .into_iter()
                 .filter_map(|buffer| {
@@ -5893,12 +5891,12 @@ impl LspStore {
                 .collect();
 
             for (worktree, language) in language_server_lookup_info {
-                self.restart_language_servers(worktree, language, cx);
+                self.restart_local_language_servers(worktree, language, cx);
             }
         }
     }
 
-    pub fn restart_language_servers(
+    fn restart_local_language_servers(
         &mut self,
         worktree: Model<Worktree>,
         language: LanguageName,
@@ -5912,7 +5910,8 @@ impl LspStore {
             .lsp_adapters(&language)
             .iter()
             .map(|adapter| {
-                let stop_task = self.stop_language_server(worktree_id, adapter.name.clone(), cx);
+                let stop_task =
+                    self.stop_local_language_server(worktree_id, adapter.name.clone(), cx);
                 (stop_task, adapter.name.clone())
             })
             .collect::<Vec<_>>();
@@ -5951,93 +5950,14 @@ impl LspStore {
         .detach();
     }
 
-    fn check_errored_server(
-        language: LanguageName,
-        adapter: Arc<CachedLspAdapter>,
-        server_id: LanguageServerId,
-        installation_test_binary: Option<LanguageServerBinary>,
-        cx: &mut ModelContext<Self>,
-    ) {
-        if !adapter.can_be_reinstalled() {
-            log::info!(
-                "Validation check requested for {:?} but it cannot be reinstalled",
-                adapter.name.0
-            );
-            return;
-        }
-
-        cx.spawn(move |this, mut cx| async move {
-            log::info!("About to spawn test binary");
-
-            // A lack of test binary counts as a failure
-            let process = installation_test_binary.and_then(|binary| {
-                smol::process::Command::new(&binary.path)
-                    .current_dir(&binary.path)
-                    .args(binary.arguments)
-                    .stdin(Stdio::piped())
-                    .stdout(Stdio::piped())
-                    .stderr(Stdio::inherit())
-                    .kill_on_drop(true)
-                    .spawn()
-                    .ok()
-            });
-
-            const PROCESS_TIMEOUT: Duration = Duration::from_secs(5);
-            let mut timeout = cx.background_executor().timer(PROCESS_TIMEOUT).fuse();
-
-            let mut errored = false;
-            if let Some(mut process) = process {
-                futures::select! {
-                    status = process.status().fuse() => match status {
-                        Ok(status) => errored = !status.success(),
-                        Err(_) => errored = true,
-                    },
-
-                    _ = timeout => {
-                        log::info!("test binary time-ed out, this counts as a success");
-                        _ = process.kill();
-                    }
-                }
-            } else {
-                log::warn!("test binary failed to launch");
-                errored = true;
-            }
-
-            if errored {
-                log::warn!("test binary check failed");
-                let task = this
-                    .update(&mut cx, move |this, cx| {
-                        this.reinstall_language_server(language, adapter, server_id, cx)
-                    })
-                    .ok()
-                    .flatten();
-
-                if let Some(task) = task {
-                    task.await;
-                }
-            }
-        })
-        .detach();
-    }
-
-    async fn setup_pending_language_server(
+    fn setup_lsp_messages(
         this: WeakModel<Self>,
-        override_options: Option<serde_json::Value>,
-        pending_server: PendingLanguageServer,
+        language_server: &LanguageServer,
         delegate: Arc<dyn LspAdapterDelegate>,
         adapter: Arc<CachedLspAdapter>,
-        server_id: LanguageServerId,
-        cx: &mut AsyncAppContext,
-    ) -> Result<Arc<LanguageServer>> {
-        let workspace_config = adapter
-            .adapter
-            .clone()
-            .workspace_configuration(&delegate, cx)
-            .await?;
-        // This has to come from the server
-        let (language_server, mut initialization_options) = pending_server.task.await?;
-
+    ) {
         let name = language_server.name();
+        let server_id = language_server.server_id();
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
@@ -6091,7 +6011,6 @@ impl LspStore {
             })
             .detach();
 
-        let id = language_server.server_id();
         language_server
             .on_request::<lsp::request::WorkspaceFoldersRequest, _, _>({
                 let this = this.clone();
@@ -6099,7 +6018,7 @@ impl LspStore {
                     let this = this.clone();
                     async move {
                         let Some(server) =
-                            this.update(&mut cx, |this, _| this.language_server_for_id(id))?
+                            this.update(&mut cx, |this, _| this.language_server_for_id(server_id))?
                         else {
                             return Ok(None);
                         };
@@ -6375,9 +6294,6 @@ impl LspStore {
             })
             .detach();
 
-        let disk_based_diagnostics_progress_token =
-            adapter.disk_based_diagnostics_progress_token.clone();
-
         language_server
             .on_notification::<ServerStatus, _>({
                 let this = this.clone();
@@ -6448,6 +6364,10 @@ impl LspStore {
                 }
             })
             .detach();
+
+        let disk_based_diagnostics_progress_token =
+            adapter.disk_based_diagnostics_progress_token.clone();
+
         language_server
             .on_notification::<lsp::notification::Progress, _>({
                 let this = this.clone();
@@ -6502,36 +6422,6 @@ impl LspStore {
                 }
             })
             .detach();
-
-        match (&mut initialization_options, override_options) {
-            (Some(initialization_options), Some(override_options)) => {
-                merge_json_value_into(override_options, initialization_options);
-            }
-            (None, override_options) => initialization_options = override_options,
-            _ => {}
-        }
-
-        let language_server = cx
-            .update(|cx| language_server.initialize(initialization_options, cx))?
-            .await
-            .inspect_err(|_| {
-                if let Some(this) = this.upgrade() {
-                    this.update(cx, |_, cx| {
-                        cx.emit(LspStoreEvent::LanguageServerRemoved(server_id))
-                    })
-                    .ok();
-                }
-            })?;
-
-        language_server
-            .notify::<lsp::notification::DidChangeConfiguration>(
-                lsp::DidChangeConfigurationParams {
-                    settings: workspace_config,
-                },
-            )
-            .ok();
-
-        Ok(language_server)
     }
 
     pub fn update_diagnostics(
@@ -6664,7 +6554,7 @@ impl LspStore {
         server_id: LanguageServerId,
         key: (WorktreeId, LanguageServerName),
         cx: &mut ModelContext<Self>,
-    ) -> Result<()> {
+    ) {
         // If the language server for this key doesn't match the server id, don't store the
         // server. Which will cause it to be dropped, killing the process
         if self
@@ -6673,7 +6563,7 @@ impl LspStore {
             .map(|id| id != &server_id)
             .unwrap_or(false)
         {
-            return Ok(());
+            return;
         }
 
         // Update language_servers collection with Running variant of LanguageServerState
@@ -6703,13 +6593,15 @@ impl LspStore {
         cx.emit(LspStoreEvent::LanguageServerAdded(server_id));
 
         if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() {
-            downstream_client.send(proto::StartLanguageServer {
-                project_id: *project_id,
-                server: Some(proto::LanguageServer {
-                    id: server_id.0 as u64,
-                    name: language_server.name().to_string(),
-                }),
-            })?;
+            downstream_client
+                .send(proto::StartLanguageServer {
+                    project_id: *project_id,
+                    server: Some(proto::LanguageServer {
+                        id: server_id.0 as u64,
+                        name: language_server.name().to_string(),
+                    }),
+                })
+                .log_err();
         }
 
         // Tell the language server about every open buffer in the worktree that matches the language.
@@ -6756,16 +6648,18 @@ impl LspStore {
                 let version = snapshot.version;
                 let initial_snapshot = &snapshot.snapshot;
                 let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                language_server.notify::<lsp::notification::DidOpenTextDocument>(
-                    lsp::DidOpenTextDocumentParams {
-                        text_document: lsp::TextDocumentItem::new(
-                            uri,
-                            adapter.language_id(&language.name()),
-                            version,
-                            initial_snapshot.text(),
-                        ),
-                    },
-                )?;
+                language_server
+                    .notify::<lsp::notification::DidOpenTextDocument>(
+                        lsp::DidOpenTextDocumentParams {
+                            text_document: lsp::TextDocumentItem::new(
+                                uri,
+                                adapter.language_id(&language.name()),
+                                version,
+                                initial_snapshot.text(),
+                            ),
+                        },
+                    )
+                    .log_err();
 
                 buffer_handle.update(cx, |buffer, cx| {
                     buffer.set_completion_triggers(
@@ -6779,11 +6673,9 @@ impl LspStore {
                     )
                 });
             }
-            anyhow::Ok(())
-        })?;
+        });
 
         cx.notify();
-        Ok(())
     }
 
     fn buffer_snapshot_for_lsp_version(
@@ -6878,7 +6770,7 @@ impl LspStore {
             })
     }
 
-    pub fn register_supplementary_language_server(
+    fn register_supplementary_language_server(
         &mut self,
         id: LanguageServerId,
         name: LanguageServerName,
@@ -6893,7 +6785,7 @@ impl LspStore {
         }
     }
 
-    pub fn unregister_supplementary_language_server(
+    fn unregister_supplementary_language_server(
         &mut self,
         id: LanguageServerId,
         cx: &mut ModelContext<Self>,
@@ -7807,11 +7699,8 @@ impl LspAdapter for SshLspAdapter {
     ) -> Result<LanguageServerBinary> {
         anyhow::bail!("SshLspAdapter does not support fetch_server_binary")
     }
-
-    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
-        None
-    }
 }
+
 pub fn language_server_settings<'a, 'b: 'a>(
     delegate: &'a dyn LspAdapterDelegate,
     language: &LanguageServerName,
@@ -7855,22 +7744,6 @@ impl LocalLspAdapterDelegate {
         Self::new(lsp_store, worktree, http_client, local.fs.clone(), cx)
     }
 
-    // fn for_ssh(
-    //     lsp_store: &LspStore,
-    //     worktree: &Model<Worktree>,
-    //     upstream_client: AnyProtoClient,
-    //     cx: &mut ModelContext<LspStore>,
-    // ) -> Arc<Self> {
-    //     Self::new(
-    //         lsp_store,
-    //         worktree,
-    //         Arc::new(BlockedHttpClient),
-    //         None,
-    //         Some(upstream_client),
-    //         cx,
-    //     )
-    // }
-
     pub fn new(
         lsp_store: &LspStore,
         worktree: &Model<Worktree>,
@@ -7972,6 +7845,19 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
             .update_lsp_status(server_name, status);
     }
 
+    async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>> {
+        let dir = self.language_registry.language_server_download_dir(name)?;
+
+        if !dir.exists() {
+            smol::fs::create_dir_all(&dir)
+                .await
+                .context("failed to create container directory")
+                .log_err()?;
+        }
+
+        Some(dir)
+    }
+
     async fn read_text_file(&self, path: PathBuf) -> Result<String> {
         if self.worktree.entry_for_path(&path).is_none() {
             return Err(anyhow!("no such path {path:?}"));
@@ -8056,6 +7942,10 @@ impl LspAdapterDelegate for SshLspAdapterDelegate {
         Ok(())
     }
 
+    async fn language_server_download_dir(&self, _: &LanguageServerName) -> Option<Arc<Path>> {
+        None
+    }
+
     fn update_status(
         &self,
         server_name: LanguageServerName,

crates/project/src/project.rs 🔗

@@ -3958,14 +3958,6 @@ impl Project {
         self.lsp_store.read(cx).supplementary_language_servers()
     }
 
-    pub fn language_server_adapter_for_id(
-        &self,
-        id: LanguageServerId,
-        cx: &AppContext,
-    ) -> Option<Arc<CachedLspAdapter>> {
-        self.lsp_store.read(cx).language_server_adapter_for_id(id)
-    }
-
     pub fn language_server_for_id(
         &self,
         id: LanguageServerId,

crates/project/src/project_settings.rs 🔗

@@ -55,7 +55,7 @@ pub struct NodeBinarySettings {
     pub npm_path: Option<String>,
     /// If disabled, zed will download its own copy of node.
     #[serde(default)]
-    pub disable_path_lookup: Option<bool>,
+    pub ignore_system_version: Option<bool>,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -143,7 +143,7 @@ const fn true_value() -> bool {
 pub struct BinarySettings {
     pub path: Option<String>,
     pub arguments: Option<Vec<String>>,
-    pub path_lookup: Option<bool>,
+    pub ignore_system_version: Option<bool>,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]

crates/zed/src/main.rs 🔗

@@ -481,7 +481,7 @@ fn main() {
         cx.observe_global::<SettingsStore>(move |cx| {
             let settings = &ProjectSettings::get_global(cx).node;
             let options = NodeBinaryOptions {
-                allow_path_lookup: !settings.disable_path_lookup.unwrap_or_default(),
+                allow_path_lookup: !settings.ignore_system_version.unwrap_or_default(),
                 // TODO: Expose this setting
                 allow_binary_download: true,
                 use_paths: settings.path.as_ref().map(|node_path| {

docs/src/languages/rust.md 🔗

@@ -64,14 +64,14 @@ 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.
 
-If you want to disable Zed looking for a `rust-analyzer` binary, you can set `path_lookup` to `false` in your `settings.json`:
+If you want to disable Zed looking for a `rust-analyzer` binary, you can set `ignore_system_version` to `true` in your `settings.json`:
 
 ```json
 {
   "lsp": {
     "rust-analyzer": {
       "binary": {
-        "path_lookup": false
+        "ignore_system_version": true
       }
     }
   }