Show message indicating when we're downloading language servers

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                          |  1 
crates/editor/src/items.rs          |  2 
crates/language/src/language.rs     | 30 ++++++++++--
crates/language/src/tests.rs        | 47 ++++++++++---------
crates/project/src/project.rs       | 18 +++++--
crates/server/src/rpc.rs            | 72 +++++++++++++++++-------------
crates/theme/src/theme.rs           |  1 
crates/workspace/Cargo.toml         |  1 
crates/workspace/src/lsp_status.rs  | 65 +++++++++++++++++++++++++++
crates/workspace/src/status_bar.rs  | 24 ++++++----
crates/workspace/src/workspace.rs   |  1 
crates/zed/assets/themes/_base.toml |  5 +
crates/zed/src/language.rs          | 11 ++--
crates/zed/src/main.rs              |  2 
crates/zed/src/test.rs              | 19 ++++---
crates/zed/src/zed.rs               |  8 +++
16 files changed, 214 insertions(+), 93 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5701,6 +5701,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "futures",
  "gpui",
  "language",
  "log",

crates/editor/src/items.rs 🔗

@@ -410,8 +410,6 @@ impl View for DiagnosticMessage {
                 diagnostic.message.split('\n').next().unwrap().to_string(),
                 theme.diagnostic_message.clone(),
             )
-            .contained()
-            .with_margin_left(theme.item_spacing)
             .boxed()
         } else {
             Empty::new().boxed()

crates/language/src/language.rs 🔗

@@ -12,10 +12,11 @@ use futures::{
     future::{BoxFuture, Shared},
     FutureExt,
 };
-use gpui::{AppContext, Task};
+use gpui::{executor, AppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
+use postage::watch;
 use serde::Deserialize;
 use std::{
     cell::RefCell,
@@ -127,18 +128,33 @@ pub struct Grammar {
     pub(crate) highlight_map: Mutex<HighlightMap>,
 }
 
-#[derive(Default)]
 pub struct LanguageRegistry {
     languages: Vec<Arc<Language>>,
+    pending_lsp_binaries_tx: Arc<Mutex<watch::Sender<usize>>>,
+    pending_lsp_binaries_rx: watch::Receiver<usize>,
 }
 
 impl LanguageRegistry {
     pub fn new() -> Self {
-        Self::default()
+        let (pending_lsp_binaries_tx, pending_lsp_binaries_rx) = watch::channel();
+        Self {
+            languages: Default::default(),
+            pending_lsp_binaries_tx: Arc::new(Mutex::new(pending_lsp_binaries_tx)),
+            pending_lsp_binaries_rx,
+        }
     }
 
-    pub fn add(&mut self, language: Arc<Language>) {
-        self.languages.push(language);
+    pub fn add(&mut self, language: Arc<Language>, cx: &executor::Background) {
+        self.languages.push(language.clone());
+        if let Some(lsp_binary_path) = language.lsp_binary_path() {
+            let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone();
+            cx.spawn(async move {
+                *pending_lsp_binaries_tx.lock().borrow_mut() += 1;
+                lsp_binary_path.await;
+                *pending_lsp_binaries_tx.lock().borrow_mut() -= 1;
+            })
+            .detach();
+        }
     }
 
     pub fn set_theme(&self, theme: &SyntaxTheme) {
@@ -166,6 +182,10 @@ impl LanguageRegistry {
                 .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
         })
     }
+
+    pub fn pending_lsp_binaries(&self) -> watch::Receiver<usize> {
+        self.pending_lsp_binaries_rx.clone()
+    }
 }
 
 impl Language {

crates/language/src/tests.rs 🔗

@@ -22,28 +22,31 @@ fn init_logger() {
     }
 }
 
-#[test]
-fn test_select_language() {
-    let registry = LanguageRegistry {
-        languages: vec![
-            Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".to_string(),
-                    path_suffixes: vec!["rs".to_string()],
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )),
-            Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Make".to_string(),
-                    path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )),
-        ],
-    };
+#[gpui::test]
+fn test_select_language(cx: &mut MutableAppContext) {
+    let mut registry = LanguageRegistry::new();
+    registry.add(
+        Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".to_string(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )),
+        cx.background(),
+    );
+    registry.add(
+        Arc::new(Language::new(
+            LanguageConfig {
+                name: "Make".to_string(),
+                path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )),
+        cx.background(),
+    );
 
     // matching file extension
     assert_eq!(

crates/project/src/project.rs 🔗

@@ -3069,8 +3069,10 @@ mod tests {
         .await;
 
         let project = Project::test(fs, &mut cx);
-        project.update(&mut cx, |project, _| {
-            Arc::get_mut(&mut project.languages).unwrap().add(language);
+        project.update(&mut cx, |project, cx| {
+            Arc::get_mut(&mut project.languages)
+                .unwrap()
+                .add(language, cx.background());
         });
 
         let (tree, _) = project
@@ -3215,8 +3217,10 @@ mod tests {
         .await;
 
         let project = Project::test(fs, &mut cx);
-        project.update(&mut cx, |project, _| {
-            Arc::get_mut(&mut project.languages).unwrap().add(language);
+        project.update(&mut cx, |project, cx| {
+            Arc::get_mut(&mut project.languages)
+                .unwrap()
+                .add(language, cx.background());
         });
 
         let (tree, _) = project
@@ -4108,8 +4112,10 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), &mut cx);
-        project.update(&mut cx, |project, _| {
-            Arc::get_mut(&mut project.languages).unwrap().add(language);
+        project.update(&mut cx, |project, cx| {
+            Arc::get_mut(&mut project.languages)
+                .unwrap()
+                .add(language, cx.background());
         });
 
         let (tree, _) = project

crates/server/src/rpc.rs 🔗

@@ -2001,9 +2001,8 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2011,7 +2010,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2232,9 +2233,8 @@ mod tests {
             }),
             ..Default::default()
         });
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2242,7 +2242,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2434,9 +2436,8 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2444,7 +2445,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2551,9 +2554,8 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2561,7 +2563,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2699,9 +2703,8 @@ mod tests {
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
 
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2709,7 +2712,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2800,9 +2805,8 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -2810,7 +2814,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3039,9 +3045,8 @@ mod tests {
 
         // Set up a fake language server.
         let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -3049,7 +3054,9 @@ mod tests {
                     ..Default::default()
                 },
                 Some(tree_sitter_rust::language()),
-            )));
+            )),
+            &cx_a.background(),
+        );
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3845,9 +3852,8 @@ mod tests {
             });
         });
 
-        Arc::get_mut(&mut host_lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
+        Arc::get_mut(&mut host_lang_registry).unwrap().add(
+            Arc::new(Language::new(
                 LanguageConfig {
                     name: "Rust".to_string(),
                     path_suffixes: vec!["rs".to_string()],
@@ -3855,7 +3861,9 @@ mod tests {
                     ..Default::default()
                 },
                 None,
-            )));
+            )),
+            &cx.background(),
+        );
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(

crates/theme/src/theme.rs 🔗

@@ -141,6 +141,7 @@ pub struct StatusBar {
     pub item_spacing: f32,
     pub cursor_position: TextStyle,
     pub diagnostic_message: TextStyle,
+    pub lsp_message: TextStyle,
 }
 
 #[derive(Deserialize, Default)]

crates/workspace/Cargo.toml 🔗

@@ -19,6 +19,7 @@ project = { path = "../project" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
+futures = "0.3"
 log = "0.4"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }

crates/workspace/src/lsp_status.rs 🔗

@@ -0,0 +1,65 @@
+use crate::{ItemViewHandle, Settings, StatusItemView};
+use futures::StreamExt;
+use gpui::{elements::*, Entity, RenderContext, View, ViewContext};
+use language::LanguageRegistry;
+use postage::watch;
+use std::sync::Arc;
+
+pub struct LspStatus {
+    pending_lsp_binaries: usize,
+    settings_rx: watch::Receiver<Settings>,
+}
+
+impl LspStatus {
+    pub fn new(
+        languages: Arc<LanguageRegistry>,
+        settings_rx: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let mut pending_lsp_binaries = languages.pending_lsp_binaries();
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some(pending_lsp_binaries) = pending_lsp_binaries.next().await {
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.pending_lsp_binaries = pending_lsp_binaries;
+                        cx.notify();
+                    });
+                } else {
+                    break;
+                }
+            }
+        })
+        .detach();
+        Self {
+            pending_lsp_binaries: 0,
+            settings_rx,
+        }
+    }
+}
+
+impl Entity for LspStatus {
+    type Event = ();
+}
+
+impl View for LspStatus {
+    fn ui_name() -> &'static str {
+        "LspStatus"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        if self.pending_lsp_binaries == 0 {
+            Empty::new().boxed()
+        } else {
+            let theme = &self.settings_rx.borrow().theme;
+            Label::new(
+                "Downloading language servers...".to_string(),
+                theme.workspace.status_bar.lsp_message.clone(),
+            )
+            .boxed()
+        }
+    }
+}
+
+impl StatusItemView for LspStatus {
+    fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext<Self>) {}
+}

crates/workspace/src/status_bar.rs 🔗

@@ -42,17 +42,21 @@ impl View for StatusBar {
     fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
         let theme = &self.settings.borrow().theme.workspace.status_bar;
         Flex::row()
-            .with_children(
-                self.left_items
-                    .iter()
-                    .map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
-            )
+            .with_children(self.left_items.iter().map(|i| {
+                ChildView::new(i.as_ref())
+                    .aligned()
+                    .contained()
+                    .with_margin_right(theme.item_spacing)
+                    .boxed()
+            }))
             .with_child(Empty::new().flexible(1., true).boxed())
-            .with_children(
-                self.right_items
-                    .iter()
-                    .map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
-            )
+            .with_children(self.right_items.iter().map(|i| {
+                ChildView::new(i.as_ref())
+                    .aligned()
+                    .contained()
+                    .with_margin_left(theme.item_spacing)
+                    .boxed()
+            }))
             .contained()
             .with_style(theme.container)
             .constrained()

crates/zed/assets/themes/_base.toml 🔗

@@ -77,9 +77,10 @@ border = { width = 1, color = "$border.0", left = true }
 [workspace.status_bar]
 padding = { left = 6, right = 6 }
 height = 24
-item_spacing = 24
+item_spacing = 8
 cursor_position = "$text.2"
 diagnostic_message = "$text.2"
+lsp_message = "$text.2"
 
 [workspace.toolbar]
 height = 44
@@ -188,7 +189,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"

crates/zed/src/language.rs 🔗

@@ -2,6 +2,7 @@ use anyhow::anyhow;
 use async_compression::futures::bufread::GzipDecoder;
 use client::http;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
+use gpui::executor;
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
@@ -45,7 +46,7 @@ impl RustLsp {
             .ok_or_else(|| anyhow!("no release found matching {:?}", release_name))?;
 
         let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name));
-        if fs::metadata(&destination_path).await.is_ok() {
+        if fs::metadata(&destination_path).await.is_err() {
             let response = client
                 .get(&asset.browser_download_url)
                 .send()
@@ -195,10 +196,10 @@ impl LspExt for RustLsp {
     }
 }
 
-pub fn build_language_registry() -> LanguageRegistry {
-    let mut languages = LanguageRegistry::default();
-    languages.add(Arc::new(rust()));
-    languages.add(Arc::new(markdown()));
+pub fn build_language_registry(executor: &Arc<executor::Background>) -> LanguageRegistry {
+    let mut languages = LanguageRegistry::new();
+    languages.add(Arc::new(rust()), executor);
+    languages.add(Arc::new(markdown()), executor);
     languages
 }
 

crates/zed/src/main.rs 🔗

@@ -39,7 +39,7 @@ fn main() {
             },
         );
     let (settings_tx, settings) = postage::watch::channel_with(settings);
-    let languages = Arc::new(language::build_language_registry());
+    let languages = Arc::new(language::build_language_registry(&app.background()));
     languages.set_theme(&settings.borrow().theme.editor.syntax);
 
     app.run(move |cx| {

crates/zed/src/test.rs 🔗

@@ -26,14 +26,17 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
     let client = Client::new(http.clone());
     let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
     let mut languages = LanguageRegistry::new();
-    languages.add(Arc::new(language::Language::new(
-        language::LanguageConfig {
-            name: "Rust".to_string(),
-            path_suffixes: vec!["rs".to_string()],
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    )));
+    languages.add(
+        Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Rust".to_string(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )),
+        cx.background(),
+    );
     Arc::new(AppState {
         settings_tx: Arc::new(Mutex::new(settings_tx)),
         settings,

crates/zed/src/zed.rs 🔗

@@ -97,11 +97,19 @@ pub fn build_workspace(
             cx,
         )
     });
+    let lsp_status = cx.add_view(|cx| {
+        workspace::lsp_status::LspStatus::new(
+            app_state.languages.clone(),
+            app_state.settings.clone(),
+            cx,
+        )
+    });
     let cursor_position =
         cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
     workspace.status_bar().update(cx, |status_bar, cx| {
         status_bar.add_left_item(diagnostic_summary, cx);
         status_bar.add_left_item(diagnostic_message, cx);
+        status_bar.add_left_item(lsp_status, cx);
         status_bar.add_right_item(cursor_position, cx);
     });