From 60d3fb48e26aa42e06857ea4275aa8ce049f2908 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 15 Mar 2023 14:34:48 +0100 Subject: [PATCH 1/8] Start computing workspace configuration more dynamically --- crates/language/src/language.rs | 29 +++++++++- crates/project/src/project.rs | 98 ++++++++++++++++++-------------- crates/util/Cargo.toml | 5 +- crates/util/src/util.rs | 18 ++++++ crates/zed/src/languages.rs | 8 ++- crates/zed/src/languages/json.rs | 66 +++++++++++++++++++-- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 40 ++----------- 8 files changed, 174 insertions(+), 92 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e1e60a1ecfe4cdd960547ccde7d246b8dedcf20f..59fca33fb603369281c7acde19dd8afb7208f0ef 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,7 +44,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; -use util::{ResultExt, TryFutureExt as _, UnwrapFuture}; +use util::{merge_json_value_into, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -208,6 +208,13 @@ pub trait LspAdapter: 'static + Send + Sync { None } + fn workspace_configuration( + &self, + _: &mut MutableAppContext, + ) -> Option> { + None + } + async fn disk_based_diagnostic_sources(&self) -> Vec { Default::default() } @@ -541,6 +548,26 @@ impl LanguageRegistry { result } + pub fn workspace_configuration(&self, cx: &mut MutableAppContext) -> Task { + let mut language_configs = Vec::new(); + for language in self.available_languages.read().iter() { + if let Some(adapter) = language.lsp_adapter.as_ref() { + if let Some(language_config) = adapter.workspace_configuration(cx) { + language_configs.push(language_config); + } + } + } + + cx.background().spawn(async move { + let mut config = serde_json::json!({}); + let language_configs = futures::future::join_all(language_configs).await; + for language_config in language_configs { + merge_json_value_into(language_config, &mut config); + } + config + }) + } + pub fn add(&self, language: Arc) { if let Some(theme) = self.theme.read().clone() { language.set_theme(&theme.editor.syntax); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fb4e6b5b803e41a5fc8e713a2800cd2e82602c90..43c61079bfd79b1e4166fb25d24c0a1cf1f676a4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -64,7 +64,7 @@ use std::{ }; use terminals::Terminals; -use util::{debug_panic, defer, post_inc, ResultExt, TryFutureExt as _}; +use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; pub use worktree::*; @@ -125,6 +125,7 @@ pub struct Project { buffers_being_formatted: HashSet, nonce: u128, _maintain_buffer_languages: Task<()>, + _maintain_workspace_config: Task<()>, terminals: Terminals, } @@ -428,6 +429,7 @@ impl Project { client_subscriptions: Vec::new(), _subscriptions: vec![cx.observe_global::(Self::on_settings_changed)], _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), active_entry: None, languages, client, @@ -486,6 +488,7 @@ impl Project { active_entry: None, collaborators: Default::default(), _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), + _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), languages, user_store: user_store.clone(), fs, @@ -1836,6 +1839,46 @@ impl Project { }) } + fn maintain_workspace_config( + languages: Arc, + cx: &mut ModelContext, + ) -> Task<()> { + let mut languages_changed = languages.subscribe(); + let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); + let settings_observation = cx.observe_global::(move |_, _| { + *settings_changed_tx.borrow_mut() = (); + }); + cx.spawn_weak(|this, mut cx| async move { + loop { + futures::select_biased! { + _ = languages_changed.next().fuse() => {}, + _ = settings_changed_rx.next().fuse() => {} + } + + let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; + if let Some(this) = this.upgrade(&cx) { + this.read_with(&cx, |this, _| { + for server_state in this.language_servers.values() { + if let LanguageServerState::Running { server, .. } = server_state { + server + .notify::( + lsp::DidChangeConfigurationParams { + settings: workspace_config.clone(), + }, + ) + .ok(); + } + } + }) + } else { + break; + } + } + + drop(settings_observation); + }) + } + fn detect_language_for_buffer( &mut self, buffer: &ModelHandle, @@ -1875,24 +1918,6 @@ impl Project { } } - fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { - use serde_json::Value; - - match (source, target) { - (Value::Object(source), Value::Object(target)) => { - for (key, value) in source { - if let Some(target) = target.get_mut(&key) { - Self::merge_json_value_into(value, target); - } else { - target.insert(key.clone(), value); - } - } - } - - (source, target) => *target = source, - } - } - fn start_language_server( &mut self, worktree_id: WorktreeId, @@ -1920,17 +1945,16 @@ impl Project { let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { - Self::merge_json_value_into(override_options, initialization_options); + merge_json_value_into(override_options, initialization_options); } - (None, override_options) => initialization_options = override_options, - _ => {} } self.language_server_ids .entry(key.clone()) .or_insert_with(|| { + let languages = self.languages.clone(); let server_id = post_inc(&mut self.next_language_server_id); let language_server = self.languages.start_language_server( server_id, @@ -1977,23 +2001,24 @@ impl Project { language_server .on_request::({ - let settings = this.read_with(&cx, |this, _| { - this.language_server_settings.clone() - }); - move |params, _| { - let settings = settings.lock().clone(); + move |params, mut cx| { + let languages = languages.clone(); async move { + let workspace_config = cx + .update(|cx| languages.workspace_configuration(cx)) + .await; + Ok(params .items .into_iter() .map(|item| { if let Some(section) = &item.section { - settings + workspace_config .get(section) .cloned() .unwrap_or(serde_json::Value::Null) } else { - settings.clone() + workspace_config.clone() } }) .collect()) @@ -2539,21 +2564,6 @@ impl Project { } } - pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for server_state in self.language_servers.values() { - if let LanguageServerState::Running { server, .. } = server_state { - server - .notify::( - lsp::DidChangeConfigurationParams { - settings: settings.clone(), - }, - ) - .ok(); - } - } - *self.language_server_settings.lock() = settings; - } - pub fn language_server_statuses( &self, ) -> impl DoubleEndedIterator { diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index e8c158b637010d1faf3fbe2c0578f0f1fa82a0e7..b13b8af956c0f174c1897a920d47555bde119543 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -9,7 +9,7 @@ path = "src/util.rs" doctest = false [features] -test-support = ["serde_json", "tempdir", "git2"] +test-support = ["tempdir", "git2"] [dependencies] anyhow = "1.0.38" @@ -19,11 +19,10 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } lazy_static = "1.4.0" rand = { workspace = true } tempdir = { version = "0.3.7", optional = true } -serde_json = { version = "1.0", features = ["preserve_order"], optional = true } +serde_json = { version = "1.0", features = ["preserve_order"] } git2 = { version = "0.15", default-features = false, optional = true } dirs = "3.0" [dev-dependencies] tempdir = { version = "0.3.7" } -serde_json = { version = "1.0", features = ["preserve_order"] } git2 = { version = "0.15", default-features = false } diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3824312a4fa9ec0d9993063c855b37bc55c3121c..6a5ccb8bd52e95b4aa54aa88c5b07d0f71b1906f 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -83,6 +83,24 @@ where } } +pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { + use serde_json::Value; + + match (source, target) { + (Value::Object(source), Value::Object(target)) => { + for (key, value) in source { + if let Some(target) = target.get_mut(&key) { + merge_json_value_into(value, target); + } else { + target.insert(key.clone(), value); + } + } + } + + (source, target) => *target = source, + } +} + pub trait ResultExt { type Ok; diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index a99c80c00121534347eb35daebf5fb22414490df..5d5ffc4f950e637206ad7758a6bcc909f49f0810 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -2,6 +2,7 @@ use anyhow::Context; pub use language::*; use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; +use theme::ThemeRegistry; mod c; mod elixir; @@ -31,7 +32,7 @@ mod yaml; #[exclude = "*.rs"] struct LanguageDir; -pub fn init(languages: Arc) { +pub fn init(languages: Arc, themes: Arc) { for (name, grammar, lsp_adapter) in [ ( "c", @@ -61,7 +62,10 @@ pub fn init(languages: Arc) { ( "json", tree_sitter_json::language(), - Some(Box::new(json::JsonLspAdapter)), + Some(Box::new(json::JsonLspAdapter::new( + languages.clone(), + themes.clone(), + ))), ), ( "markdown", diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index d7f87bee6c0d64cf936eedac874581e33690e25d..d5c59d28d119b35ac6b23bbed9c384f303d04e08 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -4,14 +4,32 @@ use async_compression::futures::bufread::GzipDecoder; use async_trait::async_trait; use client::http::HttpClient; use collections::HashMap; -use futures::{io::BufReader, StreamExt}; -use language::{LanguageServerName, LspAdapter}; +use futures::{future::BoxFuture, io::BufReader, FutureExt, StreamExt}; +use gpui::MutableAppContext; +use language::{LanguageRegistry, LanguageServerName, LspAdapter}; use serde_json::json; +use settings::{keymap_file_json_schema, settings_file_json_schema}; use smol::fs::{self, File}; -use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; -use util::ResultExt; +use std::{ + any::Any, + env::consts, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use theme::ThemeRegistry; +use util::{paths, ResultExt, StaffMode}; -pub struct JsonLspAdapter; +pub struct JsonLspAdapter { + languages: Arc, + themes: Arc, +} + +impl JsonLspAdapter { + pub fn new(languages: Arc, themes: Arc) -> Self { + Self { languages, themes } + } +} #[async_trait] impl LspAdapter for JsonLspAdapter { @@ -102,7 +120,45 @@ impl LspAdapter for JsonLspAdapter { })) } + fn workspace_configuration( + &self, + cx: &mut MutableAppContext, + ) -> Option> { + let action_names = cx.all_action_names().collect::>(); + let theme_names = self + .themes + .list(**cx.default_global::()) + .map(|meta| meta.name) + .collect(); + let language_names = self.languages.language_names(); + Some( + future::ready(serde_json::json!({ + "json": { + "format": { + "enable": true, + }, + "schemas": [ + { + "fileMatch": [schema_file_match(&paths::SETTINGS)], + "schema": settings_file_json_schema(theme_names, &language_names), + }, + { + "fileMatch": [schema_file_match(&paths::KEYMAP)], + "schema": keymap_file_json_schema(&action_names), + } + ] + } + })) + .boxed(), + ) + } + async fn language_ids(&self) -> HashMap { [("JSON".into(), "jsonc".into())].into_iter().collect() } } + +fn schema_file_match(path: &Path) -> &Path { + path.strip_prefix(path.parent().unwrap().parent().unwrap()) + .unwrap() +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8cf6879f7abcd5163463d0d6dc9ca2baadc7324d..6b511554980cef30893e9965be2944e09e349287 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -139,7 +139,7 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - languages::init(languages.clone()); + languages::init(languages.clone(), themes.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); cx.set_global(client.clone()); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 83a8038d77249999ec8e39ba6ba50f97f95ef629..b0239d234c8a30476ee915bbec4f5fec04e3faa1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -29,10 +29,10 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; +use settings::Settings; use std::{borrow::Cow, env, path::Path, str, sync::Arc}; use terminal_view::terminal_button::{self, TerminalButton}; -use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode}; +use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; pub use workspace; use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace}; @@ -296,34 +296,6 @@ pub fn initialize_workspace( cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone())); - let theme_names = app_state - .themes - .list(**cx.default_global::()) - .map(|meta| meta.name) - .collect(); - let language_names = app_state.languages.language_names(); - - workspace.project().update(cx, |project, cx| { - let action_names = cx.all_action_names().collect::>(); - project.set_language_server_settings(serde_json::json!({ - "json": { - "format": { - "enable": true, - }, - "schemas": [ - { - "fileMatch": [schema_file_match(&paths::SETTINGS)], - "schema": settings_file_json_schema(theme_names, &language_names), - }, - { - "fileMatch": [schema_file_match(&paths::KEYMAP)], - "schema": keymap_file_json_schema(&action_names), - } - ] - } - })); - }); - let collab_titlebar_item = cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx)); workspace.set_titlebar_item(collab_titlebar_item, cx); @@ -676,11 +648,6 @@ fn open_bundled_file( .detach(); } -fn schema_file_match(path: &Path) -> &Path { - path.strip_prefix(path.parent().unwrap().parent().unwrap()) - .unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -1882,7 +1849,8 @@ mod tests { let mut languages = LanguageRegistry::new(Task::ready(())); languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); - languages::init(languages.clone()); + let themes = ThemeRegistry::new((), cx.font_cache().clone()); + languages::init(languages.clone(), themes); for name in languages.language_names() { languages.language_for_name(&name); } From e30ea43a14b2fa26145bf724073406585e6a9fc8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 09:29:43 +0100 Subject: [PATCH 2/8] Include loaded languages when computing lsp workspace configuration --- crates/language/src/language.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 59fca33fb603369281c7acde19dd8afb7208f0ef..cb4f5c7b87eb9be49f873857da5f5a22ed2293d6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -133,6 +133,13 @@ impl CachedLspAdapter { self.adapter.cached_server_binary(container_dir).await } + pub fn workspace_configuration( + &self, + cx: &mut MutableAppContext, + ) -> Option> { + self.adapter.workspace_configuration(cx) + } + pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { self.adapter.process_diagnostics(params).await } @@ -557,6 +564,13 @@ impl LanguageRegistry { } } } + for language in self.languages.read().iter() { + if let Some(adapter) = language.lsp_adapter() { + if let Some(language_config) = adapter.workspace_configuration(cx) { + language_configs.push(language_config); + } + } + } cx.background().spawn(async move { let mut config = serde_json::json!({}); From a8ac08f5bd05827665d9f3c9d2cb197c6c23c201 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 09:54:04 +0100 Subject: [PATCH 3/8] Coalesce multiple `RwLock`s into one `LanguageRegistryState` struct --- crates/language/src/language.rs | 95 ++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index cb4f5c7b87eb9be49f873857da5f5a22ed2293d6..47b8e23374c7f7968cb32c8d31c8ffc3abe72213 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -475,8 +475,7 @@ struct AvailableLanguage { } pub struct LanguageRegistry { - languages: RwLock>>, - available_languages: RwLock>, + state: RwLock, language_server_download_dir: Option>, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, @@ -488,26 +487,33 @@ pub struct LanguageRegistry { Shared>>>, >, >, - subscription: RwLock<(watch::Sender<()>, watch::Receiver<()>)>, - theme: RwLock>>, executor: Option>, - version: AtomicUsize, +} + +struct LanguageRegistryState { + languages: Vec>, + available_languages: Vec, + subscription: (watch::Sender<()>, watch::Receiver<()>), + theme: Option>, + version: usize, } impl LanguageRegistry { pub fn new(login_shell_env_loaded: Task<()>) -> Self { let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16); Self { + state: RwLock::new(LanguageRegistryState { + languages: vec![PLAIN_TEXT.clone()], + available_languages: Default::default(), + subscription: watch::channel(), + theme: Default::default(), + version: 0, + }), language_server_download_dir: None, - languages: RwLock::new(vec![PLAIN_TEXT.clone()]), - available_languages: Default::default(), lsp_binary_statuses_tx, lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), lsp_binary_paths: Default::default(), - subscription: RwLock::new(watch::channel()), - theme: Default::default(), - version: Default::default(), executor: None, } } @@ -529,42 +535,41 @@ impl LanguageRegistry { lsp_adapter: Option>, get_queries: fn(&str) -> LanguageQueries, ) { - self.available_languages.write().push(AvailableLanguage { - path, - config, - grammar, - lsp_adapter, - get_queries, - }); + self.state + .write() + .available_languages + .push(AvailableLanguage { + path, + config, + grammar, + lsp_adapter, + get_queries, + }); } pub fn language_names(&self) -> Vec { - let mut result = self + let state = self.state.read(); + let mut result = state .available_languages - .read() .iter() .map(|l| l.config.name.to_string()) - .chain( - self.languages - .read() - .iter() - .map(|l| l.config.name.to_string()), - ) + .chain(state.languages.iter().map(|l| l.config.name.to_string())) .collect::>(); result.sort_unstable_by_key(|language_name| language_name.to_lowercase()); result } pub fn workspace_configuration(&self, cx: &mut MutableAppContext) -> Task { + let state = self.state.read(); let mut language_configs = Vec::new(); - for language in self.available_languages.read().iter() { + for language in &state.available_languages { if let Some(adapter) = language.lsp_adapter.as_ref() { if let Some(language_config) = adapter.workspace_configuration(cx) { language_configs.push(language_config); } } } - for language in self.languages.read().iter() { + for language in &state.languages { if let Some(adapter) = language.lsp_adapter() { if let Some(language_config) = adapter.workspace_configuration(cx) { language_configs.push(language_config); @@ -583,25 +588,27 @@ impl LanguageRegistry { } pub fn add(&self, language: Arc) { - if let Some(theme) = self.theme.read().clone() { + let mut state = self.state.write(); + if let Some(theme) = state.theme.as_ref() { language.set_theme(&theme.editor.syntax); } - self.languages.write().push(language); - self.version.fetch_add(1, SeqCst); - *self.subscription.write().0.borrow_mut() = (); + state.languages.push(language); + state.version += 1; + *state.subscription.0.borrow_mut() = (); } pub fn subscribe(&self) -> watch::Receiver<()> { - self.subscription.read().1.clone() + self.state.read().subscription.1.clone() } pub fn version(&self) -> usize { - self.version.load(SeqCst) + self.state.read().version } pub fn set_theme(&self, theme: Arc) { - *self.theme.write() = Some(theme.clone()); - for language in self.languages.read().iter() { + let mut state = self.state.write(); + state.theme = Some(theme.clone()); + for language in &state.languages { language.set_theme(&theme.editor.syntax); } } @@ -654,19 +661,21 @@ impl LanguageRegistry { ) -> UnwrapFuture>>> { let (tx, rx) = oneshot::channel(); - if let Some(language) = self + let mut state = self.state.write(); + if let Some(language) = state .languages - .read() .iter() .find(|language| callback(&language.config)) { let _ = tx.send(Ok(language.clone())); } else if let Some(executor) = self.executor.clone() { - let mut available_languages = self.available_languages.write(); - - if let Some(ix) = available_languages.iter().position(|l| callback(&l.config)) { - let language = available_languages.remove(ix); - drop(available_languages); + if let Some(ix) = state + .available_languages + .iter() + .position(|l| callback(&l.config)) + { + let language = state.available_languages.remove(ix); + drop(state); let name = language.config.name.clone(); let this = self.clone(); executor @@ -702,7 +711,7 @@ impl LanguageRegistry { } pub fn to_vec(&self) -> Vec> { - self.languages.read().iter().cloned().collect() + self.state.read().languages.iter().cloned().collect() } pub fn start_language_server( From 4d52fc0d12dff4cc1ad171a6741a17a5aa849677 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 10:28:07 +0100 Subject: [PATCH 4/8] Remove available language only when it has loaded This also ensures that, if you load the same language more than once, a future that resolves to the language (or an error) is returned at all times. Previously, we would only return it the first time the language was loaded. --- crates/language/src/language.rs | 147 +++++++++++++++++++------------ crates/zed/src/languages.rs | 32 +++---- crates/zed/src/languages/go.rs | 2 +- crates/zed/src/languages/rust.rs | 4 +- 4 files changed, 111 insertions(+), 74 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 47b8e23374c7f7968cb32c8d31c8ffc3abe72213..0c05fdccb1739b4b16e81f6244c7e15f5fa72405 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -44,7 +44,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; -use util::{merge_json_value_into, ResultExt, TryFutureExt as _, UnwrapFuture}; +use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] use futures::channel::mpsc; @@ -87,11 +87,11 @@ pub struct CachedLspAdapter { pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, - pub adapter: Box, + pub adapter: Arc, } impl CachedLspAdapter { - pub async fn new(adapter: Box) -> Arc { + pub async fn new(adapter: Arc) -> Arc { let name = adapter.name().await; let server_args = adapter.server_args().await; let initialization_options = adapter.initialization_options().await; @@ -242,7 +242,7 @@ pub struct CodeLabel { pub filter_range: Range, } -#[derive(Deserialize)] +#[derive(Clone, Deserialize)] pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, @@ -279,7 +279,7 @@ pub struct LanguageScope { override_id: Option, } -#[derive(Deserialize, Default, Debug)] +#[derive(Clone, Deserialize, Default, Debug)] pub struct LanguageConfigOverride { #[serde(default)] pub line_comment: Override>, @@ -289,7 +289,7 @@ pub struct LanguageConfigOverride { pub disabled_bracket_ixs: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Clone, Deserialize, Debug)] #[serde(untagged)] pub enum Override { Remove { remove: bool }, @@ -466,11 +466,15 @@ pub enum LanguageServerBinaryStatus { Failed { error: String }, } +type AvailableLanguageId = usize; + +#[derive(Clone)] struct AvailableLanguage { + id: AvailableLanguageId, path: &'static str, config: LanguageConfig, grammar: tree_sitter::Language, - lsp_adapter: Option>, + lsp_adapter: Option>, get_queries: fn(&str) -> LanguageQueries, } @@ -493,6 +497,8 @@ pub struct LanguageRegistry { struct LanguageRegistryState { languages: Vec>, available_languages: Vec, + next_available_language_id: AvailableLanguageId, + loading_languages: HashMap>>>>, subscription: (watch::Sender<()>, watch::Receiver<()>), theme: Option>, version: usize, @@ -505,6 +511,8 @@ impl LanguageRegistry { state: RwLock::new(LanguageRegistryState { languages: vec![PLAIN_TEXT.clone()], available_languages: Default::default(), + next_available_language_id: 0, + loading_languages: Default::default(), subscription: watch::channel(), theme: Default::default(), version: 0, @@ -532,19 +540,18 @@ impl LanguageRegistry { path: &'static str, config: LanguageConfig, grammar: tree_sitter::Language, - lsp_adapter: Option>, + lsp_adapter: Option>, get_queries: fn(&str) -> LanguageQueries, ) { - self.state - .write() - .available_languages - .push(AvailableLanguage { - path, - config, - grammar, - lsp_adapter, - get_queries, - }); + let state = &mut *self.state.write(); + state.available_languages.push(AvailableLanguage { + id: post_inc(&mut state.next_available_language_id), + path, + config, + grammar, + lsp_adapter, + get_queries, + }); } pub fn language_names(&self) -> Vec { @@ -588,13 +595,7 @@ impl LanguageRegistry { } pub fn add(&self, language: Arc) { - let mut state = self.state.write(); - if let Some(theme) = state.theme.as_ref() { - language.set_theme(&theme.editor.syntax); - } - state.languages.push(language); - state.version += 1; - *state.subscription.0.borrow_mut() = (); + self.state.write().add(language); } pub fn subscribe(&self) -> watch::Receiver<()> { @@ -669,37 +670,62 @@ impl LanguageRegistry { { let _ = tx.send(Ok(language.clone())); } else if let Some(executor) = self.executor.clone() { - if let Some(ix) = state + if let Some(language) = state .available_languages .iter() - .position(|l| callback(&l.config)) + .find(|l| callback(&l.config)) + .cloned() { - let language = state.available_languages.remove(ix); - drop(state); - let name = language.config.name.clone(); - let this = self.clone(); - executor - .spawn(async move { - let queries = (language.get_queries)(&language.path); - let language = Language::new(language.config, Some(language.grammar)) - .with_lsp_adapter(language.lsp_adapter) - .await; - match language.with_queries(queries) { - Ok(language) => { - let language = Arc::new(language); - this.add(language.clone()); - let _ = tx.send(Ok(language)); - } - Err(err) => { - let _ = tx.send(Err(anyhow!( - "failed to load language {}: {}", - name, - err - ))); - } - }; - }) - .detach(); + let txs = state + .loading_languages + .entry(language.id) + .or_insert_with(|| { + let this = self.clone(); + executor + .spawn(async move { + let id = language.id; + let queries = (language.get_queries)(&language.path); + let language = + Language::new(language.config, Some(language.grammar)) + .with_lsp_adapter(language.lsp_adapter) + .await; + let name = language.name(); + match language.with_queries(queries) { + Ok(language) => { + let language = Arc::new(language); + let mut state = this.state.write(); + state.add(language.clone()); + state + .available_languages + .retain(|language| language.id != id); + if let Some(mut txs) = state.loading_languages.remove(&id) { + for tx in txs.drain(..) { + let _ = tx.send(Ok(language.clone())); + } + } + } + Err(err) => { + let mut state = this.state.write(); + state + .available_languages + .retain(|language| language.id != id); + if let Some(mut txs) = state.loading_languages.remove(&id) { + for tx in txs.drain(..) { + let _ = tx.send(Err(anyhow!( + "failed to load language {}: {}", + name, + err + ))); + } + } + } + }; + }) + .detach(); + + Vec::new() + }); + txs.push(tx); } else { let _ = tx.send(Err(anyhow!("language not found"))); } @@ -804,6 +830,17 @@ impl LanguageRegistry { } } +impl LanguageRegistryState { + fn add(&mut self, language: Arc) { + if let Some(theme) = self.theme.as_ref() { + language.set_theme(&theme.editor.syntax); + } + self.languages.push(language); + self.version += 1; + *self.subscription.0.borrow_mut() = (); + } +} + #[cfg(any(test, feature = "test-support"))] impl Default for LanguageRegistry { fn default() -> Self { @@ -1135,7 +1172,7 @@ impl Language { Arc::get_mut(self.grammar.as_mut().unwrap()).unwrap() } - pub async fn with_lsp_adapter(mut self, lsp_adapter: Option>) -> Self { + pub async fn with_lsp_adapter(mut self, lsp_adapter: Option>) -> Self { if let Some(adapter) = lsp_adapter { self.adapter = Some(CachedLspAdapter::new(adapter).await); } @@ -1149,7 +1186,7 @@ impl Language { ) -> mpsc::UnboundedReceiver { let (servers_tx, servers_rx) = mpsc::unbounded(); self.fake_adapter = Some((servers_tx, fake_lsp_adapter.clone())); - let adapter = CachedLspAdapter::new(Box::new(fake_lsp_adapter)).await; + let adapter = CachedLspAdapter::new(Arc::new(fake_lsp_adapter)).await; self.adapter = Some(adapter); servers_rx } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 5d5ffc4f950e637206ad7758a6bcc909f49f0810..1acee4bad4d09814a656bd21e6536278bd62889a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -37,12 +37,12 @@ pub fn init(languages: Arc, themes: Arc) { ( "c", tree_sitter_c::language(), - Some(Box::new(c::CLspAdapter) as Box), + Some(Arc::new(c::CLspAdapter) as Arc), ), ( "cpp", tree_sitter_cpp::language(), - Some(Box::new(c::CLspAdapter)), + Some(Arc::new(c::CLspAdapter)), ), ( "css", @@ -52,17 +52,17 @@ pub fn init(languages: Arc, themes: Arc) { ( "elixir", tree_sitter_elixir::language(), - Some(Box::new(elixir::ElixirLspAdapter)), + Some(Arc::new(elixir::ElixirLspAdapter)), ), ( "go", tree_sitter_go::language(), - Some(Box::new(go::GoLspAdapter)), + Some(Arc::new(go::GoLspAdapter)), ), ( "json", tree_sitter_json::language(), - Some(Box::new(json::JsonLspAdapter::new( + Some(Arc::new(json::JsonLspAdapter::new( languages.clone(), themes.clone(), ))), @@ -75,12 +75,12 @@ pub fn init(languages: Arc, themes: Arc) { ( "python", tree_sitter_python::language(), - Some(Box::new(python::PythonLspAdapter)), + Some(Arc::new(python::PythonLspAdapter)), ), ( "rust", tree_sitter_rust::language(), - Some(Box::new(rust::RustLspAdapter)), + Some(Arc::new(rust::RustLspAdapter)), ), ( "toml", @@ -90,32 +90,32 @@ pub fn init(languages: Arc, themes: Arc) { ( "tsx", tree_sitter_typescript::language_tsx(), - Some(Box::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter)), ), ( "typescript", tree_sitter_typescript::language_typescript(), - Some(Box::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter)), ), ( "javascript", tree_sitter_typescript::language_tsx(), - Some(Box::new(typescript::TypeScriptLspAdapter)), + Some(Arc::new(typescript::TypeScriptLspAdapter)), ), ( "html", tree_sitter_html::language(), - Some(Box::new(html::HtmlLspAdapter)), + Some(Arc::new(html::HtmlLspAdapter)), ), ( "ruby", tree_sitter_ruby::language(), - Some(Box::new(ruby::RubyLanguageServer)), + Some(Arc::new(ruby::RubyLanguageServer)), ), ( "erb", tree_sitter_embedded_template::language(), - Some(Box::new(ruby::RubyLanguageServer)), + Some(Arc::new(ruby::RubyLanguageServer)), ), ( "scheme", @@ -130,12 +130,12 @@ pub fn init(languages: Arc, themes: Arc) { ( "lua", tree_sitter_lua::language(), - Some(Box::new(lua::LuaLspAdapter)), + Some(Arc::new(lua::LuaLspAdapter)), ), ( "yaml", tree_sitter_yaml::language(), - Some(Box::new(yaml::YamlLspAdapter)), + Some(Arc::new(yaml::YamlLspAdapter)), ), ] { languages.register(name, load_config(name), grammar, lsp_adapter, load_queries); @@ -146,7 +146,7 @@ pub fn init(languages: Arc, themes: Arc) { pub async fn language( name: &str, grammar: tree_sitter::Language, - lsp_adapter: Option>, + lsp_adapter: Option>, ) -> Arc { Arc::new( Language::new(load_config(name), Some(grammar)) diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index dc84599e4eff350f7f27e17d15fb4d79692acd88..dd819338d0bf9a3a36566c97b72dd248a90a8e6d 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -314,7 +314,7 @@ mod tests { let language = language( "go", tree_sitter_go::language(), - Some(Box::new(GoLspAdapter)), + Some(Arc::new(GoLspAdapter)), ) .await; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 40948d5005d75a8f04441cd5aef62829f0568a10..a8f7fcbc4dcf34ff3e017be53cfd967a0e185dfe 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -306,7 +306,7 @@ mod tests { let language = language( "rust", tree_sitter_rust::language(), - Some(Box::new(RustLspAdapter)), + Some(Arc::new(RustLspAdapter)), ) .await; let grammar = language.grammar().unwrap(); @@ -392,7 +392,7 @@ mod tests { let language = language( "rust", tree_sitter_rust::language(), - Some(Box::new(RustLspAdapter)), + Some(Arc::new(RustLspAdapter)), ) .await; let grammar = language.grammar().unwrap(); From 8a685fa52a73c16ff27db33c565a263f0de87668 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 10:30:22 +0100 Subject: [PATCH 5/8] Use `LanguageRegistry::workspace_configuration` everywhere --- crates/project/src/project.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 43c61079bfd79b1e4166fb25d24c0a1cf1f676a4..20422a8b5936f59fa65009cff9a5ebbf28005acd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -37,7 +37,6 @@ use lsp::{ MarkedString, }; use lsp_command::*; -use parking_lot::Mutex; use postage::watch; use rand::prelude::*; use search::SearchQuery; @@ -95,7 +94,6 @@ pub struct Project { language_servers: HashMap, language_server_ids: HashMap<(WorktreeId, LanguageServerName), usize>, language_server_statuses: BTreeMap, - language_server_settings: Arc>, last_workspace_edits_by_language_server: HashMap, next_language_server_id: usize, client: Arc, @@ -441,7 +439,6 @@ impl Project { language_server_ids: Default::default(), language_server_statuses: Default::default(), last_workspace_edits_by_language_server: Default::default(), - language_server_settings: Default::default(), buffers_being_formatted: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), @@ -504,7 +501,6 @@ impl Project { }), language_servers: Default::default(), language_server_ids: Default::default(), - language_server_settings: Default::default(), language_server_statuses: response .language_servers .into_iter() @@ -1843,18 +1839,14 @@ impl Project { languages: Arc, cx: &mut ModelContext, ) -> Task<()> { - let mut languages_changed = languages.subscribe(); let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); + let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); + let settings_observation = cx.observe_global::(move |_, _| { *settings_changed_tx.borrow_mut() = (); }); cx.spawn_weak(|this, mut cx| async move { - loop { - futures::select_biased! { - _ = languages_changed.next().fuse() => {}, - _ = settings_changed_rx.next().fuse() => {} - } - + while let Some(_) = settings_changed_rx.next().await { let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; if let Some(this) = this.upgrade(&cx) { this.read_with(&cx, |this, _| { @@ -2001,13 +1993,13 @@ impl Project { language_server .on_request::({ + let languages = languages.clone(); move |params, mut cx| { let languages = languages.clone(); async move { let workspace_config = cx .update(|cx| languages.workspace_configuration(cx)) .await; - Ok(params .items .into_iter() @@ -2096,6 +2088,16 @@ impl Project { }) .detach(); + let workspace_config = + cx.update(|cx| languages.workspace_configuration(cx)).await; + language_server + .notify::( + lsp::DidChangeConfigurationParams { + settings: workspace_config, + }, + ) + .ok(); + this.update(&mut cx, |this, cx| { // If the language server for this key doesn't match the server id, don't store the // server. Which will cause it to be dropped, killing the process @@ -2128,13 +2130,6 @@ impl Project { progress_tokens: Default::default(), }, ); - language_server - .notify::( - lsp::DidChangeConfigurationParams { - settings: this.language_server_settings.lock().clone(), - }, - ) - .ok(); if let Some(project_id) = this.remote_id() { this.client From 88e664bfd9dfa16efca6a5df142891855667679a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 10:40:04 +0100 Subject: [PATCH 6/8] Add test for language registration and loading --- crates/language/src/language.rs | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 0c05fdccb1739b4b16e81f6244c7e15f5fa72405..84311f9d790bcc401bbe4265bec4ff06587dd1b4 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1502,3 +1502,76 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { } start..end } + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + + use super::*; + + #[gpui::test(iterations = 10)] + async fn test_language_loading(cx: &mut TestAppContext) { + let mut languages = LanguageRegistry::new(Task::ready(())); + languages.set_executor(cx.background()); + let languages = Arc::new(languages); + languages.register( + "/JSON", + LanguageConfig { + name: "JSON".into(), + path_suffixes: vec!["json".into()], + ..Default::default() + }, + tree_sitter_json::language(), + None, + |_| Default::default(), + ); + languages.register( + "/rust", + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".into()], + ..Default::default() + }, + tree_sitter_rust::language(), + None, + |_| Default::default(), + ); + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); + + let rust1 = languages.language_for_name("Rust"); + let rust2 = languages.language_for_name("Rust"); + + // Ensure language is still listed even if it's being loaded. + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); + + let (rust1, rust2) = futures::join!(rust1, rust2); + assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap())); + + // Ensure language is still listed even after loading it. + assert_eq!( + languages.language_names(), + &[ + "JSON".to_string(), + "Plain Text".to_string(), + "Rust".to_string(), + ] + ); + + // Loading an unknown language returns an error. + assert!(languages.language_for_name("Unknown").await.is_err()); + } +} From f5a4c6a7c1afb2696d101b0118a548b9cb7b78f3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 10:45:46 +0100 Subject: [PATCH 7/8] Provide `editor.tabSize` in workspace configuration for YAML This fixes a bug that caused the hover popover to display lots of ` ` occurrences. --- crates/zed/src/languages/yaml.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 46569111f1ddc82743c4ec379c3e3424b7d6b326..9750ecce88d6f5ea73824bb801d9ac73764d69d9 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -1,12 +1,13 @@ -use std::{any::Any, path::PathBuf, sync::Arc}; - use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::http::HttpClient; -use futures::StreamExt; -use smol::fs; - +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use gpui::MutableAppContext; use language::{LanguageServerName, LspAdapter}; +use serde_json::Value; +use settings::Settings; +use smol::fs; +use std::{any::Any, future, path::PathBuf, sync::Arc}; use util::ResultExt; use super::installation::{npm_install_packages, npm_package_latest_version}; @@ -90,4 +91,19 @@ impl LspAdapter for YamlLspAdapter { .await .log_err() } + + fn workspace_configuration( + &self, + cx: &mut MutableAppContext, + ) -> Option> { + let settings = cx.global::(); + Some( + future::ready(serde_json::json!({ + "[yaml]": { + "editor.tabSize": settings.tab_size(Some("YAML")) + } + })) + .boxed(), + ) + } } From 9498f02f2c45453f3cb764ab8e9c5b85f4b44bf6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Mar 2023 15:01:31 +0100 Subject: [PATCH 8/8] Retrieve workspace configuration before initializing language server --- crates/project/src/project.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 20422a8b5936f59fa65009cff9a5ebbf28005acd..5b2fd412e8a272651c54722adc518a791c6cafc5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1958,6 +1958,8 @@ impl Project { self.language_servers.insert( server_id, LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { + let workspace_config = + cx.update(|cx| languages.workspace_configuration(cx)).await; let language_server = language_server?.await.log_err()?; let language_server = language_server .initialize(initialization_options) @@ -2088,8 +2090,6 @@ impl Project { }) .detach(); - let workspace_config = - cx.update(|cx| languages.workspace_configuration(cx)).await; language_server .notify::( lsp::DidChangeConfigurationParams {