From 794fcba842349708bd3eaeef3cc37041124b9ea1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Feb 2022 11:37:52 +0100 Subject: [PATCH 01/15] Temporarily allow uploading artifacts on all branches --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50d817637ecfe6afec5f95238b36b4988215be37..01665afc39acef48446cb90569a2c82e6ab503b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,6 @@ jobs: - name: Upload app bundle to workflow run if main branch uses: actions/upload-artifact@v2 - if: ${{ github.ref == 'refs/heads/main' }} with: name: Zed.dmg path: target/release/Zed.dmg From fc3bccc1a1cd1411cb4be7b8400a6d5816e8bb17 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 20 Feb 2022 12:14:43 +0100 Subject: [PATCH 02/15] WIP: avoid code-signing rust-analyzer to prevent proc macro errors If this works, I think we should set the permissions asynchronously, maybe as part of starting the language server, so that we avoid doing synchronous I/O. --- crates/language/src/language.rs | 11 +++++++++-- script/bundle | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 73de5af12c3db0b2c4d541700225e20d4f9b8e6e..786b158551da35c7f8baee240dc349e5c9d930a6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -260,8 +260,15 @@ impl Language { 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)? + let bundled_path = cx + .platform() + .path_for_resource(Some(&config.binary), None)?; + std::fs::set_permissions( + &bundled_path, + ::from_mode(0o755), + ) + .unwrap(); + bundled_path } else { Path::new(&config.binary).to_path_buf() }; diff --git a/script/bundle b/script/bundle index ecc295de9a76484fdbed475ec203cbcbfb903c99..a9b0fe3ddea1d48463d29868428d7ed5e7685d5d 100755 --- a/script/bundle +++ b/script/bundle @@ -23,6 +23,7 @@ lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/ # Bundle rust-analyzer cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/ +chmod -x target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer # 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 @@ -34,7 +35,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 From b8523509da899f8816bef81f86e7a5c46aa3ed93 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 08:13:05 +0100 Subject: [PATCH 03/15] Revert "WIP: avoid code-signing rust-analyzer to prevent proc macro errors" This reverts commit fc3bccc1a1cd1411cb4be7b8400a6d5816e8bb17. --- crates/language/src/language.rs | 11 ++--------- script/bundle | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 786b158551da35c7f8baee240dc349e5c9d930a6..73de5af12c3db0b2c4d541700225e20d4f9b8e6e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -260,15 +260,8 @@ impl Language { const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE"); let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? { - let bundled_path = cx - .platform() - .path_for_resource(Some(&config.binary), None)?; - std::fs::set_permissions( - &bundled_path, - ::from_mode(0o755), - ) - .unwrap(); - bundled_path + cx.platform() + .path_for_resource(Some(&config.binary), None)? } else { Path::new(&config.binary).to_path_buf() }; diff --git a/script/bundle b/script/bundle index a9b0fe3ddea1d48463d29868428d7ed5e7685d5d..ecc295de9a76484fdbed475ec203cbcbfb903c99 100755 --- a/script/bundle +++ b/script/bundle @@ -23,7 +23,6 @@ lipo -create target/x86_64-apple-darwin/release/Zed target/aarch64-apple-darwin/ # Bundle rust-analyzer cp vendor/bin/rust-analyzer target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/ -chmod -x target/x86_64-apple-darwin/release/bundle/osx/Zed.app/Contents/Resources/rust-analyzer # 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 @@ -35,6 +34,7 @@ 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 From 1ca50d0134dc861714d1851a9d94afa8d237dab4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 09:39:28 +0100 Subject: [PATCH 04/15] Make language server initialization asynchronous --- 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(-) delete mode 100755 script/download-rust-analyzer diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 73de5af12c3db0b2c4d541700225e20d4f9b8e6e..b5a88cf8436d94f8f6f174dd6271b6cec4f3a6d8 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 9a231b707bba42a9d4f4c4f05429c4b881f55642..24d632ad2c268fb2c84c1b2ea19ad26970b0a071 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 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, + &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(); - - // 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(); + 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::({ + 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::(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::(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( diff --git a/script/bundle b/script/bundle index ecc295de9a76484fdbed475ec203cbcbfb903c99..bcaa68c1e9c04c9d6aaa7379a5b2de5e82920a93 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 8c366c5609fc9fc813973a41ade4ca33d4103c8b..0000000000000000000000000000000000000000 --- 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-* From fafe521e9fea664500c0065548e9d8a442be025d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 09:46:18 +0100 Subject: [PATCH 05/15] Introduce `LspPostProcessor::download_language_server` --- crates/language/src/language.rs | 32 ++++++++++++++++++++++++-------- crates/zed/src/language.rs | 7 +++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b5a88cf8436d94f8f6f174dd6271b6cec4f3a6d8..39aa74e943dff5816f3fc08ee19c998fb6623e6f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -8,12 +8,20 @@ mod tests; use anyhow::{anyhow, Result}; use collections::HashSet; +use futures::future::BoxFuture; 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, + future::Future, + ops::Range, + path::{Path, PathBuf}, + str, + sync::Arc, +}; use theme::SyntaxTheme; use tree_sitter::{self, Query}; @@ -48,6 +56,7 @@ pub trait ToLspPosition { } pub trait LspPostProcessor: 'static + Send + Sync { + fn download_language_server(&self) -> BoxFuture<'static, Result>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -259,13 +268,14 @@ impl Language { } 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 }) + let server_binary_path = self + .download_language_server() + .ok_or_else(|| anyhow!("cannot download language server")); + 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(Some(server)) + }) } else { Task::ready(Ok(None)) } @@ -321,6 +331,12 @@ impl Language { result } + fn download_language_server(&self) -> Option>> { + self.lsp_post_processor + .as_ref() + .map(|processor| processor.download_language_server()) + } + pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index a1ad424c117fd7dd498aa79b4d48fe36bb345534..e8b88b6a31a81d448bbf6eb698f9e64942c23237 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -1,8 +1,11 @@ +use anyhow::Result; +use futures::future::BoxFuture; pub use language::*; use lazy_static::lazy_static; use regex::Regex; use rust_embed::RustEmbed; use std::borrow::Cow; +use std::path::PathBuf; use std::{str, sync::Arc}; #[derive(RustEmbed)] @@ -12,6 +15,10 @@ struct LanguageDir; struct RustPostProcessor; impl LspPostProcessor for RustPostProcessor { + fn download_language_server(&self) -> BoxFuture<'static, Result> { + todo!() + } + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); From 793d9e8bbac3e91dbe22c9ea149c3e340c78e1f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 16:07:09 +0100 Subject: [PATCH 06/15] Download rust-analyzer from GitHub --- Cargo.lock | 1 + crates/language/src/language.rs | 62 +++++++++------- crates/zed/Cargo.toml | 1 + crates/zed/languages/rust/config.toml | 1 - crates/zed/src/language.rs | 100 ++++++++++++++++++++++---- 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bc19c7ad98e1a5da85c24391d94941c94e7b01b..3fec56a450a0c539161354b0b50b0d25a36c29d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5741,6 +5741,7 @@ name = "zed" version = "0.15.2" dependencies = [ "anyhow", + "async-compression", "async-recursion", "async-trait", "chat_panel", diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 39aa74e943dff5816f3fc08ee19c998fb6623e6f..624ae29b86e9477ec5c36c88fa581ea5d72ce442 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -8,7 +8,10 @@ mod tests; use anyhow::{anyhow, Result}; use collections::HashSet; -use futures::future::BoxFuture; +use futures::{ + future::{BoxFuture, Shared}, + FutureExt, +}; use gpui::{AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; @@ -55,8 +58,8 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub trait LspPostProcessor: 'static + Send + Sync { - fn download_language_server(&self) -> BoxFuture<'static, Result>; +pub trait LspExt: 'static + Send + Sync { + fn server_bin_path(&self) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -86,7 +89,6 @@ pub struct LanguageConfig { #[derive(Default, Deserialize)] pub struct LanguageServerConfig { - pub binary: String, pub disk_based_diagnostic_sources: HashSet, pub disk_based_diagnostics_progress_token: Option, #[cfg(any(test, feature = "test-support"))] @@ -112,7 +114,8 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) lsp_post_processor: Option>, + pub(crate) lsp_ext: Option>, + lsp_binary_path: Mutex>>>>, } pub struct Grammar { @@ -179,7 +182,8 @@ impl Language { highlight_map: Default::default(), }) }), - lsp_post_processor: None, + lsp_ext: None, + lsp_binary_path: Default::default(), } } @@ -223,8 +227,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, processor: impl LspExt) -> Self { + self.lsp_ext = Some(Box::new(processor)); self } @@ -241,8 +245,8 @@ impl Language { root_path: Arc, cx: &AppContext, ) -> Task>>> { + #[cfg(any(test, feature = "test-support"))] 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; @@ -266,19 +270,20 @@ impl Language { return Task::ready(Ok(Some(server.clone()))); } + } - let background = cx.background().clone(); - let server_binary_path = self - .download_language_server() - .ok_or_else(|| anyhow!("cannot download language server")); - cx.background().spawn(async move { - let server_binary_path = server_binary_path?.await?; + let background = cx.background().clone(); + let server_binary_path = self + .lsp_binary_path() + .ok_or_else(|| anyhow!("cannot locate or download language server")); + cx.background().spawn(async move { + if let Some(server_binary_path) = server_binary_path?.await { let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?; Ok(Some(server)) - }) - } else { - Task::ready(Ok(None)) - } + } else { + Ok(None) + } + }) } pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { @@ -296,7 +301,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); } } @@ -305,7 +310,7 @@ impl Language { &self, completion: &lsp::CompletionItem, ) -> Option { - self.lsp_post_processor + self.lsp_ext .as_ref()? .label_for_completion(completion, self) } @@ -331,10 +336,17 @@ impl Language { result } - fn download_language_server(&self) -> Option>> { - self.lsp_post_processor - .as_ref() - .map(|processor| processor.download_language_server()) + fn lsp_binary_path(&self) -> Option>> { + if let Some(lsp_ext) = self.lsp_ext.as_ref() { + Some( + self.lsp_binary_path + .lock() + .get_or_insert_with(|| lsp_ext.server_bin_path().shared()) + .clone(), + ) + } else { + None + } } pub fn brackets(&self) -> &[BracketPair] { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3f4cf8a96f6c26ea95ad0b0568f0d95961d1d7e1..cd581cb90dd3bc28444ed868ae8b36156bf86b21 100644 --- a/crates/zed/Cargo.toml +++ b/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" diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index 426dcc2b48b3c16b74a255499180605b099c6a7c..e4bf50a92972ab71f44fb198986b0b9896193ecc 100644 --- a/crates/zed/languages/rust/config.toml +++ b/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" diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index e8b88b6a31a81d448bbf6eb698f9e64942c23237..93cc777bb76ba5ab47f6facf7373df7da875becc 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -1,22 +1,98 @@ -use anyhow::Result; -use futures::future::BoxFuture; +use anyhow::anyhow; +use async_compression::futures::bufread::GzipDecoder; +use client::http; +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::path::PathBuf; -use std::{str, sync::Arc}; +use serde::Deserialize; +use smol::fs::{self, File}; +use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] #[folder = "languages"] struct LanguageDir; -struct RustPostProcessor; +struct RustLsp; -impl LspPostProcessor for RustPostProcessor { - fn download_language_server(&self) -> BoxFuture<'static, Result> { - todo!() +#[derive(Deserialize)] +struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +impl RustLsp { + async fn download(destination_dir_path: PathBuf) -> anyhow::Result { + let client = surf::client().with(surf::middleware::Redirect::default()); + let release = client + .get("https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest") + .recv_json::() + .await + .map_err(|err| anyhow!("error getting latest release: {}", err))?; + let release_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == release_name) + .ok_or_else(|| anyhow!("no release found matching {:?}", release_name))?; + + let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name)); + if fs::metadata(&destination_path).await.is_ok() { + let response = client + .get(&asset.browser_download_url) + .send() + .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, + ::from_mode(0o755), + ) + .await?; + } + + Ok::<_, anyhow::Error>(destination_path) + } +} + +impl LspExt for RustLsp { + fn server_bin_path(&self) -> BoxFuture<'static, Option> { + async move { + let destination_dir_path = dirs::home_dir() + .ok_or_else(|| anyhow!("can't determine home directory"))? + .join(".zed/rust-analyzer"); + fs::create_dir_all(&destination_dir_path).await?; + + let mut server_bin_path = Self::download(destination_dir_path.clone()).await.log_err(); + 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 let Some(downloaded_server_path) = server_bin_path.as_ref() { + if downloaded_server_path != entry_path.as_path() { + fs::remove_file(entry_path).await.log_err(); + } + } else { + server_bin_path = Some(entry_path); + } + } + } + } + + server_bin_path.ok_or_else(|| anyhow!("could not locate or download server")) + } + .log_err() + .boxed() } fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { @@ -138,7 +214,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 { @@ -160,7 +236,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] @@ -187,7 +263,7 @@ mod tests { }, ], }; - RustPostProcessor.process_diagnostics(&mut params); + RustLsp.process_diagnostics(&mut params); assert_eq!(params.diagnostics[0].message, "use of moved value `a`"); From d2c83a70970b8cec5b68e1b0be749a7c22434312 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 17:04:47 +0100 Subject: [PATCH 07/15] Use a `Shared` future to represent started language servers Co-Authored-By: Nathan Sobo --- crates/project/src/project.rs | 248 +++++++++++++++++----------------- 1 file changed, 126 insertions(+), 122 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 24d632ad2c268fb2c84c1b2ea19ad26970b0a071..d1303900cf6db5276f7dbb8b0d56412e5e569e27 100644 --- a/crates/project/src/project.rs +++ b/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,8 +39,8 @@ pub struct Project { active_entry: Option, languages: Arc, language_servers: HashMap<(WorktreeId, String), Arc>, - loading_language_servers: - HashMap<(WorktreeId, String), watch::Receiver>>>, + started_language_servers: + HashMap<(WorktreeId, String), Shared>>>>, client: Arc, user_store: ModelHandle, fs: Arc, @@ -260,7 +260,7 @@ impl Project { fs, language_servers_with_diagnostics_running: 0, language_servers: Default::default(), - loading_language_servers: Default::default(), + started_language_servers: Default::default(), } }) } @@ -312,7 +312,7 @@ impl Project { }, language_servers_with_diagnostics_running: 0, language_servers: Default::default(), - loading_language_servers: Default::default(), + started_language_servers: Default::default(), }; for worktree in worktrees { this.add_worktree(&worktree, cx); @@ -824,7 +824,7 @@ impl Project { worktree_path: Arc, language: Arc, cx: &mut ModelContext, - ) -> Task>> { + ) -> Shared>>> { enum LspEvent { DiagnosticsStart, DiagnosticsUpdate(lsp::PublishDiagnosticsParams), @@ -832,131 +832,137 @@ impl Project { } 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); + self.started_language_servers + .entry(key.clone()) + .or_insert_with(|| { + 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, _| { + if let Some(language_server) = language_server.clone() { + this.language_servers.insert(key, language_server); + } + }); } - }); - } - 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(); - } - } - }) - .detach(); + let language_server = language_server?; - // 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), - }; + 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(); + } + } + }) + .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(); + 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(); + + // 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(); + } + }); } - _ => {} - }, - } - } - }) - .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::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(); + 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( @@ -2885,8 +2891,6 @@ impl Entity for Project { &mut self, _: &mut MutableAppContext, ) -> Option>>> { - use futures::FutureExt; - let shutdown_futures = self .language_servers .drain() From aee479d61555fbb5931a7d16959861c0ad50e26a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 17:25:52 +0100 Subject: [PATCH 08/15] Show message indicating when we're downloading language servers Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/editor/src/items.rs | 2 - crates/language/src/language.rs | 30 ++++++++++-- crates/language/src/tests.rs | 47 ++++++++++--------- crates/project/src/project.rs | 18 +++++--- crates/server/src/rpc.rs | 72 ++++++++++++++++------------- crates/theme/src/theme.rs | 1 + crates/workspace/Cargo.toml | 1 + crates/workspace/src/lsp_status.rs | 65 ++++++++++++++++++++++++++ crates/workspace/src/status_bar.rs | 24 ++++++---- crates/workspace/src/workspace.rs | 1 + crates/zed/assets/themes/_base.toml | 5 +- crates/zed/src/language.rs | 11 +++-- crates/zed/src/main.rs | 2 +- crates/zed/src/test.rs | 19 ++++---- crates/zed/src/zed.rs | 8 ++++ 16 files changed, 214 insertions(+), 93 deletions(-) create mode 100644 crates/workspace/src/lsp_status.rs diff --git a/Cargo.lock b/Cargo.lock index 3fec56a450a0c539161354b0b50b0d25a36c29d3..76fb2e7cb068efb3931a217b964e3d8b435449dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5701,6 +5701,7 @@ dependencies = [ "client", "clock", "collections", + "futures", "gpui", "language", "log", diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c669bc744e1b7dc82642aeba9f2e4ac8248a8a9d..af12cae947f91618db76365df6e319b3c60b70d6 100644 --- a/crates/editor/src/items.rs +++ b/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() diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 624ae29b86e9477ec5c36c88fa581ea5d72ce442..34276c051e428bf13116719ae7c1ce77b4733ac7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -12,10 +12,11 @@ use futures::{ future::{BoxFuture, Shared}, FutureExt, }; -use gpui::{AppContext, Task}; +use gpui::{executor, AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; use parking_lot::Mutex; +use postage::watch; use serde::Deserialize; use std::{ cell::RefCell, @@ -127,18 +128,33 @@ pub struct Grammar { pub(crate) highlight_map: Mutex, } -#[derive(Default)] pub struct LanguageRegistry { languages: Vec>, + pending_lsp_binaries_tx: Arc>>, + pending_lsp_binaries_rx: watch::Receiver, } impl LanguageRegistry { pub fn new() -> Self { - Self::default() + let (pending_lsp_binaries_tx, pending_lsp_binaries_rx) = watch::channel(); + Self { + languages: Default::default(), + pending_lsp_binaries_tx: Arc::new(Mutex::new(pending_lsp_binaries_tx)), + pending_lsp_binaries_rx, + } } - pub fn add(&mut self, language: Arc) { - self.languages.push(language); + pub fn add(&mut self, language: Arc, cx: &executor::Background) { + self.languages.push(language.clone()); + if let Some(lsp_binary_path) = language.lsp_binary_path() { + let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone(); + cx.spawn(async move { + *pending_lsp_binaries_tx.lock().borrow_mut() += 1; + lsp_binary_path.await; + *pending_lsp_binaries_tx.lock().borrow_mut() -= 1; + }) + .detach(); + } } pub fn set_theme(&self, theme: &SyntaxTheme) { @@ -166,6 +182,10 @@ impl LanguageRegistry { .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) }) } + + pub fn pending_lsp_binaries(&self) -> watch::Receiver { + self.pending_lsp_binaries_rx.clone() + } } impl Language { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 0ae1fbe7074ce341bd4e9ea45b4336096b572df5..98ce02e2f641a757663576af41eb3b9063335d35 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -22,28 +22,31 @@ fn init_logger() { } } -#[test] -fn test_select_language() { - let registry = LanguageRegistry { - languages: vec![ - Arc::new(Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )), - Arc::new(Language::new( - LanguageConfig { - name: "Make".to_string(), - path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )), - ], - }; +#[gpui::test] +fn test_select_language(cx: &mut MutableAppContext) { + let mut registry = LanguageRegistry::new(); + registry.add( + Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )), + cx.background(), + ); + registry.add( + Arc::new(Language::new( + LanguageConfig { + name: "Make".to_string(), + path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )), + cx.background(), + ); // matching file extension assert_eq!( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d1303900cf6db5276f7dbb8b0d56412e5e569e27..7b53eba88bcc6b2ebc599dc61d41b3b717dd3ad5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3069,8 +3069,10 @@ mod tests { .await; let project = Project::test(fs, &mut cx); - project.update(&mut cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); + project.update(&mut cx, |project, cx| { + Arc::get_mut(&mut project.languages) + .unwrap() + .add(language, cx.background()); }); let (tree, _) = project @@ -3215,8 +3217,10 @@ mod tests { .await; let project = Project::test(fs, &mut cx); - project.update(&mut cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); + project.update(&mut cx, |project, cx| { + Arc::get_mut(&mut project.languages) + .unwrap() + .add(language, cx.background()); }); let (tree, _) = project @@ -4108,8 +4112,10 @@ mod tests { .await; let project = Project::test(fs.clone(), &mut cx); - project.update(&mut cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); + project.update(&mut cx, |project, cx| { + Arc::get_mut(&mut project.languages) + .unwrap() + .add(language, cx.background()); }); let (tree, _) = project diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index c6a0ef2be6c5d554feebe2422aab06649bd61159..db9843fc95683ae6ae17d56d0de7ef76428d8c9a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -2001,9 +2001,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2011,7 +2010,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2232,9 +2233,8 @@ mod tests { }), ..Default::default() }); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2242,7 +2242,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2434,9 +2436,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2444,7 +2445,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2551,9 +2554,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2561,7 +2563,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2699,9 +2703,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2709,7 +2712,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2800,9 +2805,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -2810,7 +2814,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3039,9 +3045,8 @@ mod tests { // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3049,7 +3054,9 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )), + &cx_a.background(), + ); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3845,9 +3852,8 @@ mod tests { }); }); - Arc::get_mut(&mut host_lang_registry) - .unwrap() - .add(Arc::new(Language::new( + Arc::get_mut(&mut host_lang_registry).unwrap().add( + Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3855,7 +3861,9 @@ mod tests { ..Default::default() }, None, - ))); + )), + &cx.background(), + ); let fs = FakeFs::new(cx.background()); fs.insert_tree( diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1e63830792f2e5c9e84a7e2261a171cf60318f70..a30dccb1a3154a90754b96293b1e927fc2dd4a7f 100644 --- a/crates/theme/src/theme.rs +++ b/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)] diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index f3ea330ca319620aa14f115a501b648798dde7a2..a7a8bfa744f1e57fec37785a256a598f8e27e843 100644 --- a/crates/workspace/Cargo.toml +++ b/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"] } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs new file mode 100644 index 0000000000000000000000000000000000000000..e254a05a931ea255a76b9a3d76b324783b64112b --- /dev/null +++ b/crates/workspace/src/lsp_status.rs @@ -0,0 +1,65 @@ +use crate::{ItemViewHandle, Settings, StatusItemView}; +use futures::StreamExt; +use gpui::{elements::*, Entity, RenderContext, View, ViewContext}; +use language::LanguageRegistry; +use postage::watch; +use std::sync::Arc; + +pub struct LspStatus { + pending_lsp_binaries: usize, + settings_rx: watch::Receiver, +} + +impl LspStatus { + pub fn new( + languages: Arc, + settings_rx: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let mut pending_lsp_binaries = languages.pending_lsp_binaries(); + cx.spawn_weak(|this, mut cx| async move { + while let Some(pending_lsp_binaries) = pending_lsp_binaries.next().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.pending_lsp_binaries = pending_lsp_binaries; + cx.notify(); + }); + } else { + break; + } + } + }) + .detach(); + Self { + pending_lsp_binaries: 0, + settings_rx, + } + } +} + +impl Entity for LspStatus { + type Event = (); +} + +impl View for LspStatus { + fn ui_name() -> &'static str { + "LspStatus" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + if self.pending_lsp_binaries == 0 { + Empty::new().boxed() + } else { + let theme = &self.settings_rx.borrow().theme; + Label::new( + "Downloading language servers...".to_string(), + theme.workspace.status_bar.lsp_message.clone(), + ) + .boxed() + } + } +} + +impl StatusItemView for LspStatus { + fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext) {} +} diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 2d26c33a8aa70e4ec44b358367751a69f6c86f4a..d4cd939cb8afc08a38b0c0f3399035d905187978 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -42,17 +42,21 @@ impl View for StatusBar { fn render(&mut self, _: &mut RenderContext) -> 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() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ddff6f9bf7bb3cf6e3da9ae449514ec06d8204da..c7368ddbe96da439aea8443536e1dc64ff095d68 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,3 +1,4 @@ +pub mod lsp_status; pub mod menu; pub mod pane; pub mod pane_group; diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index a9c500b640df2b512642b054a12d4917a48ea433..5b9f822bf96f97c1b99278bfb61a11e42b8ba56b 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/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" diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 93cc777bb76ba5ab47f6facf7373df7da875becc..da5fa8105f5dc4ea59bd3adff48af9868424ee5a 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -2,6 +2,7 @@ use anyhow::anyhow; use async_compression::futures::bufread::GzipDecoder; use client::http; use futures::{future::BoxFuture, FutureExt, StreamExt}; +use gpui::executor; pub use language::*; use lazy_static::lazy_static; use regex::Regex; @@ -45,7 +46,7 @@ impl RustLsp { .ok_or_else(|| anyhow!("no release found matching {:?}", release_name))?; let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name)); - if fs::metadata(&destination_path).await.is_ok() { + if fs::metadata(&destination_path).await.is_err() { let response = client .get(&asset.browser_download_url) .send() @@ -195,10 +196,10 @@ impl LspExt for RustLsp { } } -pub fn build_language_registry() -> LanguageRegistry { - let mut languages = LanguageRegistry::default(); - languages.add(Arc::new(rust())); - languages.add(Arc::new(markdown())); +pub fn build_language_registry(executor: &Arc) -> LanguageRegistry { + let mut languages = LanguageRegistry::new(); + languages.add(Arc::new(rust()), executor); + languages.add(Arc::new(markdown()), executor); languages } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b15424134399f0ec98e5606421ecff199abd55e4..0724f145246b14792b4eca97bce436b433022059 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -39,7 +39,7 @@ fn main() { }, ); let (settings_tx, settings) = postage::watch::channel_with(settings); - let languages = Arc::new(language::build_language_registry()); + let languages = Arc::new(language::build_language_registry(&app.background())); languages.set_theme(&settings.borrow().theme.editor.syntax); app.run(move |cx| { diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 18819b25a6452ff3144cd57c351a3036414eca8b..608dea4fa2861deac33ace03ee4af4534123309d 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -26,14 +26,17 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let client = Client::new(http.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + languages.add( + Arc::new(language::Language::new( + language::LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )), + cx.background(), + ); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4c98e0fa98fd5b8847206a84d74e836c4db4f40e..7178ff44eb7e3dae0b0df21b792d7ea6f16a66bd 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -97,11 +97,19 @@ pub fn build_workspace( cx, ) }); + let lsp_status = cx.add_view(|cx| { + workspace::lsp_status::LspStatus::new( + app_state.languages.clone(), + app_state.settings.clone(), + cx, + ) + }); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone())); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); status_bar.add_left_item(diagnostic_message, cx); + status_bar.add_left_item(lsp_status, cx); status_bar.add_right_item(cursor_position, cx); }); From af0ce62d3b8a1507aa8a56708d668f8c162da7dd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 17:28:55 +0100 Subject: [PATCH 09/15] Remove rust-analyzer smoke test Co-Authored-By: Nathan Sobo --- .github/workflows/ci.yml | 10 +------ crates/lsp/src/lsp.rs | 60 ---------------------------------------- 2 files changed, 1 insertion(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01665afc39acef48446cb90569a2c82e6ab503b0..35d20cc5250ff400988f1e342737aa3553c5a956 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 0281e8cd8bd17080c0f8aa68a2035682050a9147..b05befedf847215238f5b578028ce6967a956e27 100644 --- a/crates/lsp/src/lsp.rs +++ b/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::(DidOpenTextDocumentParams { - text_document: TextDocumentItem::new( - lib_file_uri.clone(), - "rust".to_string(), - 0, - lib_source, - ), - }) - .await - .unwrap(); - - let hover = server - .request::(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()); From 277d86bd29a9f977bf3ad8da4e22a4060b868374 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 17:31:43 +0100 Subject: [PATCH 10/15] Remove unused method --- crates/lsp/src/lsp.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index b05befedf847215238f5b578028ce6967a956e27..93df14f89dd9b08c849b074d6ddbd00f068de5b0 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -768,19 +768,6 @@ mod tests { fake.receive_notification::().await; } - impl LanguageServer { - async fn next_idle_notification(self: &Arc) { - let (tx, rx) = channel::unbounded(); - let _subscription = - self.on_notification::(move |params| { - if params.quiescent { - tx.try_send(()).unwrap(); - } - }); - let _ = rx.recv().await; - } - } - pub enum ServerStatusNotification {} impl notification::Notification for ServerStatusNotification { From db23a87228704ad49dfb171b0827e4d0d758778a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Feb 2022 18:04:29 +0100 Subject: [PATCH 11/15] Revert "Temporarily allow uploading artifacts on all branches" This reverts commit 794fcba842349708bd3eaeef3cc37041124b9ea1. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35d20cc5250ff400988f1e342737aa3553c5a956..4050d4e120140b64ccfc7745cba12b0940a216f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,7 @@ jobs: - name: Upload app bundle to workflow run if main branch uses: actions/upload-artifact@v2 + if: ${{ github.ref == 'refs/heads/main' }} with: name: Zed.dmg path: target/release/Zed.dmg From ededfff3a8fa24901f5396a25172a6862a24b160 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Feb 2022 13:54:52 -0800 Subject: [PATCH 12/15] Download language servers on-demand Co-Authored-By: Nathan Sobo --- Cargo.lock | 1 + crates/client/src/client.rs | 4 + crates/language/Cargo.toml | 3 + crates/language/src/language.rs | 148 ++++++++++++++++---------------- crates/language/src/tests.rs | 40 ++++----- crates/project/src/project.rs | 33 +++---- crates/server/src/rpc.rs | 16 ++-- crates/zed/src/language.rs | 74 ++++++++++------ crates/zed/src/main.rs | 2 +- crates/zed/src/test.rs | 2 +- 10 files changed, 172 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76fb2e7cb068efb3931a217b964e3d8b435449dc..7bf51789b764aeb7380f3487c84dad4c030b176a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2620,6 +2620,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "client", "clock", "collections", "ctor", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 93ab2c6ad661ec5a20c9bb56cdbb48c17d27fb21..2c31e8eef376311210ab9e3ae76d703ca903eb69 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -224,6 +224,10 @@ impl Client { self.id } + pub fn http_client(&self) -> Arc { + self.http.clone() + } + #[cfg(any(test, feature = "test-support"))] pub fn override_authenticate(&mut self, authenticate: F) -> &mut Self where diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index eef1a010546b9c3b3e3c0f5e05b57a5bfe382f26..5a3637c3bb956982213a0bccfdf6b1f322b405e8 100644 --- a/crates/language/Cargo.toml +++ b/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" } @@ -44,6 +46,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"] } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 34276c051e428bf13116719ae7c1ce77b4733ac7..fd91d2e546c0673a8088a05088125064c6712690 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,12 +7,13 @@ pub mod proto; mod tests; use anyhow::{anyhow, Result}; +use client::http::HttpClient; use collections::HashSet; use futures::{ future::{BoxFuture, Shared}, - FutureExt, + FutureExt, TryFutureExt, }; -use gpui::{executor, AppContext, Task}; +use gpui::{AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -20,7 +21,6 @@ use postage::watch; use serde::Deserialize; use std::{ cell::RefCell, - future::Future, ops::Range, path::{Path, PathBuf}, str, @@ -60,7 +60,10 @@ pub trait ToLspPosition { } pub trait LspExt: 'static + Send + Sync { - fn server_bin_path(&self) -> BoxFuture<'static, Option>; + fn fetch_latest_language_server( + &self, + http: Arc, + ) -> BoxFuture<'static, Result>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -116,7 +119,7 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) lsp_ext: Option>, - lsp_binary_path: Mutex>>>>, + lsp_binary_path: Mutex>>>>>, } pub struct Grammar { @@ -144,17 +147,8 @@ impl LanguageRegistry { } } - pub fn add(&mut self, language: Arc, cx: &executor::Background) { + pub fn add(&mut self, language: Arc) { self.languages.push(language.clone()); - if let Some(lsp_binary_path) = language.lsp_binary_path() { - let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone(); - cx.spawn(async move { - *pending_lsp_binaries_tx.lock().borrow_mut() += 1; - lsp_binary_path.await; - *pending_lsp_binaries_tx.lock().borrow_mut() -= 1; - }) - .detach(); - } } pub fn set_theme(&self, theme: &SyntaxTheme) { @@ -183,6 +177,71 @@ impl LanguageRegistry { }) } + pub fn start_language_server( + &self, + language: &Arc, + root_path: Arc, + http_client: Arc, + cx: &AppContext, + ) -> Option>>> { + #[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 lsp_ext = language.lsp_ext.as_ref()?; + let background = cx.background().clone(); + let server_binary_path = { + Some( + language + .lsp_binary_path + .lock() + .get_or_insert_with(|| { + let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone(); + let language_server_path = + lsp_ext.fetch_latest_language_server(http_client); + async move { + *pending_lsp_binaries_tx.lock().borrow_mut() += 1; + let path = language_server_path.map_err(Arc::new).await; + *pending_lsp_binaries_tx.lock().borrow_mut() -= 1; + path + } + .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 pending_lsp_binaries(&self) -> watch::Receiver { self.pending_lsp_binaries_rx.clone() } @@ -260,52 +319,6 @@ impl Language { self.config.line_comment.as_deref() } - pub fn start_server( - &self, - root_path: Arc, - cx: &AppContext, - ) -> Task>>> { - #[cfg(any(test, feature = "test-support"))] - if let Some(config) = &self.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 Task::ready(Ok(Some(server.clone()))); - } - } - - let background = cx.background().clone(); - let server_binary_path = self - .lsp_binary_path() - .ok_or_else(|| anyhow!("cannot locate or download language server")); - cx.background().spawn(async move { - if let Some(server_binary_path) = server_binary_path?.await { - let server = lsp::LanguageServer::new(&server_binary_path, &root_path, background)?; - Ok(Some(server)) - } else { - Ok(None) - } - }) - } - pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { self.config .language_server @@ -356,19 +369,6 @@ impl Language { result } - fn lsp_binary_path(&self) -> Option>> { - if let Some(lsp_ext) = self.lsp_ext.as_ref() { - Some( - self.lsp_binary_path - .lock() - .get_or_insert_with(|| lsp_ext.server_bin_path().shared()) - .clone(), - ) - } else { - None - } - } - pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 98ce02e2f641a757663576af41eb3b9063335d35..1adcca91a5c3cb6b8276d34e1e369c494359f715 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -23,30 +23,24 @@ fn init_logger() { } #[gpui::test] -fn test_select_language(cx: &mut MutableAppContext) { +fn test_select_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()), - )), - cx.background(), - ); - registry.add( - Arc::new(Language::new( - LanguageConfig { - name: "Make".to_string(), - path_suffixes: vec!["Makefile".to_string(), "mk".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )), - cx.background(), - ); + 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!( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7b53eba88bcc6b2ebc599dc61d41b3b717dd3ad5..c1ef8fdfcdfb7cf38c97839a42782c37bb7a84f9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -835,20 +835,21 @@ impl Project { self.started_language_servers .entry(key.clone()) .or_insert_with(|| { - let language_server = language.start_server(worktree_path, cx); + 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().flatten(); + let language_server = language_server?.await.log_err()?; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, _| { - if let Some(language_server) = language_server.clone() { - this.language_servers.insert(key, language_server); - } + this.language_servers.insert(key, language_server.clone()); }); } - let language_server = language_server?; - let disk_based_sources = language .disk_based_diagnostic_sources() .cloned() @@ -3069,10 +3070,8 @@ mod tests { .await; let project = Project::test(fs, &mut cx); - project.update(&mut cx, |project, cx| { - Arc::get_mut(&mut project.languages) - .unwrap() - .add(language, cx.background()); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project @@ -3217,10 +3216,8 @@ mod tests { .await; let project = Project::test(fs, &mut cx); - project.update(&mut cx, |project, cx| { - Arc::get_mut(&mut project.languages) - .unwrap() - .add(language, cx.background()); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project @@ -4112,10 +4109,8 @@ mod tests { .await; let project = Project::test(fs.clone(), &mut cx); - project.update(&mut cx, |project, cx| { - Arc::get_mut(&mut project.languages) - .unwrap() - .add(language, cx.background()); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index db9843fc95683ae6ae17d56d0de7ef76428d8c9a..0bfb918b45ad42e4d6498a58ff98ee7ed35cd2c6 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -2011,7 +2011,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -2243,7 +2243,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -2446,7 +2446,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -2564,7 +2564,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -2713,7 +2713,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -2815,7 +2815,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -3055,7 +3055,7 @@ mod tests { }, Some(tree_sitter_rust::language()), )), - &cx_a.background(), + ); // Connect to a server as 2 clients. @@ -3862,7 +3862,7 @@ mod tests { }, None, )), - &cx.background(), + ); let fs = FakeFs::new(cx.background()); diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index da5fa8105f5dc4ea59bd3adff48af9868424ee5a..1203e87309e5ecf66a1afb59f8def33e2855f771 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -1,8 +1,7 @@ -use anyhow::anyhow; +use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use client::http; +use client::http::{self, HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui::executor; pub use language::*; use lazy_static::lazy_static; use regex::Regex; @@ -10,7 +9,7 @@ use rust_embed::RustEmbed; use serde::Deserialize; use smol::fs::{self, File}; use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; -use util::{ResultExt, TryFutureExt}; +use util::ResultExt; #[derive(RustEmbed)] #[folder = "languages"] @@ -31,13 +30,27 @@ struct GithubReleaseAsset { } impl RustLsp { - async fn download(destination_dir_path: PathBuf) -> anyhow::Result { - let client = surf::client().with(surf::middleware::Redirect::default()); - let release = client - .get("https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest") - .recv_json::() + async fn download( + destination_dir_path: PathBuf, + http: Arc, + ) -> anyhow::Result { + 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::() .await - .map_err(|err| anyhow!("error getting latest release: {}", err))?; + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; let release_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); let asset = release .assets @@ -47,9 +60,12 @@ impl RustLsp { let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name)); if fs::metadata(&destination_path).await.is_err() { - let response = client - .get(&asset.browser_download_url) - .send() + let response = http + .send( + surf::RequestBuilder::new(Method::Get, asset.browser_download_url.clone()) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) .await .map_err(|err| anyhow!("error downloading release: {}", err))?; let decompressed_bytes = GzipDecoder::new(response); @@ -67,32 +83,40 @@ impl RustLsp { } impl LspExt for RustLsp { - fn server_bin_path(&self) -> BoxFuture<'static, Option> { + fn fetch_latest_language_server( + &self, + http: Arc, + ) -> BoxFuture<'static, Result> { async move { let destination_dir_path = dirs::home_dir() .ok_or_else(|| anyhow!("can't determine home directory"))? .join(".zed/rust-analyzer"); fs::create_dir_all(&destination_dir_path).await?; - let mut server_bin_path = Self::download(destination_dir_path.clone()).await.log_err(); + let downloaded_bin_path = Self::download(destination_dir_path.clone(), http).await; + let mut last_cached_bin_path = None; 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 let Some(downloaded_server_path) = server_bin_path.as_ref() { - if downloaded_server_path != entry_path.as_path() { - fs::remove_file(entry_path).await.log_err(); + if let Ok(downloaded_bin_path) = downloaded_bin_path.as_ref() { + if downloaded_bin_path != entry_path.as_path() { + fs::remove_file(&entry_path).await.log_err(); } - } else { - server_bin_path = Some(entry_path); } + last_cached_bin_path = Some(entry_path); } } } - server_bin_path.ok_or_else(|| anyhow!("could not locate or download server")) + if downloaded_bin_path.is_err() { + if let Some(last_cached_bin_path) = last_cached_bin_path { + return Ok(last_cached_bin_path); + } + } + + downloaded_bin_path } - .log_err() .boxed() } @@ -196,10 +220,10 @@ impl LspExt for RustLsp { } } -pub fn build_language_registry(executor: &Arc) -> LanguageRegistry { +pub fn build_language_registry() -> LanguageRegistry { let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(rust()), executor); - languages.add(Arc::new(markdown()), executor); + languages.add(Arc::new(rust())); + languages.add(Arc::new(markdown())); languages } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0724f145246b14792b4eca97bce436b433022059..b15424134399f0ec98e5606421ecff199abd55e4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -39,7 +39,7 @@ fn main() { }, ); let (settings_tx, settings) = postage::watch::channel_with(settings); - let languages = Arc::new(language::build_language_registry(&app.background())); + let languages = Arc::new(language::build_language_registry()); languages.set_theme(&settings.borrow().theme.editor.syntax); app.run(move |cx| { diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 608dea4fa2861deac33ace03ee4af4534123309d..8df47ee95e6b72cae941315ff5e00d3beef86c7c 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -35,7 +35,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { }, Some(tree_sitter_rust::language()), )), - cx.background(), + ); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), From 79910ba9311b92a8922d6ba45f4ab7c34ab333c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Feb 2022 16:11:40 -0800 Subject: [PATCH 13/15] Show more information in lsp status bar item * Distinguish between checking for updates and downloading * Show dismissable error message when downloading failed and there is no cached server. Co-Authored-By: Nathan Sobo --- Cargo.lock | 16 +++- crates/language/Cargo.toml | 1 + crates/language/src/language.rs | 120 +++++++++++++++++++++++------ crates/workspace/src/lsp_status.rs | 98 +++++++++++++++++++---- crates/zed/src/language.rs | 114 +++++++++++++++------------ crates/zed/src/zed.rs | 2 + 6 files changed, 262 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bf51789b764aeb7380f3487c84dad4c030b176a..c3e6f6f884e677f905c96d0946618faccecb7ed9 100644 --- a/Cargo.lock +++ b/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,6 +2630,7 @@ name = "language" version = "0.1.0" dependencies = [ "anyhow", + "async-broadcast", "async-trait", "client", "clock", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 5a3637c3bb956982213a0bccfdf6b1f322b405e8..ec90b9c76afc0ee278b2dc059bc105d5ce763d43 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -30,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" diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fd91d2e546c0673a8088a05088125064c6712690..7979850899fa1adab0529f01b508c122fba47a82 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,7 +7,7 @@ pub mod proto; mod tests; use anyhow::{anyhow, Result}; -use client::http::HttpClient; +use client::http::{self, HttpClient}; use collections::HashSet; use futures::{ future::{BoxFuture, Shared}, @@ -17,7 +17,6 @@ use gpui::{AppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; use parking_lot::Mutex; -use postage::watch; use serde::Deserialize; use std::{ cell::RefCell, @@ -59,11 +58,22 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } +pub struct LspBinaryVersion { + pub name: String, + pub url: http::Url, +} + pub trait LspExt: 'static + Send + Sync { - fn fetch_latest_language_server( + fn fetch_latest_server_version( &self, http: Arc, + ) -> BoxFuture<'static, Result>; + fn fetch_server_binary( + &self, + version: LspBinaryVersion, + http: Arc, ) -> BoxFuture<'static, Result>; + fn cached_server_binary(&self) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -118,7 +128,7 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, - pub(crate) lsp_ext: Option>, + pub(crate) lsp_ext: Option>, lsp_binary_path: Mutex>>>>>, } @@ -131,19 +141,28 @@ pub struct Grammar { pub(crate) highlight_map: Mutex, } +#[derive(Clone)] +pub enum LanguageServerBinaryStatus { + CheckingForUpdate, + Downloading, + Downloaded, + Cached, + Failed, +} + pub struct LanguageRegistry { languages: Vec>, - pending_lsp_binaries_tx: Arc>>, - pending_lsp_binaries_rx: watch::Receiver, + lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, } impl LanguageRegistry { pub fn new() -> Self { - let (pending_lsp_binaries_tx, pending_lsp_binaries_rx) = watch::channel(); + let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { languages: Default::default(), - pending_lsp_binaries_tx: Arc::new(Mutex::new(pending_lsp_binaries_tx)), - pending_lsp_binaries_rx, + lsp_binary_statuses_tx, + lsp_binary_statuses_rx, } } @@ -211,7 +230,7 @@ impl LanguageRegistry { } } - let lsp_ext = language.lsp_ext.as_ref()?; + let lsp_ext = language.lsp_ext.clone()?; let background = cx.background().clone(); let server_binary_path = { Some( @@ -219,15 +238,13 @@ impl LanguageRegistry { .lsp_binary_path .lock() .get_or_insert_with(|| { - let pending_lsp_binaries_tx = self.pending_lsp_binaries_tx.clone(); - let language_server_path = - lsp_ext.fetch_latest_language_server(http_client); - async move { - *pending_lsp_binaries_tx.lock().borrow_mut() += 1; - let path = language_server_path.map_err(Arc::new).await; - *pending_lsp_binaries_tx.lock().borrow_mut() -= 1; - path - } + get_server_binary_path( + lsp_ext, + language.clone(), + http_client, + self.lsp_binary_statuses_tx.clone(), + ) + .map_err(Arc::new) .boxed() .shared() }) @@ -242,11 +259,68 @@ impl LanguageRegistry { })) } - pub fn pending_lsp_binaries(&self) -> watch::Receiver { - self.pending_lsp_binaries_rx.clone() + pub fn language_server_binary_statuses( + &self, + ) -> async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)> { + self.lsp_binary_statuses_rx.clone() } } +async fn get_server_binary_path( + lsp_ext: Arc, + language: Arc, + http_client: Arc, + statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, +) -> Result { + let path = fetch_latest_server_binary_path( + lsp_ext.clone(), + language.clone(), + http_client, + statuses.clone(), + ) + .await; + if path.is_err() { + if let Some(cached_path) = lsp_ext.cached_server_binary().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, + language: Arc, + http_client: Arc, + lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, +) -> Result { + 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) + .await?; + lsp_binary_statuses_tx + .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) + .await?; + Ok(path) +} + impl Language { pub fn new(config: LanguageConfig, ts_language: Option) -> Self { Self { @@ -306,8 +380,8 @@ impl Language { Ok(self) } - pub fn with_lsp_ext(mut self, processor: impl LspExt) -> Self { - self.lsp_ext = 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 } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index e254a05a931ea255a76b9a3d76b324783b64112b..093f10b1435fa0aa528c4d7cf2add2c0baa34089 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,13 +1,24 @@ use crate::{ItemViewHandle, Settings, StatusItemView}; use futures::StreamExt; -use gpui::{elements::*, Entity, RenderContext, View, ViewContext}; -use language::LanguageRegistry; +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 { - pending_lsp_binaries: usize, settings_rx: watch::Receiver, + checking_for_update: Vec, + downloading: Vec, + failed: Vec, +} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(LspStatus::dismiss_error_message); } impl LspStatus { @@ -16,12 +27,33 @@ impl LspStatus { settings_rx: watch::Receiver, cx: &mut ViewContext, ) -> Self { - let mut pending_lsp_binaries = languages.pending_lsp_binaries(); + let mut status_events = languages.language_server_binary_statuses(); cx.spawn_weak(|this, mut cx| async move { - while let Some(pending_lsp_binaries) = pending_lsp_binaries.next().await { + while let Some((language, event)) = status_events.next().await { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.pending_lsp_binaries = pending_lsp_binaries; + 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 { @@ -31,10 +63,17 @@ impl LspStatus { }) .detach(); Self { - pending_lsp_binaries: 0, settings_rx, + checking_for_update: Default::default(), + downloading: Default::default(), + failed: Default::default(), } } + + fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { + self.failed.clear(); + cx.notify(); + } } impl Entity for LspStatus { @@ -46,16 +85,49 @@ impl View for LspStatus { "LspStatus" } - fn render(&mut self, _: &mut RenderContext) -> ElementBox { - if self.pending_lsp_binaries == 0 { - Empty::new().boxed() - } else { - let theme = &self.settings_rx.borrow().theme; + fn render(&mut self, cx: &mut RenderContext) -> 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( - "Downloading language servers...".to_string(), + 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::(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() } } } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 1203e87309e5ecf66a1afb59f8def33e2855f771..9c8810b7ff161838defd1f15e92ed8297cb137e8 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -9,7 +9,7 @@ use rust_embed::RustEmbed; use serde::Deserialize; use smol::fs::{self, File}; use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; -use util::ResultExt; +use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] #[folder = "languages"] @@ -29,12 +29,13 @@ struct GithubReleaseAsset { browser_download_url: http::Url, } -impl RustLsp { - async fn download( - destination_dir_path: PathBuf, +impl LspExt for RustLsp { + fn fetch_latest_server_version( + &self, http: Arc, - ) -> anyhow::Result { - let release = http + ) -> BoxFuture<'static, Result> { + async move { + let release = http .send( surf::RequestBuilder::new( Method::Get, @@ -51,40 +52,23 @@ impl RustLsp { .body_json::() .await .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let release_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == release_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", release_name))?; - - let destination_path = destination_dir_path.join(format!("rust-analyzer-{}", release.name)); - if fs::metadata(&destination_path).await.is_err() { - let response = http - .send( - surf::RequestBuilder::new(Method::Get, asset.browser_download_url.clone()) - .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, - ::from_mode(0o755), - ) - .await?; + 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(), + }) } - - Ok::<_, anyhow::Error>(destination_path) + .boxed() } -} -impl LspExt for RustLsp { - fn fetch_latest_language_server( + fn fetch_server_binary( &self, + version: LspBinaryVersion, http: Arc, ) -> BoxFuture<'static, Result> { async move { @@ -92,31 +76,59 @@ impl LspExt for RustLsp { .ok_or_else(|| anyhow!("can't determine home directory"))? .join(".zed/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, + ::from_mode(0o755), + ) + .await?; - let downloaded_bin_path = Self::download(destination_dir_path.clone(), http).await; - let mut last_cached_bin_path = None; - 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 let Ok(downloaded_bin_path) = downloaded_bin_path.as_ref() { - if downloaded_bin_path != entry_path.as_path() { + 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(); } } - last_cached_bin_path = Some(entry_path); } } } - if downloaded_bin_path.is_err() { - if let Some(last_cached_bin_path) = last_cached_bin_path { - return Ok(last_cached_bin_path); - } - } + Ok(destination_path) + } + .boxed() + } - downloaded_bin_path + fn cached_server_binary(&self) -> BoxFuture<'static, Option> { + async move { + let destination_dir_path = dirs::home_dir() + .ok_or_else(|| anyhow!("can't determine home directory"))? + .join(".zed/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() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7178ff44eb7e3dae0b0df21b792d7ea6f16a66bd..cdc24e5a52e5d74381d28fd3e10e5d547ea41d12 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -43,6 +43,8 @@ pub fn init(app_state: &Arc, 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), From 03ec6e11b7346236a235b199169fb8c65cf1b71d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Feb 2022 16:23:38 -0800 Subject: [PATCH 14/15] Assign language server download directory on startup Co-Authored-By: Nathan Sobo --- crates/language/src/language.rs | 22 +++++++++++++++++++--- crates/zed/src/language.rs | 24 ++++++++++++++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7979850899fa1adab0529f01b508c122fba47a82..e1316e2dc9fd21cbc5d5b7935c52149caa29ce19 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -72,8 +72,9 @@ pub trait LspExt: 'static + Send + Sync { &self, version: LspBinaryVersion, http: Arc, + download_dir: Arc, ) -> BoxFuture<'static, Result>; - fn cached_server_binary(&self) -> BoxFuture<'static, Option>; + fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option>; fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); fn label_for_completion( &self, @@ -152,6 +153,7 @@ pub enum LanguageServerBinaryStatus { pub struct LanguageRegistry { languages: Vec>, + language_server_download_dir: Option>, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, } @@ -160,6 +162,7 @@ impl LanguageRegistry { pub fn new() -> Self { 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, @@ -176,6 +179,10 @@ impl LanguageRegistry { } } + pub fn set_language_server_download_dir(&mut self, path: impl Into>) { + self.language_server_download_dir = Some(path.into()); + } + pub fn get_language(&self, name: &str) -> Option<&Arc> { self.languages .iter() @@ -230,6 +237,11 @@ impl LanguageRegistry { } } + let download_dir = self + .language_server_download_dir + .clone() + .expect("language server download directory has not been assigned"); + let lsp_ext = language.lsp_ext.clone()?; let background = cx.background().clone(); let server_binary_path = { @@ -242,6 +254,7 @@ impl LanguageRegistry { lsp_ext, language.clone(), http_client, + download_dir, self.lsp_binary_statuses_tx.clone(), ) .map_err(Arc::new) @@ -270,17 +283,19 @@ async fn get_server_binary_path( lsp_ext: Arc, language: Arc, http_client: Arc, + download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { 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().await { + if let Some(cached_path) = lsp_ext.cached_server_binary(download_dir).await { statuses .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) .await?; @@ -298,6 +313,7 @@ async fn fetch_latest_server_binary_path( lsp_ext: Arc, language: Arc, http_client: Arc, + download_dir: Arc, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { lsp_binary_statuses_tx @@ -313,7 +329,7 @@ async fn fetch_latest_server_binary_path( .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; let path = lsp_ext - .fetch_server_binary(version_info, http_client) + .fetch_server_binary(version_info, http_client, download_dir) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 9c8810b7ff161838defd1f15e92ed8297cb137e8..748e82ffe877eeeabda918b8eec7ac1a97f3c41c 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -8,7 +8,13 @@ use regex::Regex; use rust_embed::RustEmbed; use serde::Deserialize; use smol::fs::{self, File}; -use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use std::{ + borrow::Cow, + env::consts, + path::{Path, PathBuf}, + str, + sync::Arc, +}; use util::{ResultExt, TryFutureExt}; #[derive(RustEmbed)] @@ -70,11 +76,10 @@ impl LspExt for RustLsp { &self, version: LspBinaryVersion, http: Arc, + download_dir: Arc, ) -> BoxFuture<'static, Result> { async move { - let destination_dir_path = dirs::home_dir() - .ok_or_else(|| anyhow!("can't determine home directory"))? - .join(".zed/rust-analyzer"); + 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)); @@ -114,11 +119,9 @@ impl LspExt for RustLsp { .boxed() } - fn cached_server_binary(&self) -> BoxFuture<'static, Option> { + fn cached_server_binary(&self, download_dir: Arc) -> BoxFuture<'static, Option> { async move { - let destination_dir_path = dirs::home_dir() - .ok_or_else(|| anyhow!("can't determine home directory"))? - .join(".zed/rust-analyzer"); + let destination_dir_path = download_dir.join("rust-analyzer"); fs::create_dir_all(&destination_dir_path).await?; let mut last = None; @@ -234,6 +237,11 @@ impl LspExt for RustLsp { pub fn build_language_registry() -> LanguageRegistry { 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 From 99594333a574916e9b4161d0823b9ffd6330e42f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Feb 2022 17:44:00 -0700 Subject: [PATCH 15/15] Log an error instead of panicking when there's no LSP download dir --- crates/language/src/language.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e1316e2dc9fd21cbc5d5b7935c52149caa29ce19..9f685befffa99f6f9271639b33148db166a8e802 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -27,6 +27,7 @@ use std::{ }; use theme::SyntaxTheme; use tree_sitter::{self, Query}; +use util::ResultExt; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -240,7 +241,8 @@ impl LanguageRegistry { let download_dir = self .language_server_download_dir .clone() - .expect("language server download directory has not been assigned"); + .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();