From 25f407baabd16cac7f2e23e2d84ca5cfdf3a106d Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 13 Mar 2025 11:50:07 -0500 Subject: [PATCH] settings: Auto-update JSON schemas for settings when extensions are un/installed (#26633) Because of #26562, it is now possible to subscribe to extension update events within the LSP store, where we can then update the Schemas sent to the JSON LSP resulting in dynamic updates to the auto-complete suggestions and diagnostics in settings. Notably, this means newly installed languages and (icon) themes will auto-complete correctly as soon as the extension is installed. Closes #15436 Release Notes: - Fixed an issue where autocomplete suggestions and diagnostics for languages and (icon) themes in settings would not update when the extension with which they were added was installed or uninstalled --- Cargo.lock | 2 +- crates/extension/src/extension_events.rs | 10 +- crates/extension_host/src/extension_host.rs | 11 +- crates/extensions_ui/Cargo.toml | 1 - crates/extensions_ui/src/extensions_ui.rs | 17 +-- crates/language/src/language.rs | 17 +++ crates/languages/src/json.rs | 33 ++++-- crates/project/Cargo.toml | 1 + crates/project/src/lsp_store.rs | 112 ++++++++++++++++++++ crates/project/src/project.rs | 8 +- 10 files changed, 180 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a53a38f08dd69f8f9f33180a4cec75c4b26b222..b872085661f7a72927879f6c3237ed571a58c0bf 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 17a6545cf165fc94656435f0f3e849b33ffc90cd..831010177d2f0af8531adab661e82012e0a46dbb 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 a9ad8dc9e9ccb1c7b4a141ea92ea9b23930a2034..a31736061448316fd50157c2977d17cc90d12143 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 7a67d98a8cf7ae23d25d5f3122b7662a227c85c7..afdb3bf0a359d97640c905f4d7e6955d2d0b02d0 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 0b79d654b086c2730d7d2a2ce7db077748e2b724..4c0fbf82902753f90b06437d50f0a14f61b2b5d9 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 ea2b23996eb0e6c8a59cda241a026cfa638e2068..4960591d7c8f2abfdb8a38abe33d90e141856e11 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 5be02583f53582bde70a0a0130130f98a622d4ca..778d3ae3c81bd804ce8c41922f0af13c4e2eb8c0 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 4b66efda12228b2a6c766debb338010d2a5abe74..84163a0a4d20bba4c51cfce4cb4a8f0c748a2757 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 6ceabc83086085e8782c374a5d630874faa5d667..3f15d0d25ad191c6e88d47084469377ecfd930e0 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 61ccfe27aece15be7fcc34633c3c0c4cea37b0cd..038cc544f85e4c886c0ef5e3004dabf0dff6c32c 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::{