diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index f99b1cad910..6e9207a2e9f 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 b22d175da56..676577cbd0c 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 6868701eb9e..59b00f936f9 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);