Start computing workspace configuration more dynamically

Antonio Scandurra created

Change summary

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(-)

Detailed changes

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<BoxFuture<'static, Value>> {
+        None
+    }
+
     async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
         Default::default()
     }
@@ -541,6 +548,26 @@ impl LanguageRegistry {
         result
     }
 
+    pub fn workspace_configuration(&self, cx: &mut MutableAppContext) -> Task<serde_json::Value> {
+        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<Language>) {
         if let Some(theme) = self.theme.read().clone() {
             language.set_theme(&theme.editor.syntax);

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<usize>,
     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::<Settings, _>(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<LanguageRegistry>,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<()> {
+        let mut languages_changed = languages.subscribe();
+        let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
+        let settings_observation = cx.observe_global::<Settings, _>(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::notification::DidChangeConfiguration>(
+                                        lsp::DidChangeConfigurationParams {
+                                            settings: workspace_config.clone(),
+                                        },
+                                    )
+                                    .ok();
+                            }
+                        }
+                    })
+                } else {
+                    break;
+                }
+            }
+
+            drop(settings_observation);
+        })
+    }
+
     fn detect_language_for_buffer(
         &mut self,
         buffer: &ModelHandle<Buffer>,
@@ -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::<lsp::request::WorkspaceConfiguration, _, _>({
-                                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::notification::DidChangeConfiguration>(
-                        lsp::DidChangeConfigurationParams {
-                            settings: settings.clone(),
-                        },
-                    )
-                    .ok();
-            }
-        }
-        *self.language_server_settings.lock() = settings;
-    }
-
     pub fn language_server_statuses(
         &self,
     ) -> impl DoubleEndedIterator<Item = &LanguageServerStatus> {

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 }

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;
 

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<LanguageRegistry>) {
+pub fn init(languages: Arc<LanguageRegistry>, themes: Arc<ThemeRegistry>) {
     for (name, grammar, lsp_adapter) in [
         (
             "c",
@@ -61,7 +62,10 @@ pub fn init(languages: Arc<LanguageRegistry>) {
         (
             "json",
             tree_sitter_json::language(),
-            Some(Box::new(json::JsonLspAdapter)),
+            Some(Box::new(json::JsonLspAdapter::new(
+                languages.clone(),
+                themes.clone(),
+            ))),
         ),
         (
             "markdown",

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<LanguageRegistry>,
+    themes: Arc<ThemeRegistry>,
+}
+
+impl JsonLspAdapter {
+    pub fn new(languages: Arc<LanguageRegistry>, themes: Arc<ThemeRegistry>) -> 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<BoxFuture<'static, serde_json::Value>> {
+        let action_names = cx.all_action_names().collect::<Vec<_>>();
+        let theme_names = self
+            .themes
+            .list(**cx.default_global::<StaffMode>())
+            .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<String, String> {
         [("JSON".into(), "jsonc".into())].into_iter().collect()
     }
 }
+
+fn schema_file_match(path: &Path) -> &Path {
+    path.strip_prefix(path.parent().unwrap().parent().unwrap())
+        .unwrap()
+}

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());

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::<StaffMode>())
-        .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::<Vec<_>>();
-        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);
         }