diff --git a/Cargo.lock b/Cargo.lock index 8a53a38f08..b872085661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4645,7 +4645,6 @@ dependencies = [ "collections", "db", "editor", - "extension", "extension_host", "feature_flags", "fs", @@ -10308,6 +10307,7 @@ dependencies = [ "clock", "collections", "env_logger 0.11.6", + "extension", "fancy-regex 0.14.0", "fs", "futures 0.3.31", diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs index 17a6545cf1..831010177d 100644 --- a/crates/extension/src/extension_events.rs +++ b/crates/extension/src/extension_events.rs @@ -1,4 +1,4 @@ -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _}; +use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global}; pub fn init(cx: &mut App) { let extension_events = cx.new(ExtensionEvents::new); @@ -14,8 +14,10 @@ pub struct ExtensionEvents; impl ExtensionEvents { /// Returns the global [`ExtensionEvents`]. - pub fn global(cx: &App) -> Entity { - GlobalExtensionEvents::global(cx).0.clone() + pub fn try_global(cx: &App) -> Option> { + return cx + .try_global::() + .map(|g| g.0.clone()); } fn new(_cx: &mut Context) -> Self { @@ -29,7 +31,7 @@ impl ExtensionEvents { #[derive(Clone)] pub enum Event { - ExtensionsUpdated, + ExtensionsInstalledChanged, } impl EventEmitter for ExtensionEvents {} diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index a9ad8dc9e9..a317360614 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -127,6 +127,7 @@ pub enum ExtensionOperation { #[derive(Clone)] pub enum Event { + ExtensionsUpdated, StartedReloading, ExtensionInstalled(Arc), ExtensionFailedToLoad(Arc), @@ -1213,9 +1214,7 @@ impl ExtensionStore { self.extension_index = new_index; cx.notify(); - ExtensionEvents::global(cx).update(cx, |this, cx| { - this.emit(extension::Event::ExtensionsUpdated, cx) - }); + cx.emit(Event::ExtensionsUpdated); cx.spawn(|this, mut cx| async move { cx.background_spawn({ @@ -1317,6 +1316,12 @@ impl ExtensionStore { this.proxy.set_extensions_loaded(); this.proxy.reload_current_theme(cx); this.proxy.reload_current_icon_theme(cx); + + if let Some(events) = ExtensionEvents::try_global(cx) { + events.update(cx, |this, cx| { + this.emit(extension::Event::ExtensionsInstalledChanged, cx) + }); + } }) .ok(); }) diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 7a67d98a8c..afdb3bf0a3 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -17,7 +17,6 @@ client.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -extension.workspace = true extension_host.workspace = true feature_flags.workspace = true fs.workspace = true diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 0b79d654b0..4c0fbf8290 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -9,7 +9,6 @@ use std::{ops::Range, sync::Arc}; use client::{ExtensionMetadata, ExtensionProvides}; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; -use extension::ExtensionEvents; use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use feature_flags::FeatureFlagAppExt as _; use fuzzy::{match_strings, StringMatchCandidate}; @@ -213,7 +212,7 @@ pub struct ExtensionsPage { query_editor: Entity, query_contains_error: bool, provides_filter: Option, - _subscriptions: Vec, + _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, } @@ -227,12 +226,15 @@ impl ExtensionsPage { cx.new(|cx| { let store = ExtensionStore::global(cx); let workspace_handle = workspace.weak_handle(); - let subscriptions = vec![ + let subscriptions = [ cx.observe(&store, |_: &mut Self, _, cx| cx.notify()), cx.subscribe_in( &store, window, move |this, _, event, window, cx| match event { + extension_host::Event::ExtensionsUpdated => { + this.fetch_extensions_debounced(cx) + } extension_host::Event::ExtensionInstalled(extension_id) => this .on_extension_installed( workspace_handle.clone(), @@ -243,15 +245,6 @@ impl ExtensionsPage { _ => {} }, ), - cx.subscribe_in( - &ExtensionEvents::global(cx), - window, - move |this, _, event, _window, cx| match event { - extension::Event::ExtensionsUpdated => { - this.fetch_extensions_debounced(cx); - } - }, - ), ]; let query_editor = cx.new(|cx| { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ea2b23996e..4960591d7c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -555,6 +555,23 @@ pub trait LspAdapter: 'static + Send + Sync { // By default all language servers are rooted at the root of the worktree. Some(Arc::from("".as_ref())) } + + /// Method only implemented by the default JSON language server adapter. + /// Used to provide dynamic reloading of the JSON schemas used to + /// provide autocompletion and diagnostics in Zed setting and keybind + /// files + fn is_primary_zed_json_schema_adapter(&self) -> bool { + false + } + + /// Method only implemented by the default JSON language server adapter. + /// Used to clear the cache of JSON schemas that are used to provide + /// autocompletion and diagnostics in Zed settings and keybinds files. + /// Should not be called unless the callee is sure that + /// `Self::is_primary_zed_json_schema_adapter` returns `true` + async fn clear_zed_json_schema_cache(&self) { + unreachable!("Not implemented for this adapter. This method should only be called on the default JSON language server adapter"); + } } async fn try_fetch_server_binary( diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 5be02583f5..778d3ae3c8 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -15,6 +15,7 @@ use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::{ fs::{self}, io::BufReader, + lock::RwLock, }; use std::{ any::Any, @@ -22,7 +23,7 @@ use std::{ ffi::OsString, path::{Path, PathBuf}, str::FromStr, - sync::{Arc, OnceLock}, + sync::Arc, }; use task::{TaskTemplate, TaskTemplates, VariableName}; use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt}; @@ -60,7 +61,7 @@ fn server_binary_arguments(server_path: &Path) -> Vec { pub struct JsonLspAdapter { node: NodeRuntime, languages: Arc, - workspace_config: OnceLock, + workspace_config: RwLock>, } impl JsonLspAdapter { @@ -141,6 +142,20 @@ impl JsonLspAdapter { } }) } + + async fn get_or_init_workspace_config(&self, cx: &mut AsyncApp) -> Result { + { + let reader = self.workspace_config.read().await; + if let Some(config) = reader.as_ref() { + return Ok(config.clone()); + } + } + let mut writer = self.workspace_config.write().await; + let config = + cx.update(|cx| Self::get_workspace_config(self.languages.language_names(), cx))?; + writer.replace(config.clone()); + return Ok(config); + } } #[async_trait(?Send)] @@ -251,11 +266,7 @@ impl LspAdapter for JsonLspAdapter { _: Arc, cx: &mut AsyncApp, ) -> Result { - let mut config = cx.update(|cx| { - self.workspace_config - .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx)) - .clone() - })?; + let mut config = self.get_or_init_workspace_config(cx).await?; let project_options = cx.update(|cx| { language_server_settings(delegate.as_ref(), &self.name(), cx) @@ -277,6 +288,14 @@ impl LspAdapter for JsonLspAdapter { .into_iter() .collect() } + + fn is_primary_zed_json_schema_adapter(&self) -> bool { + true + } + + async fn clear_zed_json_schema_cache(&self) { + self.workspace_config.write().await.take(); + } } async fn get_cached_server_binary( diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 4b66efda12..84163a0a4d 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -33,6 +33,7 @@ buffer_diff.workspace = true client.workspace = true clock.workspace = true collections.workspace = true +extension.workspace = true fancy-regex.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6ceabc8308..3f15d0d25a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3017,6 +3017,15 @@ impl LspStore { .detach(); cx.subscribe(&toolchain_store, Self::on_toolchain_store_event) .detach(); + if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() { + cx.subscribe( + extension_events, + Self::reload_zed_json_schemas_on_extensions_changed, + ) + .detach(); + } else { + log::info!("No extension events global found. Skipping JSON schema auto-reload setup"); + } cx.observe_global::(Self::on_settings_changed) .detach(); @@ -3277,6 +3286,109 @@ impl LspStore { Ok(()) } + pub fn reload_zed_json_schemas_on_extensions_changed( + &mut self, + _: Entity, + evt: &extension::Event, + cx: &mut Context, + ) { + #[expect( + irrefutable_let_patterns, + reason = "Make sure to handle new event types in extension properly" + )] + let extension::Event::ExtensionsInstalledChanged = evt + else { + return; + }; + if self.as_local().is_none() { + return; + } + cx.spawn(async move |this, mut cx| { + let weak_ref = this.clone(); + let servers = this + .update(&mut cx, |this, cx| { + let local = this.as_local()?; + + let mut servers = Vec::new(); + for ((worktree_id, _), server_ids) in &local.language_server_ids { + for server_id in server_ids { + let Some(states) = local.language_servers.get(server_id) else { + continue; + }; + let (json_adapter, json_server) = match states { + LanguageServerState::Running { + adapter, server, .. + } if adapter.adapter.is_primary_zed_json_schema_adapter() => { + (adapter.adapter.clone(), server.clone()) + } + _ => continue, + }; + + let Some(worktree) = this + .worktree_store + .read(cx) + .worktree_for_id(*worktree_id, cx) + else { + continue; + }; + let json_delegate: Arc = + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + weak_ref.clone(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ); + + servers.push((json_adapter, json_server, json_delegate)); + } + } + return Some(servers); + }) + .ok() + .flatten(); + + let Some(servers) = servers else { + return; + }; + + let Ok(Some((fs, toolchain_store))) = this.read_with(&cx, |this, cx| { + let local = this.as_local()?; + let toolchain_store = this.toolchain_store(cx); + return Some((local.fs.clone(), toolchain_store)); + }) else { + return; + }; + for (adapter, server, delegate) in servers { + adapter.clear_zed_json_schema_cache().await; + + let Some(json_workspace_config) = adapter + .workspace_configuration( + fs.as_ref(), + &delegate, + toolchain_store.clone(), + &mut cx, + ) + .await + .context("generate new workspace configuration for JSON language server while trying to refresh JSON Schemas") + .ok() + else { + continue; + }; + server + .notify::( + &lsp::DidChangeConfigurationParams { + settings: json_workspace_config, + }, + ) + .ok(); + } + }) + .detach(); + } + pub(crate) fn register_buffer_with_language_servers( &mut self, buffer: &Entity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 61ccfe27ae..038cc544f8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -95,6 +95,10 @@ use util::{ ResultExt as _, }; use worktree::{CreatedEntry, Snapshot, Traversal}; +pub use worktree::{ + Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, + UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY, +}; use worktree_store::{WorktreeStore, WorktreeStoreEvent}; pub use fs::*; @@ -104,10 +108,6 @@ pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use task_inventory::{ BasicContextProvider, ContextProviderWithTasks, Inventory, TaskContexts, TaskSourceKind, }; -pub use worktree::{ - Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet, - UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY, -}; pub use buffer_store::ProjectTransaction; pub use lsp_store::{