Merge pull request #459 from zed-industries/spurious-macro-errors

Nathan Sobo created

Download language servers dynamically on startup

Change summary

.github/workflows/ci.yml              |  10 -
Cargo.lock                            |  19 +
crates/client/src/client.rs           |   4 
crates/editor/src/items.rs            |   2 
crates/language/Cargo.toml            |   4 
crates/language/src/language.rs       | 254 ++++++++++++++++++++++------
crates/language/src/tests.rs          |  39 ++--
crates/lsp/src/lsp.rs                 |  73 --------
crates/project/src/project.rs         | 259 ++++++++++++++++------------
crates/server/src/rpc.rs              |  72 ++++---
crates/theme/src/theme.rs             |   1 
crates/workspace/Cargo.toml           |   1 
crates/workspace/src/lsp_status.rs    | 137 +++++++++++++++
crates/workspace/src/status_bar.rs    |  24 +-
crates/workspace/src/workspace.rs     |   1 
crates/zed/Cargo.toml                 |   1 
crates/zed/assets/themes/_base.toml   |   5 
crates/zed/languages/rust/config.toml |   1 
crates/zed/src/language.rs            | 144 +++++++++++++++
crates/zed/src/test.rs                |  19 +
crates/zed/src/zed.rs                 |  10 +
script/bundle                         |   4 
script/download-rust-analyzer         |  19 --
23 files changed, 742 insertions(+), 361 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -34,11 +34,6 @@ jobs:
         with:
           clean: false
 
-      - name: Download rust-analyzer
-        run: |
-          script/download-rust-analyzer
-          echo "$PWD/vendor/bin" >> $GITHUB_PATH
-
       - name: Run tests
         run: cargo test --workspace --no-fail-fast
 
@@ -69,14 +64,11 @@ jobs:
         uses: actions/checkout@v2
         with:
           clean: false
-          
+
       - name: Validate version
         if: ${{ startsWith(github.ref, 'refs/tags/v') }}
         run: script/validate-version
 
-      - name: Download rust-analyzer
-        run: script/download-rust-analyzer
-
       - name: Create app bundle
         run: script/bundle
 

Cargo.lock 🔗

@@ -178,6 +178,17 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "async-broadcast"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
+dependencies = [
+ "easy-parallel",
+ "event-listener",
+ "futures-core",
+]
+
 [[package]]
 name = "async-channel"
 version = "1.6.1"
@@ -1926,9 +1937,9 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.12"
+version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
 
 [[package]]
 name = "futures-executor"
@@ -2619,7 +2630,9 @@ name = "language"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-broadcast",
  "async-trait",
+ "client",
  "clock",
  "collections",
  "ctor",
@@ -5701,6 +5714,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "futures",
  "gpui",
  "language",
  "log",
@@ -5741,6 +5755,7 @@ name = "zed"
 version = "0.15.2"
 dependencies = [
  "anyhow",
+ "async-compression",
  "async-recursion",
  "async-trait",
  "chat_panel",

crates/client/src/client.rs 🔗

@@ -224,6 +224,10 @@ impl Client {
         self.id
     }
 
+    pub fn http_client(&self) -> Arc<dyn HttpClient> {
+        self.http.clone()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn override_authenticate<F>(&mut self, authenticate: F) -> &mut Self
     where

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/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/language.rs"
 [features]
 test-support = [
     "rand",
+    "client/test-support",
     "collections/test-support",
     "lsp/test-support",
     "text/test-support",
@@ -17,6 +18,7 @@ test-support = [
 ]
 
 [dependencies]
+client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 fuzzy = { path = "../fuzzy" }
@@ -28,6 +30,7 @@ text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
 anyhow = "1.0.38"
+async-broadcast = "0.3.4"
 async-trait = "0.1"
 futures = "0.3"
 lazy_static = "1.4"
@@ -44,6 +47,7 @@ tree-sitter = "0.20"
 tree-sitter-rust = { version = "0.20.0", optional = true }
 
 [dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }

crates/language/src/language.rs 🔗

@@ -7,15 +7,27 @@ pub mod proto;
 mod tests;
 
 use anyhow::{anyhow, Result};
+use client::http::{self, HttpClient};
 use collections::HashSet;
-use gpui::AppContext;
+use futures::{
+    future::{BoxFuture, Shared},
+    FutureExt, TryFutureExt,
+};
+use gpui::{AppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Deserialize;
-use std::{cell::RefCell, ops::Range, path::Path, str, sync::Arc};
+use std::{
+    cell::RefCell,
+    ops::Range,
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+};
 use theme::SyntaxTheme;
 use tree_sitter::{self, Query};
+use util::ResultExt;
 
 #[cfg(any(test, feature = "test-support"))]
 use futures::channel::mpsc;
@@ -47,7 +59,23 @@ pub trait ToLspPosition {
     fn to_lsp_position(self) -> lsp::Position;
 }
 
-pub trait LspPostProcessor: 'static + Send + Sync {
+pub struct LspBinaryVersion {
+    pub name: String,
+    pub url: http::Url,
+}
+
+pub trait LspExt: 'static + Send + Sync {
+    fn fetch_latest_server_version(
+        &self,
+        http: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>>;
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        http: Arc<dyn HttpClient>,
+        download_dir: Arc<Path>,
+    ) -> BoxFuture<'static, Result<PathBuf>>;
+    fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>>;
     fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams);
     fn label_for_completion(
         &self,
@@ -77,7 +105,6 @@ pub struct LanguageConfig {
 
 #[derive(Default, Deserialize)]
 pub struct LanguageServerConfig {
-    pub binary: String,
     pub disk_based_diagnostic_sources: HashSet<String>,
     pub disk_based_diagnostics_progress_token: Option<String>,
     #[cfg(any(test, feature = "test-support"))]
@@ -103,7 +130,8 @@ pub struct BracketPair {
 pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
-    pub(crate) lsp_post_processor: Option<Box<dyn LspPostProcessor>>,
+    pub(crate) lsp_ext: Option<Arc<dyn LspExt>>,
+    lsp_binary_path: Mutex<Option<Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>>>,
 }
 
 pub struct Grammar {
@@ -115,18 +143,35 @@ pub struct Grammar {
     pub(crate) highlight_map: Mutex<HighlightMap>,
 }
 
-#[derive(Default)]
+#[derive(Clone)]
+pub enum LanguageServerBinaryStatus {
+    CheckingForUpdate,
+    Downloading,
+    Downloaded,
+    Cached,
+    Failed,
+}
+
 pub struct LanguageRegistry {
     languages: Vec<Arc<Language>>,
+    language_server_download_dir: Option<Arc<Path>>,
+    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
 }
 
 impl LanguageRegistry {
     pub fn new() -> Self {
-        Self::default()
+        let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
+        Self {
+            language_server_download_dir: None,
+            languages: Default::default(),
+            lsp_binary_statuses_tx,
+            lsp_binary_statuses_rx,
+        }
     }
 
     pub fn add(&mut self, language: Arc<Language>) {
-        self.languages.push(language);
+        self.languages.push(language.clone());
     }
 
     pub fn set_theme(&self, theme: &SyntaxTheme) {
@@ -135,6 +180,10 @@ impl LanguageRegistry {
         }
     }
 
+    pub fn set_language_server_download_dir(&mut self, path: impl Into<Arc<Path>>) {
+        self.language_server_download_dir = Some(path.into());
+    }
+
     pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
         self.languages
             .iter()
@@ -154,6 +203,140 @@ impl LanguageRegistry {
                 .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
         })
     }
+
+    pub fn start_language_server(
+        &self,
+        language: &Arc<Language>,
+        root_path: Arc<Path>,
+        http_client: Arc<dyn HttpClient>,
+        cx: &AppContext,
+    ) -> Option<Task<Result<Arc<lsp::LanguageServer>>>> {
+        #[cfg(any(test, feature = "test-support"))]
+        if let Some(config) = &language.config.language_server {
+            if let Some(fake_config) = &config.fake_config {
+                use postage::prelude::Stream;
+
+                let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
+                    fake_config.capabilities.clone(),
+                    cx.background().clone(),
+                );
+
+                if let Some(initalizer) = &fake_config.initializer {
+                    initalizer(&mut fake_server);
+                }
+
+                let servers_tx = fake_config.servers_tx.clone();
+                let mut initialized = server.capabilities();
+                cx.background()
+                    .spawn(async move {
+                        while initialized.recv().await.is_none() {}
+                        servers_tx.unbounded_send(fake_server).ok();
+                    })
+                    .detach();
+
+                return Some(Task::ready(Ok(server.clone())));
+            }
+        }
+
+        let download_dir = self
+            .language_server_download_dir
+            .clone()
+            .ok_or_else(|| anyhow!("language server download directory has not been assigned"))
+            .log_err()?;
+
+        let lsp_ext = language.lsp_ext.clone()?;
+        let background = cx.background().clone();
+        let server_binary_path = {
+            Some(
+                language
+                    .lsp_binary_path
+                    .lock()
+                    .get_or_insert_with(|| {
+                        get_server_binary_path(
+                            lsp_ext,
+                            language.clone(),
+                            http_client,
+                            download_dir,
+                            self.lsp_binary_statuses_tx.clone(),
+                        )
+                        .map_err(Arc::new)
+                        .boxed()
+                        .shared()
+                    })
+                    .clone()
+                    .map_err(|e| anyhow!(e)),
+            )
+        }?;
+        Some(cx.background().spawn(async move {
+            let server_binary_path = server_binary_path.await?;
+            let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?;
+            Ok(server)
+        }))
+    }
+
+    pub fn language_server_binary_statuses(
+        &self,
+    ) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        self.lsp_binary_statuses_rx.clone()
+    }
+}
+
+async fn get_server_binary_path(
+    lsp_ext: Arc<dyn LspExt>,
+    language: Arc<Language>,
+    http_client: Arc<dyn HttpClient>,
+    download_dir: Arc<Path>,
+    statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+) -> Result<PathBuf> {
+    let path = fetch_latest_server_binary_path(
+        lsp_ext.clone(),
+        language.clone(),
+        http_client,
+        download_dir.clone(),
+        statuses.clone(),
+    )
+    .await;
+    if path.is_err() {
+        if let Some(cached_path) = lsp_ext.cached_server_binary(download_dir).await {
+            statuses
+                .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
+                .await?;
+            return Ok(cached_path);
+        } else {
+            statuses
+                .broadcast((language.clone(), LanguageServerBinaryStatus::Failed))
+                .await?;
+        }
+    }
+    path
+}
+
+async fn fetch_latest_server_binary_path(
+    lsp_ext: Arc<dyn LspExt>,
+    language: Arc<Language>,
+    http_client: Arc<dyn HttpClient>,
+    download_dir: Arc<Path>,
+    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+) -> Result<PathBuf> {
+    lsp_binary_statuses_tx
+        .broadcast((
+            language.clone(),
+            LanguageServerBinaryStatus::CheckingForUpdate,
+        ))
+        .await?;
+    let version_info = lsp_ext
+        .fetch_latest_server_version(http_client.clone())
+        .await?;
+    lsp_binary_statuses_tx
+        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
+        .await?;
+    let path = lsp_ext
+        .fetch_server_binary(version_info, http_client, download_dir)
+        .await?;
+    lsp_binary_statuses_tx
+        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
+        .await?;
+    Ok(path)
 }
 
 impl Language {
@@ -170,7 +353,8 @@ impl Language {
                     highlight_map: Default::default(),
                 })
             }),
-            lsp_post_processor: None,
+            lsp_ext: None,
+            lsp_binary_path: Default::default(),
         }
     }
 
@@ -214,8 +398,8 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_lsp_post_processor(mut self, processor: impl LspPostProcessor) -> Self {
-        self.lsp_post_processor = Some(Box::new(processor));
+    pub fn with_lsp_ext(mut self, lsp_ext: impl LspExt) -> Self {
+        self.lsp_ext = Some(Arc::new(lsp_ext));
         self
     }
 
@@ -227,50 +411,6 @@ impl Language {
         self.config.line_comment.as_deref()
     }
 
-    pub fn start_server(
-        &self,
-        root_path: &Path,
-        cx: &AppContext,
-    ) -> Result<Option<Arc<lsp::LanguageServer>>> {
-        if let Some(config) = &self.config.language_server {
-            #[cfg(any(test, feature = "test-support"))]
-            if let Some(fake_config) = &config.fake_config {
-                use postage::prelude::Stream;
-
-                let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
-                    fake_config.capabilities.clone(),
-                    cx.background().clone(),
-                );
-
-                if let Some(initalizer) = &fake_config.initializer {
-                    initalizer(&mut fake_server);
-                }
-
-                let servers_tx = fake_config.servers_tx.clone();
-                let mut initialized = server.capabilities();
-                cx.background()
-                    .spawn(async move {
-                        while initialized.recv().await.is_none() {}
-                        servers_tx.unbounded_send(fake_server).ok();
-                    })
-                    .detach();
-
-                return Ok(Some(server.clone()));
-            }
-
-            const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
-            let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
-                cx.platform()
-                    .path_for_resource(Some(&config.binary), None)?
-            } else {
-                Path::new(&config.binary).to_path_buf()
-            };
-            lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some)
-        } else {
-            Ok(None)
-        }
-    }
-
     pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
         self.config
             .language_server
@@ -286,7 +426,7 @@ impl Language {
     }
 
     pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
-        if let Some(processor) = self.lsp_post_processor.as_ref() {
+        if let Some(processor) = self.lsp_ext.as_ref() {
             processor.process_diagnostics(diagnostics);
         }
     }
@@ -295,7 +435,7 @@ impl Language {
         &self,
         completion: &lsp::CompletionItem,
     ) -> Option<CompletionLabel> {
-        self.lsp_post_processor
+        self.lsp_ext
             .as_ref()?
             .label_for_completion(completion, self)
     }

crates/language/src/tests.rs 🔗

@@ -22,28 +22,25 @@ fn init_logger() {
     }
 }
 
-#[test]
+#[gpui::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()),
-            )),
-        ],
-    };
+    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()),
+    )));
+    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()),
+    )));
 
     // matching file extension
     assert_eq!(

crates/lsp/src/lsp.rs 🔗

@@ -700,8 +700,6 @@ impl FakeLanguageServer {
 mod tests {
     use super::*;
     use gpui::TestAppContext;
-    use unindent::Unindent;
-    use util::test::temp_tree;
 
     #[ctor::ctor]
     fn init_logger() {
@@ -710,64 +708,6 @@ mod tests {
         }
     }
 
-    #[gpui::test]
-    async fn test_rust_analyzer(cx: TestAppContext) {
-        let lib_source = r#"
-            fn fun() {
-                let hello = "world";
-            }
-        "#
-        .unindent();
-        let root_dir = temp_tree(json!({
-            "Cargo.toml": r#"
-                [package]
-                name = "temp"
-                version = "0.1.0"
-                edition = "2018"
-            "#.unindent(),
-            "src": {
-                "lib.rs": &lib_source
-            }
-        }));
-        let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
-
-        let server =
-            LanguageServer::new(Path::new("rust-analyzer"), root_dir.path(), cx.background())
-                .unwrap();
-        server.next_idle_notification().await;
-
-        server
-            .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
-                text_document: TextDocumentItem::new(
-                    lib_file_uri.clone(),
-                    "rust".to_string(),
-                    0,
-                    lib_source,
-                ),
-            })
-            .await
-            .unwrap();
-
-        let hover = server
-            .request::<request::HoverRequest>(HoverParams {
-                text_document_position_params: TextDocumentPositionParams {
-                    text_document: TextDocumentIdentifier::new(lib_file_uri),
-                    position: Position::new(1, 21),
-                },
-                work_done_progress_params: Default::default(),
-            })
-            .await
-            .unwrap()
-            .unwrap();
-        assert_eq!(
-            hover.contents,
-            HoverContents::Markup(MarkupContent {
-                kind: MarkupKind::PlainText,
-                value: "&str".to_string()
-            })
-        );
-    }
-
     #[gpui::test]
     async fn test_fake(cx: TestAppContext) {
         let (server, mut fake) = LanguageServer::fake(cx.background());
@@ -828,19 +768,6 @@ mod tests {
         fake.receive_notification::<notification::Exit>().await;
     }
 
-    impl LanguageServer {
-        async fn next_idle_notification(self: &Arc<Self>) {
-            let (tx, rx) = channel::unbounded();
-            let _subscription =
-                self.on_notification::<ServerStatusNotification, _>(move |params| {
-                    if params.quiescent {
-                        tx.try_send(()).unwrap();
-                    }
-                });
-            let _ = rx.recv().await;
-        }
-    }
-
     pub enum ServerStatusNotification {}
 
     impl notification::Notification for ServerStatusNotification {

crates/project/src/project.rs 🔗

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
-use futures::Future;
+use futures::{future::Shared, Future, FutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
@@ -39,6 +39,8 @@ pub struct Project {
     active_entry: Option<ProjectEntry>,
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
+    started_language_servers:
+        HashMap<(WorktreeId, String), Shared<Task<Option<Arc<LanguageServer>>>>>,
     client: Arc<client::Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
@@ -258,6 +260,7 @@ impl Project {
                 fs,
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
+                started_language_servers: Default::default(),
             }
         })
     }
@@ -309,6 +312,7 @@ impl Project {
                 },
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
+                started_language_servers: Default::default(),
             };
             for worktree in worktrees {
                 this.add_worktree(&worktree, cx);
@@ -776,7 +780,7 @@ impl Project {
         };
 
         // If the buffer has a language, set it and start/assign the language server
-        if let Some(language) = self.languages.select_language(&full_path) {
+        if let Some(language) = self.languages.select_language(&full_path).cloned() {
             buffer.update(cx, |buffer, cx| {
                 buffer.set_language(Some(language.clone()), cx);
             });
@@ -786,24 +790,20 @@ impl Project {
             if let Some(local_worktree) = worktree.and_then(|w| w.read(cx).as_local()) {
                 let worktree_id = local_worktree.id();
                 let worktree_abs_path = local_worktree.abs_path().clone();
-
-                let language_server = match self
-                    .language_servers
-                    .entry((worktree_id, language.name().to_string()))
-                {
-                    hash_map::Entry::Occupied(e) => Some(e.get().clone()),
-                    hash_map::Entry::Vacant(e) => Self::start_language_server(
-                        self.client.clone(),
-                        language.clone(),
-                        &worktree_abs_path,
-                        cx,
-                    )
-                    .map(|server| e.insert(server).clone()),
-                };
-
-                buffer.update(cx, |buffer, cx| {
-                    buffer.set_language_server(language_server, cx);
-                });
+                let buffer = buffer.downgrade();
+                let language_server =
+                    self.start_language_server(worktree_id, worktree_abs_path, language, cx);
+
+                cx.spawn_weak(|_, mut cx| async move {
+                    if let Some(language_server) = language_server.await {
+                        if let Some(buffer) = buffer.upgrade(&cx) {
+                            buffer.update(&mut cx, |buffer, cx| {
+                                buffer.set_language_server(Some(language_server), cx);
+                            });
+                        }
+                    }
+                })
+                .detach();
             }
         }
 
@@ -819,116 +819,151 @@ impl Project {
     }
 
     fn start_language_server(
-        rpc: Arc<Client>,
+        &mut self,
+        worktree_id: WorktreeId,
+        worktree_path: Arc<Path>,
         language: Arc<Language>,
-        worktree_path: &Path,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Arc<LanguageServer>> {
+    ) -> Shared<Task<Option<Arc<LanguageServer>>>> {
         enum LspEvent {
             DiagnosticsStart,
             DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
             DiagnosticsFinish,
         }
 
-        let language_server = language
-            .start_server(worktree_path, cx)
-            .log_err()
-            .flatten()?;
-        let disk_based_sources = language
-            .disk_based_diagnostic_sources()
-            .cloned()
-            .unwrap_or_default();
-        let disk_based_diagnostics_progress_token =
-            language.disk_based_diagnostics_progress_token().cloned();
-        let has_disk_based_diagnostic_progress_token =
-            disk_based_diagnostics_progress_token.is_some();
-        let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
-
-        // Listen for `PublishDiagnostics` notifications.
-        language_server
-            .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                let diagnostics_tx = diagnostics_tx.clone();
-                move |params| {
-                    if !has_disk_based_diagnostic_progress_token {
-                        block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
-                    }
-                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
-                    if !has_disk_based_diagnostic_progress_token {
-                        block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
+        let key = (worktree_id, language.name().to_string());
+        self.started_language_servers
+            .entry(key.clone())
+            .or_insert_with(|| {
+                let language_server = self.languages.start_language_server(
+                    &language,
+                    worktree_path,
+                    self.client.http_client(),
+                    cx,
+                );
+                let rpc = self.client.clone();
+                cx.spawn_weak(|this, mut cx| async move {
+                    let language_server = language_server?.await.log_err()?;
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, _| {
+                            this.language_servers.insert(key, language_server.clone());
+                        });
                     }
-                }
-            })
-            .detach();
-
-        // Listen for `Progress` notifications. Send an event when the language server
-        // transitions between running jobs and not running any jobs.
-        let mut running_jobs_for_this_server: i32 = 0;
-        language_server
-            .on_notification::<lsp::notification::Progress, _>(move |params| {
-                let token = match params.token {
-                    lsp::NumberOrString::Number(_) => None,
-                    lsp::NumberOrString::String(token) => Some(token),
-                };
 
-                if token == disk_based_diagnostics_progress_token {
-                    match params.value {
-                        lsp::ProgressParamsValue::WorkDone(progress) => match progress {
-                            lsp::WorkDoneProgress::Begin(_) => {
-                                running_jobs_for_this_server += 1;
-                                if running_jobs_for_this_server == 1 {
+                    let disk_based_sources = language
+                        .disk_based_diagnostic_sources()
+                        .cloned()
+                        .unwrap_or_default();
+                    let disk_based_diagnostics_progress_token =
+                        language.disk_based_diagnostics_progress_token().cloned();
+                    let has_disk_based_diagnostic_progress_token =
+                        disk_based_diagnostics_progress_token.is_some();
+                    let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
+
+                    // Listen for `PublishDiagnostics` notifications.
+                    language_server
+                        .on_notification::<lsp::notification::PublishDiagnostics, _>({
+                            let diagnostics_tx = diagnostics_tx.clone();
+                            move |params| {
+                                if !has_disk_based_diagnostic_progress_token {
                                     block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
                                 }
-                            }
-                            lsp::WorkDoneProgress::End(_) => {
-                                running_jobs_for_this_server -= 1;
-                                if running_jobs_for_this_server == 0 {
+                                block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params)))
+                                    .ok();
+                                if !has_disk_based_diagnostic_progress_token {
                                     block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
                                 }
                             }
-                            _ => {}
-                        },
-                    }
-                }
-            })
-            .detach();
+                        })
+                        .detach();
+
+                    // Listen for `Progress` notifications. Send an event when the language server
+                    // transitions between running jobs and not running any jobs.
+                    let mut running_jobs_for_this_server: i32 = 0;
+                    language_server
+                        .on_notification::<lsp::notification::Progress, _>(move |params| {
+                            let token = match params.token {
+                                lsp::NumberOrString::Number(_) => None,
+                                lsp::NumberOrString::String(token) => Some(token),
+                            };
 
-        // Process all the LSP events.
-        cx.spawn_weak(|this, mut cx| async move {
-            while let Ok(message) = diagnostics_rx.recv().await {
-                let this = this.upgrade(&cx)?;
-                match message {
-                    LspEvent::DiagnosticsStart => {
-                        this.update(&mut cx, |this, cx| {
-                            this.disk_based_diagnostics_started(cx);
-                            if let Some(project_id) = this.remote_id() {
-                                rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
-                                    .log_err();
+                            if token == disk_based_diagnostics_progress_token {
+                                match params.value {
+                                    lsp::ProgressParamsValue::WorkDone(progress) => {
+                                        match progress {
+                                            lsp::WorkDoneProgress::Begin(_) => {
+                                                running_jobs_for_this_server += 1;
+                                                if running_jobs_for_this_server == 1 {
+                                                    block_on(
+                                                        diagnostics_tx
+                                                            .send(LspEvent::DiagnosticsStart),
+                                                    )
+                                                    .ok();
+                                                }
+                                            }
+                                            lsp::WorkDoneProgress::End(_) => {
+                                                running_jobs_for_this_server -= 1;
+                                                if running_jobs_for_this_server == 0 {
+                                                    block_on(
+                                                        diagnostics_tx
+                                                            .send(LspEvent::DiagnosticsFinish),
+                                                    )
+                                                    .ok();
+                                                }
+                                            }
+                                            _ => {}
+                                        }
+                                    }
+                                }
                             }
-                        });
-                    }
-                    LspEvent::DiagnosticsUpdate(mut params) => {
-                        language.process_diagnostics(&mut params);
-                        this.update(&mut cx, |this, cx| {
-                            this.update_diagnostics(params, &disk_based_sources, cx)
-                                .log_err();
-                        });
-                    }
-                    LspEvent::DiagnosticsFinish => {
-                        this.update(&mut cx, |this, cx| {
-                            this.disk_based_diagnostics_finished(cx);
-                            if let Some(project_id) = this.remote_id() {
-                                rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id })
-                                    .log_err();
+                        })
+                        .detach();
+
+                    // Process all the LSP events.
+                    cx.spawn(|mut cx| async move {
+                        while let Ok(message) = diagnostics_rx.recv().await {
+                            let this = this.upgrade(&cx)?;
+                            match message {
+                                LspEvent::DiagnosticsStart => {
+                                    this.update(&mut cx, |this, cx| {
+                                        this.disk_based_diagnostics_started(cx);
+                                        if let Some(project_id) = this.remote_id() {
+                                            rpc.send(proto::DiskBasedDiagnosticsUpdating {
+                                                project_id,
+                                            })
+                                            .log_err();
+                                        }
+                                    });
+                                }
+                                LspEvent::DiagnosticsUpdate(mut params) => {
+                                    language.process_diagnostics(&mut params);
+                                    this.update(&mut cx, |this, cx| {
+                                        this.update_diagnostics(params, &disk_based_sources, cx)
+                                            .log_err();
+                                    });
+                                }
+                                LspEvent::DiagnosticsFinish => {
+                                    this.update(&mut cx, |this, cx| {
+                                        this.disk_based_diagnostics_finished(cx);
+                                        if let Some(project_id) = this.remote_id() {
+                                            rpc.send(proto::DiskBasedDiagnosticsUpdated {
+                                                project_id,
+                                            })
+                                            .log_err();
+                                        }
+                                    });
+                                }
                             }
-                        });
-                    }
-                }
-            }
-            Some(())
-        })
-        .detach();
+                        }
+                        Some(())
+                    })
+                    .detach();
 
-        Some(language_server)
+                    Some(language_server)
+                })
+                .shared()
+            })
+            .clone()
     }
 
     pub fn update_diagnostics(
@@ -2857,8 +2892,6 @@ impl Entity for Project {
         &mut self,
         _: &mut MutableAppContext,
     ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
-        use futures::FutureExt;
-
         let shutdown_futures = self
             .language_servers
             .drain()

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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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()),
-            )));
+            )),
+            
+        );
 
         // 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,
-            )));
+            )),
+            
+        );
 
         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,137 @@
+use crate::{ItemViewHandle, Settings, StatusItemView};
+use futures::StreamExt;
+use gpui::{
+    action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View,
+    ViewContext,
+};
+use language::{LanguageRegistry, LanguageServerBinaryStatus};
+use postage::watch;
+use std::sync::Arc;
+
+action!(DismissErrorMessage);
+
+pub struct LspStatus {
+    settings_rx: watch::Receiver<Settings>,
+    checking_for_update: Vec<String>,
+    downloading: Vec<String>,
+    failed: Vec<String>,
+}
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(LspStatus::dismiss_error_message);
+}
+
+impl LspStatus {
+    pub fn new(
+        languages: Arc<LanguageRegistry>,
+        settings_rx: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let mut status_events = languages.language_server_binary_statuses();
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Some((language, event)) = status_events.next().await {
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        for vector in [
+                            &mut this.checking_for_update,
+                            &mut this.downloading,
+                            &mut this.failed,
+                        ] {
+                            vector.retain(|name| name != language.name());
+                        }
+
+                        match event {
+                            LanguageServerBinaryStatus::CheckingForUpdate => {
+                                this.checking_for_update.push(language.name().to_string());
+                            }
+                            LanguageServerBinaryStatus::Downloading => {
+                                this.downloading.push(language.name().to_string());
+                            }
+                            LanguageServerBinaryStatus::Failed => {
+                                this.failed.push(language.name().to_string());
+                            }
+                            LanguageServerBinaryStatus::Downloaded
+                            | LanguageServerBinaryStatus::Cached => {}
+                        }
+
+                        cx.notify();
+                    });
+                } else {
+                    break;
+                }
+            }
+        })
+        .detach();
+        Self {
+            settings_rx,
+            checking_for_update: Default::default(),
+            downloading: Default::default(),
+            failed: Default::default(),
+        }
+    }
+
+    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
+        self.failed.clear();
+        cx.notify();
+    }
+}
+
+impl Entity for LspStatus {
+    type Event = ();
+}
+
+impl View for LspStatus {
+    fn ui_name() -> &'static str {
+        "LspStatus"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &self.settings_rx.borrow().theme;
+        if !self.downloading.is_empty() {
+            Label::new(
+                format!(
+                    "Downloading {} language server{}...",
+                    self.downloading.join(", "),
+                    if self.downloading.len() > 1 { "s" } else { "" }
+                ),
+                theme.workspace.status_bar.lsp_message.clone(),
+            )
+            .boxed()
+        } else if !self.checking_for_update.is_empty() {
+            Label::new(
+                format!(
+                    "Checking for updates to {} language server{}...",
+                    self.checking_for_update.join(", "),
+                    if self.checking_for_update.len() > 1 {
+                        "s"
+                    } else {
+                        ""
+                    }
+                ),
+                theme.workspace.status_bar.lsp_message.clone(),
+            )
+            .boxed()
+        } else if !self.failed.is_empty() {
+            MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
+                Label::new(
+                    format!(
+                        "Failed to download {} language server{}. Click to dismiss.",
+                        self.failed.join(", "),
+                        if self.failed.len() > 1 { "s" } else { "" }
+                    ),
+                    theme.workspace.status_bar.lsp_message.clone(),
+                )
+                .boxed()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(|cx| cx.dispatch_action(DismissErrorMessage))
+            .boxed()
+        } else {
+            Empty::new().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/Cargo.toml 🔗

@@ -55,6 +55,7 @@ theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
 anyhow = "1.0.38"
+async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-recursion = "0.3"
 async-trait = "0.1"
 crossbeam-channel = "0.5.0"

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/languages/rust/config.toml 🔗

@@ -11,6 +11,5 @@ brackets = [
 ]
 
 [language_server]
-binary = "rust-analyzer"
 disk_based_diagnostic_sources = ["rustc"]
 disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"

crates/zed/src/language.rs 🔗

@@ -1,17 +1,140 @@
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
+use client::http::{self, HttpClient, Method};
+use futures::{future::BoxFuture, FutureExt, StreamExt};
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
 use rust_embed::RustEmbed;
-use std::borrow::Cow;
-use std::{str, sync::Arc};
+use serde::Deserialize;
+use smol::fs::{self, File};
+use std::{
+    borrow::Cow,
+    env::consts,
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+};
+use util::{ResultExt, TryFutureExt};
 
 #[derive(RustEmbed)]
 #[folder = "languages"]
 struct LanguageDir;
 
-struct RustPostProcessor;
+struct RustLsp;
+
+#[derive(Deserialize)]
+struct GithubRelease {
+    name: String,
+    assets: Vec<GithubReleaseAsset>,
+}
+
+#[derive(Deserialize)]
+struct GithubReleaseAsset {
+    name: String,
+    browser_download_url: http::Url,
+}
+
+impl LspExt for RustLsp {
+    fn fetch_latest_server_version(
+        &self,
+        http: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+        async move {
+            let release = http
+            .send(
+                surf::RequestBuilder::new(
+                    Method::Get,
+                    http::Url::parse(
+                        "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest",
+                    )
+                    .unwrap(),
+                )
+                .middleware(surf::middleware::Redirect::default())
+                .build(),
+            )
+            .await
+            .map_err(|err| anyhow!("error fetching latest release: {}", err))?
+            .body_json::<GithubRelease>()
+            .await
+            .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
+            let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
+            let asset = release
+                .assets
+                .iter()
+                .find(|asset| asset.name == asset_name)
+                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
+            Ok(LspBinaryVersion {
+                name: release.name,
+                url: asset.browser_download_url.clone(),
+            })
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: LspBinaryVersion,
+        http: Arc<dyn HttpClient>,
+        download_dir: Arc<Path>,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        async move {
+            let destination_dir_path = download_dir.join("rust-analyzer");
+            fs::create_dir_all(&destination_dir_path).await?;
+            let destination_path =
+                destination_dir_path.join(format!("rust-analyzer-{}", version.name));
+
+            if fs::metadata(&destination_path).await.is_err() {
+                let response = http
+                    .send(
+                        surf::RequestBuilder::new(Method::Get, version.url)
+                            .middleware(surf::middleware::Redirect::default())
+                            .build(),
+                    )
+                    .await
+                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
+                let decompressed_bytes = GzipDecoder::new(response);
+                let mut file = File::create(&destination_path).await?;
+                futures::io::copy(decompressed_bytes, &mut file).await?;
+                fs::set_permissions(
+                    &destination_path,
+                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+                )
+                .await?;
+
+                if let Some(mut entries) = fs::read_dir(&destination_dir_path).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != destination_path {
+                                fs::remove_file(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(destination_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, download_dir: Arc<Path>) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let destination_dir_path = download_dir.join("rust-analyzer");
+            fs::create_dir_all(&destination_dir_path).await?;
+
+            let mut last = None;
+            let mut entries = fs::read_dir(&destination_dir_path).await?;
+            while let Some(entry) = entries.next().await {
+                last = Some(entry?.path());
+            }
+            last.ok_or_else(|| anyhow!("no cached binary"))
+        }
+        .log_err()
+        .boxed()
+    }
 
-impl LspPostProcessor for RustPostProcessor {
     fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
         lazy_static! {
             static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
@@ -113,7 +236,12 @@ impl LspPostProcessor for RustPostProcessor {
 }
 
 pub fn build_language_registry() -> LanguageRegistry {
-    let mut languages = LanguageRegistry::default();
+    let mut languages = LanguageRegistry::new();
+    languages.set_language_server_download_dir(
+        dirs::home_dir()
+            .expect("failed to determine home directory")
+            .join(".zed"),
+    );
     languages.add(Arc::new(rust()));
     languages.add(Arc::new(markdown()));
     languages
@@ -131,7 +259,7 @@ fn rust() -> Language {
         .unwrap()
         .with_outline_query(load_query("rust/outline.scm").as_ref())
         .unwrap()
-        .with_lsp_post_processor(RustPostProcessor)
+        .with_lsp_ext(RustLsp)
 }
 
 fn markdown() -> Language {
@@ -153,7 +281,7 @@ fn load_query(path: &str) -> Cow<'static, str> {
 mod tests {
     use super::*;
     use gpui::color::Color;
-    use language::LspPostProcessor;
+    use language::LspExt;
     use theme::SyntaxTheme;
 
     #[test]
@@ -180,7 +308,7 @@ mod tests {
                 },
             ],
         };
-        RustPostProcessor.process_diagnostics(&mut params);
+        RustLsp.process_diagnostics(&mut params);
 
         assert_eq!(params.diagnostics[0].message, "use of moved value `a`");
 

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()),
+        )),
+        
+    );
     Arc::new(AppState {
         settings_tx: Arc::new(Mutex::new(settings_tx)),
         settings,

crates/zed/src/zed.rs 🔗

@@ -43,6 +43,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         }
     });
 
+    workspace::lsp_status::init(cx);
+
     cx.add_bindings(vec![
         Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
         Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
@@ -97,11 +99,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);
     });
 

script/bundle 🔗

@@ -21,9 +21,6 @@ cargo build --release --target aarch64-apple-darwin
 # Replace the bundle's binary with a "fat binary" that combines the two architecture-specific binaries
 lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/release/Zed -output target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/MacOS/zed
 
-# Bundle rust-analyzer
-cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/
-
 # Sign the app bundle with an ad-hoc signature so it runs on the M1. We need a real certificate but this works for now.
 if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
     echo "Signing bundle with Apple-issued certificate"
@@ -34,7 +31,6 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
     security import /tmp/zed-certificate.p12 -k zed.keychain -P $MACOS_CERTIFICATE_PASSWORD -T /usr/bin/codesign
     rm /tmp/zed-certificate.p12
     security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_CERTIFICATE_PASSWORD zed.keychain
-    /usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer -v
     /usr/bin/codesign --force --deep --timestamp --options runtime --sign "Zed Industries, Inc." target/x86_64-apple-darwin/release/bundle/osx/Zed.app -v
     security default-keychain -s login.keychain
 else

script/download-rust-analyzer 🔗

@@ -1,19 +0,0 @@
-#!/bin/bash
-
-set -e
-
-export RUST_ANALYZER_URL="https://github.com/rust-analyzer/rust-analyzer/releases/download/2022-01-24/"
-
-function download {
-    local filename="rust-analyzer-$1"
-    curl -L $RUST_ANALYZER_URL/$filename.gz | gunzip > vendor/bin/$filename
-    chmod +x vendor/bin/$filename
-}
-
-mkdir -p vendor/bin
-download "x86_64-apple-darwin"
-download "aarch64-apple-darwin"
-
-cd vendor/bin
-lipo -create rust-analyzer-* -output rust-analyzer
-rm rust-analyzer-*