Make language server initialization asynchronous

Antonio Scandurra created

Change summary

crates/language/src/language.rs |  26 +-
crates/project/src/project.rs   | 246 +++++++++++++++++++---------------
script/bundle                   |   4 
script/download-rust-analyzer   |  19 --
4 files changed, 150 insertions(+), 145 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -8,7 +8,7 @@ mod tests;
 
 use anyhow::{anyhow, Result};
 use collections::HashSet;
-use gpui::AppContext;
+use gpui::{AppContext, Task};
 use highlight_map::HighlightMap;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -229,9 +229,9 @@ impl Language {
 
     pub fn start_server(
         &self,
-        root_path: &Path,
+        root_path: Arc<Path>,
         cx: &AppContext,
-    ) -> Result<Option<Arc<lsp::LanguageServer>>> {
+    ) -> Task<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 {
@@ -255,19 +255,19 @@ impl Language {
                     })
                     .detach();
 
-                return Ok(Some(server.clone()));
+                return Task::ready(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)
+            let background = cx.background().clone();
+            let server = lsp::LanguageServer::new(
+                Path::new(&config.binary),
+                &root_path,
+                cx.background().clone(),
+            )
+            .map(Some);
+            cx.background().spawn(async move { server })
         } else {
-            Ok(None)
+            Task::ready(Ok(None))
         }
     }
 

crates/project/src/project.rs 🔗

@@ -39,6 +39,8 @@ pub struct Project {
     active_entry: Option<ProjectEntry>,
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
+    loading_language_servers:
+        HashMap<(WorktreeId, String), watch::Receiver<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(),
+                loading_language_servers: Default::default(),
             }
         })
     }
@@ -309,6 +312,7 @@ impl Project {
                 },
                 language_servers_with_diagnostics_running: 0,
                 language_servers: Default::default(),
+                loading_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,144 @@ 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>> {
+    ) -> 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();
+        let key = (worktree_id, language.name().to_string());
+        if let Some(language_server) = self.language_servers.get(&key) {
+            return Task::ready(Some(language_server.clone()));
+        } else if let Some(mut language_server) = self.loading_language_servers.get(&key).cloned() {
+            return cx
+                .foreground()
+                .spawn(async move { language_server.recv().await.flatten() });
+        }
+
+        let (mut language_server_tx, language_server_rx) = watch::channel();
+        self.loading_language_servers
+            .insert(key.clone(), language_server_rx);
+        let language_server = language.start_server(worktree_path, cx);
+        let rpc = self.client.clone();
+        cx.spawn_weak(|this, mut cx| async move {
+            let language_server = language_server.await.log_err().flatten();
+            if let Some(this) = this.upgrade(&cx) {
+                this.update(&mut cx, |this, _| {
+                    this.loading_language_servers.remove(&key);
+                    if let Some(language_server) = language_server.clone() {
+                        this.language_servers.insert(key, language_server);
                     }
-                    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 language_server = language_server?;
+            *language_server_tx.borrow_mut() = Some(language_server.clone());
+
+            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();
+                        }
                     }
-                }
-            })
-            .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),
-                };
+            // 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 {
-                                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
+                    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();
+                                lsp::WorkDoneProgress::End(_) => {
+                                    running_jobs_for_this_server -= 1;
+                                    if running_jobs_for_this_server == 0 {
+                                        block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish))
+                                            .ok();
+                                    }
                                 }
-                            }
-                            _ => {}
-                        },
+                                _ => {}
+                            },
+                        }
                     }
-                }
-            })
-            .detach();
+                })
+                .detach();
 
-        // 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();
-                            }
-                        });
-                    }
-                    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 })
+            // 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)
+        })
     }
 
     pub fn update_diagnostics(

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-*