From 6fbec96adbacdb692a8a6ca4f11a354c2b6ea42b Mon Sep 17 00:00:00 2001 From: liam Date: Wed, 24 Jun 2026 17:32:43 -0400 Subject: [PATCH] extension_host: Sync language extensions to remotes (#56487) Resolves #56068 Remote extension sync previously only sent extensions that can run remotely: language servers, debug adapters, and debug locators. That excluded language-only extensions such as TOML, so a remote session could have Tombi synced and loaded while the remote language registry still lacked TOML's language definition. This diff keeps the remote sync set narrow: it starts from extensions that can run remotely, then adds the extension that defines each language referenced by those extensions' language servers. This installs the TOML language config alongside Tombi without syncing unrelated language-only extensions, allowing project settings schemas to recognize language names like TOML and allowing remote language servers to attach to buffers whose language is provided by a separate extension. Release Notes: - Fixed remote sessions failing to recognize languages provided by language-only extensions. --------- Co-authored-by: Finn Evers --- crates/extension/src/extension_manifest.rs | 26 ++- crates/extension_host/src/extension_host.rs | 68 +++++-- .../src/extension_store_test.rs | 179 ++++++++++++++++++ 3 files changed, 257 insertions(+), 16 deletions(-) diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index f99b1cad91..6e9207a2e9 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -181,9 +181,27 @@ impl ExtensionManifest { } pub fn allow_remote_load(&self) -> bool { - !self.language_servers.is_empty() + self.remote_load().is_some() + } + + pub fn remote_load(&self) -> Option> { + (!self.language_servers.is_empty() || !self.debug_adapters.is_empty() - || !self.debug_locators.is_empty() + || !self.debug_locators.is_empty()) + .then_some(RemoteLoad { manifest: self }) + } +} + +pub struct RemoteLoad<'a> { + manifest: &'a ExtensionManifest, +} + +impl RemoteLoad<'_> { + pub fn language_dependencies(&self) -> impl Iterator + '_ { + self.manifest + .language_servers + .values() + .flat_map(|language_server_config| language_server_config.languages()) } } @@ -345,7 +363,7 @@ pub struct SlashCommandManifestEntry { pub requires_argument: bool, } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct DebugAdapterManifestEntry { pub schema_path: Option, } @@ -484,7 +502,7 @@ mod tests { #[test] fn test_build_adapter_schema_path_without_schema_path() { let adapter_name = Arc::from("my_adapter"); - let entry = DebugAdapterManifestEntry { schema_path: None }; + let entry = DebugAdapterManifestEntry::default(); let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap(); assert_eq!(path, rel_path_buf("debug_adapter_schemas/my_adapter.json")); diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index b22d175da5..676577cbd0 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use client::{Client, proto, telemetry::Telemetry}; use cloud_api_types::{ExtensionMetadata, ExtensionProvides, GetExtensionsResponse}; -use collections::{BTreeMap, BTreeSet, FxHashSet, HashSet, btree_map}; +use collections::{BTreeMap, BTreeSet, FxHashSet, HashMap, HashSet, btree_map}; pub use extension::ExtensionManifest; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::{ @@ -178,6 +178,55 @@ pub struct ExtensionIndex { pub languages: BTreeMap, } +impl ExtensionIndex { + fn extensions_to_sync_to_remote(&self) -> RemoteSyncExtensions { + let mut extensions = RemoteSyncExtensions::default(); + + for (id, entry) in &self.extensions { + if entry.manifest.remote_load().is_some() { + extensions.insert_extension_and_language_dependencies(self, id); + } + } + + extensions + } +} + +#[derive(Default)] +struct RemoteSyncExtensions(HashMap, ExtensionIndexEntry>); + +impl RemoteSyncExtensions { + fn insert_extension_and_language_dependencies( + &mut self, + index: &ExtensionIndex, + id: &Arc, + ) { + if self.0.contains_key(id) { + return; + } + + let Some(entry) = index.extensions.get(id) else { + return; + }; + + self.0.insert(id.clone(), entry.clone()); + + let Some(remote_load) = entry.manifest.remote_load() else { + return; + }; + + for language in remote_load.language_dependencies() { + if let Some(language_entry) = index.languages.get(&language) { + self.insert_extension_and_language_dependencies(index, &language_entry.extension); + } + } + } + + fn into_entries(self) -> impl Iterator, ExtensionIndexEntry)> { + self.0.into_iter() + } +} + #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct ExtensionIndexEntry { pub manifest: Arc, @@ -1812,17 +1861,12 @@ impl ExtensionStore { ) -> Result<()> { let extensions = this.update(cx, |this, _cx| { this.extension_index - .extensions - .iter() - .filter_map(|(id, entry)| { - if !entry.manifest.allow_remote_load() { - return None; - } - Some(proto::Extension { - id: id.to_string(), - version: entry.manifest.version.to_string(), - dev: entry.dev, - }) + .extensions_to_sync_to_remote() + .into_entries() + .map(|(id, entry)| proto::Extension { + id: id.to_string(), + version: entry.manifest.version.to_string(), + dev: entry.dev, }) .collect() })?; diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 6868701eb9..59b00f936f 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -34,6 +34,185 @@ fn init_logger() { zlog::init_test(); } +fn remote_sync_entry(id: &str, manifest_body: &str) -> ExtensionIndexEntry { + let manifest = format!( + r#" + id = "{id}" + name = "{id}" + version = "1.0.0" + schema_version = 0 + + {manifest_body} + "# + ); + + ExtensionIndexEntry { + manifest: Arc::new(toml::from_str(&manifest).unwrap()), + dev: false, + } +} + +fn remote_sync_language_entry(extension: &str, path: &str) -> ExtensionIndexLanguageEntry { + ExtensionIndexLanguageEntry { + extension: extension.into(), + path: path.into(), + matcher: LanguageMatcher::default(), + hidden: false, + grammar: None, + } +} + +fn remote_sync_extension_ids(index: &ExtensionIndex) -> Vec { + let mut extensions = index + .extensions_to_sync_to_remote() + .into_entries() + .map(|(id, _)| id.to_string()) + .collect::>(); + + extensions.sort(); + + extensions +} + +#[test] +fn remote_sync_includes_language_dependencies() { + let index = ExtensionIndex { + extensions: [ + ( + "bar-language".into(), + remote_sync_entry("bar-language", r#"languages = ["languages/bar"]"#), + ), + ( + "foo-lsp".into(), + remote_sync_entry( + "foo-lsp", + r#" + [language_servers.foo] + language = "Foo" + "#, + ), + ), + ( + "foo-language".into(), + remote_sync_entry("foo-language", r#"languages = ["languages/foo"]"#), + ), + ] + .into_iter() + .collect(), + languages: [ + ( + "Bar".into(), + remote_sync_language_entry("bar-language", "languages/bar"), + ), + ( + "Foo".into(), + remote_sync_language_entry("foo-language", "languages/foo"), + ), + ] + .into_iter() + .collect(), + themes: BTreeMap::default(), + icon_themes: BTreeMap::default(), + }; + + assert_eq!( + remote_sync_extension_ids(&index), + ["foo-language", "foo-lsp"] + ); +} + +#[test] +fn remote_sync_keeps_shared_language_dependency_once() { + let index = ExtensionIndex { + extensions: [ + ( + "aaa-lsp".into(), + remote_sync_entry( + "aaa-lsp", + r#" + [language_servers.aaa] + language = "Foo" + "#, + ), + ), + ( + "bbb-lsp".into(), + remote_sync_entry( + "bbb-lsp", + r#" + [language_servers.bbb] + language = "Foo" + "#, + ), + ), + ( + "zzz-language".into(), + remote_sync_entry("zzz-language", r#"languages = ["languages/foo"]"#), + ), + ] + .into_iter() + .collect(), + languages: [( + "Foo".into(), + remote_sync_language_entry("zzz-language", "languages/foo"), + )] + .into_iter() + .collect(), + themes: BTreeMap::default(), + icon_themes: BTreeMap::default(), + }; + + assert_eq!( + remote_sync_extension_ids(&index), + ["aaa-lsp", "bbb-lsp", "zzz-language"] + ); +} + +#[test] +fn remote_sync_keeps_remote_loadable_extensions_without_language_dependency() { + let index = ExtensionIndex { + extensions: [( + "foo".into(), + remote_sync_entry( + "foo", + r#" + [language_servers.foo] + language = "Foo" + "#, + ), + )] + .into_iter() + .collect(), + languages: BTreeMap::default(), + themes: BTreeMap::default(), + icon_themes: BTreeMap::default(), + }; + + assert_eq!(remote_sync_extension_ids(&index), ["foo"]); +} + +#[test] +fn remote_sync_keeps_debug_adapters() { + let index = ExtensionIndex { + extensions: [( + "foo".into(), + remote_sync_entry( + "foo", + r#" + [debug_adapters.foo] + "#, + ), + )] + .into_iter() + .collect(), + languages: BTreeMap::default(), + themes: BTreeMap::default(), + icon_themes: BTreeMap::default(), + }; + + assert_eq!(remote_sync_extension_ids(&index), ["foo"]); +} + #[gpui::test] async fn test_extension_store(cx: &mut TestAppContext) { init_test(cx);