Show a notification when a dependency for a language server is missing (#2630)

Max Brunsfeld created

Closes
https://linear.app/zed-industries/issue/Z-2402/elixir-ls-requires-a-system-elixir-runtime-to-be-installed
Closes
https://linear.app/zed-industries/issue/Z-666/notify-users-that-they-need-go-installed-to-use-go-language-server

Release Notes:

- Added a notification when unable to download the Go language server
because Go is not installed.
- Added a notification when unable to run the Elixir language server
because Elixir is not installed.

Change summary

crates/language/src/language.rs             | 134 ++++++++++++++++------
crates/project/src/project.rs               |  38 +++++-
crates/workspace/src/workspace.rs           |   4 
crates/zed/src/languages/c.rs               |  26 ++-
crates/zed/src/languages/elixir.rs          |  68 +++++++++--
crates/zed/src/languages/go.rs              |  64 +++++++++-
crates/zed/src/languages/html.rs            |  22 ++-
crates/zed/src/languages/json.rs            |  15 +
crates/zed/src/languages/language_plugin.rs |  13 +
crates/zed/src/languages/lua.rs             |  29 +++-
crates/zed/src/languages/python.rs          |  13 +
crates/zed/src/languages/ruby.rs            |  13 +
crates/zed/src/languages/rust.rs            |  26 ++-
crates/zed/src/languages/typescript.rs      |  30 +++-
crates/zed/src/languages/yaml.rs            |  12 +
15 files changed, 367 insertions(+), 140 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -17,7 +17,7 @@ use futures::{
     future::{BoxFuture, Shared},
     FutureExt, TryFutureExt as _,
 };
-use gpui::{executor::Background, AppContext, Task};
+use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use lsp::CodeActionKind;
@@ -125,27 +125,46 @@ impl CachedLspAdapter {
 
     pub async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        self.adapter.fetch_latest_server_version(http).await
+        self.adapter.fetch_latest_server_version(delegate).await
+    }
+
+    pub fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_fetch_server(delegate, cx)
+    }
+
+    pub fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        self.adapter.will_start_server(delegate, cx)
     }
 
     pub async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         self.adapter
-            .fetch_server_binary(version, http, container_dir)
+            .fetch_server_binary(version, container_dir, delegate)
             .await
     }
 
     pub async fn cached_server_binary(
         &self,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        self.adapter.cached_server_binary(container_dir).await
+        self.adapter
+            .cached_server_binary(container_dir, delegate)
+            .await
     }
 
     pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -187,23 +206,48 @@ impl CachedLspAdapter {
     }
 }
 
+pub trait LspAdapterDelegate: Send + Sync {
+    fn show_notification(&self, message: &str, cx: &mut AppContext);
+    fn http_client(&self) -> Arc<dyn HttpClient>;
+}
+
 #[async_trait]
 pub trait LspAdapter: 'static + Send + Sync {
     async fn name(&self) -> LanguageServerName;
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>>;
 
+    fn will_fetch_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
+    fn will_start_server(
+        &self,
+        _: &Arc<dyn LspAdapterDelegate>,
+        _: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        None
+    }
+
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary>;
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary>;
 
     async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
 
@@ -513,10 +557,7 @@ pub struct LanguageRegistry {
     login_shell_env_loaded: Shared<Task<()>>,
     #[allow(clippy::type_complexity)]
     lsp_binary_paths: Mutex<
-        HashMap<
-            LanguageServerName,
-            Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
-        >,
+        HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
     >,
     executor: Option<Arc<Background>>,
 }
@@ -812,7 +853,7 @@ impl LanguageRegistry {
         language: Arc<Language>,
         adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
-        http_client: Arc<dyn HttpClient>,
+        delegate: Arc<dyn LspAdapterDelegate>,
         cx: &mut AppContext,
     ) -> Option<PendingLanguageServer> {
         let server_id = self.state.write().next_language_server_id();
@@ -860,35 +901,40 @@ impl LanguageRegistry {
             .log_err()?;
         let this = self.clone();
         let language = language.clone();
-        let http_client = http_client.clone();
         let download_dir = download_dir.clone();
         let root_path = root_path.clone();
         let adapter = adapter.clone();
         let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
 
-        let task = cx.spawn(|cx| async move {
+        let task = cx.spawn(|mut cx| async move {
             login_shell_env_loaded.await;
 
-            let mut lock = this.lsp_binary_paths.lock();
-            let entry = lock
+            let entry = this
+                .lsp_binary_paths
+                .lock()
                 .entry(adapter.name.clone())
                 .or_insert_with(|| {
-                    get_binary(
-                        adapter.clone(),
-                        language.clone(),
-                        http_client,
-                        download_dir,
-                        lsp_binary_statuses,
-                    )
-                    .map_err(Arc::new)
-                    .boxed()
+                    cx.spawn(|cx| {
+                        get_binary(
+                            adapter.clone(),
+                            language.clone(),
+                            delegate.clone(),
+                            download_dir,
+                            lsp_binary_statuses,
+                            cx,
+                        )
+                        .map_err(Arc::new)
+                    })
                     .shared()
                 })
                 .clone();
-            drop(lock);
             let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
 
+            if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
+                task.await?;
+            }
+
             let server = lsp::LanguageServer::new(
                 server_id,
                 &binary.path,
@@ -958,9 +1004,10 @@ impl Default for LanguageRegistry {
 async fn get_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
-    http_client: Arc<dyn HttpClient>,
+    delegate: Arc<dyn LspAdapterDelegate>,
     download_dir: Arc<Path>,
     statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    mut cx: AsyncAppContext,
 ) -> Result<LanguageServerBinary> {
     let container_dir = download_dir.join(adapter.name.0.as_ref());
     if !container_dir.exists() {
@@ -969,17 +1016,24 @@ async fn get_binary(
             .context("failed to create container directory")?;
     }
 
+    if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
+        task.await?;
+    }
+
     let binary = fetch_latest_binary(
         adapter.clone(),
         language.clone(),
-        http_client,
+        delegate.as_ref(),
         &container_dir,
         statuses.clone(),
     )
     .await;
 
     if let Err(error) = binary.as_ref() {
-        if let Some(cached) = adapter.cached_server_binary(container_dir).await {
+        if let Some(cached) = adapter
+            .cached_server_binary(container_dir, delegate.as_ref())
+            .await
+        {
             statuses
                 .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
                 .await?;
@@ -1001,7 +1055,7 @@ async fn get_binary(
 async fn fetch_latest_binary(
     adapter: Arc<CachedLspAdapter>,
     language: Arc<Language>,
-    http_client: Arc<dyn HttpClient>,
+    delegate: &dyn LspAdapterDelegate,
     container_dir: &Path,
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
 ) -> Result<LanguageServerBinary> {
@@ -1012,14 +1066,12 @@ async fn fetch_latest_binary(
             LanguageServerBinaryStatus::CheckingForUpdate,
         ))
         .await?;
-    let version_info = adapter
-        .fetch_latest_server_version(http_client.clone())
-        .await?;
+    let version_info = adapter.fetch_latest_server_version(delegate).await?;
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
         .await?;
     let binary = adapter
-        .fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
+        .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
         .await?;
     lsp_binary_statuses_tx
         .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
@@ -1543,7 +1595,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         unreachable!();
     }
@@ -1551,13 +1603,17 @@ impl LspAdapter for Arc<FakeLspAdapter> {
     async fn fetch_server_binary(
         &self,
         _: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         _: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         unreachable!();
     }
 
-    async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         unreachable!();
     }
 

crates/project/src/project.rs 🔗

@@ -38,9 +38,9 @@ use language::{
     },
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
     Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
-    Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch,
-    PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
-    Unclipped,
+    Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
+    Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
+    ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -75,8 +75,8 @@ use std::{
 };
 use terminals::Terminals;
 use util::{
-    debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
-    ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -252,6 +252,7 @@ pub enum Event {
     LanguageServerAdded(LanguageServerId),
     LanguageServerRemoved(LanguageServerId),
     LanguageServerLog(LanguageServerId, String),
+    Notification(String),
     ActiveEntryChanged(Option<ProjectEntryId>),
     WorktreeAdded,
     WorktreeRemoved(WorktreeId),
@@ -435,6 +436,11 @@ pub enum FormatTrigger {
     Manual,
 }
 
+struct ProjectLspAdapterDelegate {
+    project: ModelHandle<Project>,
+    http_client: Arc<dyn HttpClient>,
+}
+
 impl FormatTrigger {
     fn from_proto(value: i32) -> FormatTrigger {
         match value {
@@ -2407,7 +2413,7 @@ impl Project {
                 language.clone(),
                 adapter.clone(),
                 worktree_path.clone(),
-                self.client.http_client(),
+                ProjectLspAdapterDelegate::new(self, cx),
                 cx,
             ) {
                 Some(pending_server) => pending_server,
@@ -7188,6 +7194,26 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     }
 }
 
+impl ProjectLspAdapterDelegate {
+    fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
+        Arc::new(Self {
+            project: cx.handle(),
+            http_client: project.client.http_client(),
+        })
+    }
+}
+
+impl LspAdapterDelegate for ProjectLspAdapterDelegate {
+    fn show_notification(&self, message: &str, cx: &mut AppContext) {
+        self.project
+            .update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned())));
+    }
+
+    fn http_client(&self) -> Arc<dyn HttpClient> {
+        self.http_client.clone()
+    }
+}
+
 fn split_operations(
     mut operations: Vec<proto::Operation>,
 ) -> impl Iterator<Item = Vec<proto::Operation>> {

crates/workspace/src/workspace.rs 🔗

@@ -553,6 +553,10 @@ impl Workspace {
                     }
                 }
 
+                project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
+                    cx.add_view(|_| MessageNotification::new(message.clone()))
+                }),
+
                 _ => {}
             }
             cx.notify()

crates/zed/src/languages/c.rs 🔗

@@ -4,12 +4,11 @@ use futures::StreamExt;
 pub use language::*;
 use smol::fs::{self, File};
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
-
-use util::github::GitHubLspBinaryVersion;
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct CLspAdapter;
 
@@ -21,9 +20,9 @@ impl super::LspAdapter for CLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("clangd/clangd", false, http).await?;
+        let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?;
         let asset_name = format!("clangd-mac-{}.zip", release.name);
         let asset = release
             .assets
@@ -40,8 +39,8 @@ impl super::LspAdapter for CLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
@@ -49,7 +48,8 @@ impl super::LspAdapter for CLspAdapter {
         let binary_path = version_dir.join("bin/clangd");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .context("error downloading release")?;
@@ -81,7 +81,11 @@ impl super::LspAdapter for CLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_clangd_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/elixir.rs 🔗

@@ -1,16 +1,23 @@
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
+use gpui::{AsyncAppContext, Task};
 pub use language::*;
 use lsp::{CompletionItemKind, SymbolKind};
 use smol::fs::{self, File};
-use std::{any::Any, path::PathBuf, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
-
-use util::github::GitHubLspBinaryVersion;
+use std::{
+    any::Any,
+    path::PathBuf,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct ElixirLspAdapter;
 
@@ -20,11 +27,43 @@ impl LspAdapter for ElixirLspAdapter {
         LanguageServerName("elixir-ls".into())
     }
 
+    fn will_start_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|mut cx| async move {
+            let elixir_output = smol::process::Command::new("elixir")
+                .args(["--version"])
+                .output()
+                .await;
+            if elixir_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })
+                }
+                return Err(anyhow!("cannot run elixir-ls"));
+            }
+
+            Ok(())
+        }))
+    }
+
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
+        let release =
+            latest_github_release("elixir-lsp/elixir-ls", false, delegate.http_client()).await?;
         let asset_name = "elixir-ls.zip";
         let asset = release
             .assets
@@ -41,8 +80,8 @@ impl LspAdapter for ElixirLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
@@ -50,7 +89,8 @@ impl LspAdapter for ElixirLspAdapter {
         let binary_path = version_dir.join("language_server.sh");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .context("error downloading release")?;
@@ -88,7 +128,11 @@ impl LspAdapter for ElixirLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/go.rs 🔗

@@ -1,16 +1,23 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
+use gpui::{AsyncAppContext, Task};
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
 use smol::{fs, process};
-use std::ffi::{OsStr, OsString};
-use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc};
-use util::fs::remove_matching;
-use util::github::latest_github_release;
-use util::http::HttpClient;
-use util::ResultExt;
+use std::{
+    any::Any,
+    ffi::{OsStr, OsString},
+    ops::Range,
+    path::PathBuf,
+    str,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
+};
+use util::{fs::remove_matching, github::latest_github_release, ResultExt};
 
 fn server_binary_arguments() -> Vec<OsString> {
     vec!["-mode=stdio".into()]
@@ -31,9 +38,9 @@ impl super::LspAdapter for GoLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("golang/tools", false, http).await?;
+        let release = latest_github_release("golang/tools", false, delegate.http_client()).await?;
         let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
         if version.is_none() {
             log::warn!(
@@ -44,11 +51,39 @@ impl super::LspAdapter for GoLspAdapter {
         Ok(Box::new(version) as Box<_>)
     }
 
+    fn will_fetch_server(
+        &self,
+        delegate: &Arc<dyn LspAdapterDelegate>,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Task<Result<()>>> {
+        static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
+
+        const NOTIFICATION_MESSAGE: &str =
+            "Could not install the Go language server `gopls`, because `go` was not found.";
+
+        let delegate = delegate.clone();
+        Some(cx.spawn(|mut cx| async move {
+            let install_output = process::Command::new("go").args(["version"]).output().await;
+            if install_output.is_err() {
+                if DID_SHOW_NOTIFICATION
+                    .compare_exchange(false, true, SeqCst, SeqCst)
+                    .is_ok()
+                {
+                    cx.update(|cx| {
+                        delegate.show_notification(NOTIFICATION_MESSAGE, cx);
+                    })
+                }
+                return Err(anyhow!("cannot install gopls"));
+            }
+            Ok(())
+        }))
+    }
+
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<Option<String>>().unwrap();
         let this = *self;
@@ -68,7 +103,10 @@ impl super::LspAdapter for GoLspAdapter {
                     });
                 }
             }
-        } else if let Some(path) = this.cached_server_binary(container_dir.clone()).await {
+        } else if let Some(path) = this
+            .cached_server_binary(container_dir.clone(), delegate)
+            .await
+        {
             return Ok(path);
         }
 
@@ -105,7 +143,11 @@ impl super::LspAdapter for GoLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_binary_path = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/html.rs 🔗

@@ -1,14 +1,16 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use node_runtime::NodeRuntime;
 use serde_json::json;
 use smol::fs;
-use std::ffi::OsString;
-use std::path::Path;
-use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
+use std::{
+    any::Any,
+    ffi::OsString,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
 use util::ResultExt;
 
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -36,7 +38,7 @@ impl LspAdapter for HtmlLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node
@@ -48,8 +50,8 @@ impl LspAdapter for HtmlLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
         let server_path = container_dir.join(Self::SERVER_PATH);
@@ -69,7 +71,11 @@ impl LspAdapter for HtmlLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/json.rs 🔗

@@ -3,7 +3,9 @@ use async_trait::async_trait;
 use collections::HashMap;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
-use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{
+    LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate,
+};
 use node_runtime::NodeRuntime;
 use serde_json::json;
 use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -16,7 +18,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::{paths, ResultExt};
 
 const SERVER_PATH: &'static str =
@@ -45,7 +46,7 @@ impl LspAdapter for JsonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(
             self.node
@@ -57,8 +58,8 @@ impl LspAdapter for JsonLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
         let server_path = container_dir.join(SERVER_PATH);
@@ -78,7 +79,11 @@ impl LspAdapter for JsonLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/language_plugin.rs 🔗

@@ -3,10 +3,9 @@ use async_trait::async_trait;
 use collections::HashMap;
 use futures::lock::Mutex;
 use gpui::executor::Background;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
 use util::ResultExt;
 
 #[allow(dead_code)]
@@ -72,7 +71,7 @@ impl LspAdapter for PluginLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         let runtime = self.runtime.clone();
         let function = self.fetch_latest_server_version;
@@ -92,8 +91,8 @@ impl LspAdapter for PluginLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = *version.downcast::<String>().unwrap();
         let runtime = self.runtime.clone();
@@ -110,7 +109,11 @@ impl LspAdapter for PluginLspAdapter {
             .await
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         let runtime = self.runtime.clone();
         let function = self.cached_server_binary;
 

crates/zed/src/languages/lua.rs 🔗

@@ -3,12 +3,14 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{io::BufReader, StreamExt};
-use language::{LanguageServerBinary, LanguageServerName};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapterDelegate};
 use smol::fs;
-use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
-use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt};
-
-use util::github::GitHubLspBinaryVersion;
+use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
+use util::{
+    async_iife,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 #[derive(Copy, Clone)]
 pub struct LuaLspAdapter;
@@ -28,9 +30,11 @@ impl super::LspAdapter for LuaLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("LuaLS/lua-language-server", false, http).await?;
+        let release =
+            latest_github_release("LuaLS/lua-language-server", false, delegate.http_client())
+                .await?;
         let version = release.name.clone();
         let platform = match consts::ARCH {
             "x86_64" => "x64",
@@ -53,15 +57,16 @@ impl super::LspAdapter for LuaLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
 
         let binary_path = container_dir.join("bin/lua-language-server");
 
         if fs::metadata(&binary_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -81,7 +86,11 @@ impl super::LspAdapter for LuaLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         async_iife!({
             let mut last_binary_path = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/python.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use node_runtime::NodeRuntime;
 use smol::fs;
 use std::{
@@ -10,7 +10,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::ResultExt;
 
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -37,7 +36,7 @@ impl LspAdapter for PythonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
     }
@@ -45,8 +44,8 @@ impl LspAdapter for PythonLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
         let server_path = container_dir.join(Self::SERVER_PATH);
@@ -63,7 +62,11 @@ impl LspAdapter for PythonLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/ruby.rs 🔗

@@ -1,8 +1,7 @@
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use std::{any::Any, path::PathBuf, sync::Arc};
-use util::http::HttpClient;
 
 pub struct RubyLanguageServer;
 
@@ -14,7 +13,7 @@ impl LspAdapter for RubyLanguageServer {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(()))
     }
@@ -22,13 +21,17 @@ impl LspAdapter for RubyLanguageServer {
     async fn fetch_server_binary(
         &self,
         _version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         Err(anyhow!("solargraph must be installed manually"))
     }
 
-    async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         Some(LanguageServerBinary {
             path: "solargraph".into(),
             arguments: vec!["stdio".into()],

crates/zed/src/languages/rust.rs 🔗

@@ -7,10 +7,11 @@ use lazy_static::lazy_static;
 use regex::Regex;
 use smol::fs::{self, File};
 use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
-use util::fs::remove_matching;
-use util::github::{latest_github_release, GitHubLspBinaryVersion};
-use util::http::HttpClient;
-use util::ResultExt;
+use util::{
+    fs::remove_matching,
+    github::{latest_github_release, GitHubLspBinaryVersion},
+    ResultExt,
+};
 
 pub struct RustLspAdapter;
 
@@ -22,9 +23,11 @@ impl LspAdapter for RustLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?;
+        let release =
+            latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client())
+                .await?;
         let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
         let asset = release
             .assets
@@ -40,14 +43,15 @@ impl LspAdapter for RustLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 
         if fs::metadata(&destination_path).await.is_err() {
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -69,7 +73,11 @@ impl LspAdapter for RustLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last = None;
             let mut entries = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/typescript.rs 🔗

@@ -4,7 +4,7 @@ use async_tar::Archive;
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt};
 use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
 use lsp::CodeActionKind;
 use node_runtime::NodeRuntime;
 use serde_json::{json, Value};
@@ -16,7 +16,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::{fs::remove_matching, github::latest_github_release, http::HttpClient};
+use util::{fs::remove_matching, github::latest_github_release};
 use util::{github::GitHubLspBinaryVersion, ResultExt};
 
 fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -58,7 +58,7 @@ impl LspAdapter for TypeScriptLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         Ok(Box::new(TypeScriptVersions {
             typescript_version: self.node.npm_package_latest_version("typescript").await?,
@@ -72,8 +72,8 @@ impl LspAdapter for TypeScriptLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<TypeScriptVersions>().unwrap();
         let server_path = container_dir.join(Self::NEW_SERVER_PATH);
@@ -99,7 +99,11 @@ impl LspAdapter for TypeScriptLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
             let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
@@ -204,12 +208,13 @@ impl LspAdapter for EsLintLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        http: Arc<dyn HttpClient>,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Send + Any>> {
         // At the time of writing the latest vscode-eslint release was released in 2020 and requires
         // special custom LSP protocol extensions be handled to fully initialize. Download the latest
         // prerelease instead to sidestep this issue
-        let release = latest_github_release("microsoft/vscode-eslint", true, http).await?;
+        let release =
+            latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?;
         Ok(Box::new(GitHubLspBinaryVersion {
             name: release.name,
             url: release.tarball_url,
@@ -219,8 +224,8 @@ impl LspAdapter for EsLintLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
         let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
@@ -229,7 +234,8 @@ impl LspAdapter for EsLintLspAdapter {
         if fs::metadata(&server_path).await.is_err() {
             remove_matching(&container_dir, |entry| entry != destination_path).await;
 
-            let mut response = http
+            let mut response = delegate
+                .http_client()
                 .get(&version.url, Default::default(), true)
                 .await
                 .map_err(|err| anyhow!("error downloading release: {}", err))?;
@@ -257,7 +263,11 @@ impl LspAdapter for EsLintLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             // This is unfortunate but we don't know what the version is to build a path directly
             let mut dir = fs::read_dir(&container_dir).await?;

crates/zed/src/languages/yaml.rs 🔗

@@ -4,6 +4,7 @@ use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{
     language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
+    LspAdapterDelegate,
 };
 use node_runtime::NodeRuntime;
 use serde_json::Value;
@@ -15,7 +16,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use util::http::HttpClient;
 use util::ResultExt;
 
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
@@ -42,7 +42,7 @@ impl LspAdapter for YamlLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<Box<dyn 'static + Any + Send>> {
         Ok(Box::new(
             self.node
@@ -54,8 +54,8 @@ impl LspAdapter for YamlLspAdapter {
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
         container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<String>().unwrap();
         let server_path = container_dir.join(Self::SERVER_PATH);
@@ -72,7 +72,11 @@ impl LspAdapter for YamlLspAdapter {
         })
     }
 
-    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
         (|| async move {
             let mut last_version_dir = None;
             let mut entries = fs::read_dir(&container_dir).await?;