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
This commit is contained in:
Ben Kunkle 2025-03-13 11:50:07 -05:00 committed by GitHub
parent 79874872cb
commit 25f407baab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 180 additions and 32 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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<Self> {
GlobalExtensionEvents::global(cx).0.clone()
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
return cx
.try_global::<GlobalExtensionEvents>()
.map(|g| g.0.clone());
}
fn new(_cx: &mut Context<Self>) -> Self {
@ -29,7 +31,7 @@ impl ExtensionEvents {
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
ExtensionsInstalledChanged,
}
impl EventEmitter<Event> for ExtensionEvents {}

View file

@ -127,6 +127,7 @@ pub enum ExtensionOperation {
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
StartedReloading,
ExtensionInstalled(Arc<str>),
ExtensionFailedToLoad(Arc<str>),
@ -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();
})

View file

@ -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

View file

@ -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<Editor>,
query_contains_error: bool,
provides_filter: Option<ExtensionProvides>,
_subscriptions: Vec<gpui::Subscription>,
_subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>,
}
@ -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| {

View file

@ -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<L: LspAdapter + 'static + Send + Sync + ?Sized>(

View file

@ -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<OsString> {
pub struct JsonLspAdapter {
node: NodeRuntime,
languages: Arc<LanguageRegistry>,
workspace_config: OnceLock<Value>,
workspace_config: RwLock<Option<Value>>,
}
impl JsonLspAdapter {
@ -141,6 +142,20 @@ impl JsonLspAdapter {
}
})
}
async fn get_or_init_workspace_config(&self, cx: &mut AsyncApp) -> Result<Value> {
{
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<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
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(

View file

@ -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

View file

@ -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::<SettingsStore>(Self::on_settings_changed)
.detach();
@ -3277,6 +3286,109 @@ impl LspStore {
Ok(())
}
pub fn reload_zed_json_schemas_on_extensions_changed(
&mut self,
_: Entity<extension::ExtensionEvents>,
evt: &extension::Event,
cx: &mut Context<Self>,
) {
#[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<dyn LspAdapterDelegate> =
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::notification::DidChangeConfiguration>(
&lsp::DidChangeConfigurationParams {
settings: json_workspace_config,
},
)
.ok();
}
})
.detach();
}
pub(crate) fn register_buffer_with_language_servers(
&mut self,
buffer: &Entity<Buffer>,

View file

@ -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::{