From 1ca50d0134dc861714d1851a9d94afa8d237dab4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 09:39:28 +0100 Subject: [PATCH] Make language server initialization asynchronous --- crates/language/src/language.rs | 26 ++-- crates/project/src/project.rs | 252 ++++++++++++++++++-------------- script/bundle | 4 - script/download-rust-analyzer | 19 --- 4 files changed, 153 insertions(+), 148 deletions(-) delete mode 100755 script/download-rust-analyzer diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 73de5af12c..b5a88cf843 100644 --- a/crates/language/src/language.rs +++ b/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, cx: &AppContext, - ) -> Result>> { + ) -> Task>>> { 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)) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9a231b707b..24d632ad2c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,6 +39,8 @@ pub struct Project { active_entry: Option, languages: Arc, language_servers: HashMap<(WorktreeId, String), Arc>, + loading_language_servers: + HashMap<(WorktreeId, String), watch::Receiver>>>, client: Arc, user_store: ModelHandle, fs: Arc, @@ -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 buffer = buffer.downgrade(); + let language_server = + self.start_language_server(worktree_id, worktree_abs_path, language, cx); - 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); - }); + 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, + &mut self, + worktree_id: WorktreeId, + worktree_path: Arc, language: Arc, - worktree_path: &Path, cx: &mut ModelContext, - ) -> Option> { + ) -> Task>> { 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(); + 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() }); + } - // Listen for `PublishDiagnostics` notifications. - language_server - .on_notification::({ - 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(); - - // 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::(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(); - } - } - 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(); - - // Process all the LSP events. + 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 { - 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(); - } - }); + 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); } - 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(); - }); + }); + } + + 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::({ + 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(); + } } - 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 }) + }) + .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::(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(); + } + } + 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(); + + // 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( diff --git a/script/bundle b/script/bundle index ecc295de9a..bcaa68c1e9 100755 --- a/script/bundle +++ b/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 diff --git a/script/download-rust-analyzer b/script/download-rust-analyzer deleted file mode 100755 index 8c366c5609..0000000000 --- a/script/download-rust-analyzer +++ /dev/null @@ -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-*