From da3847af7b141e0a18a4de5066ad73a001df0cf0 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Fri, 23 Jan 2026 14:49:44 +0100 Subject: [PATCH] Support multiple snippets locations per extension (#45829) This allows extensions to add more than one snippet file whilst keeping it backwards compatible. Release Notes: - Added support for specifying multiple snippets paths in extensions. --- crates/extension/src/extension_builder.rs | 10 ++++-- crates/extension/src/extension_manifest.rs | 26 +++++++++++++++- crates/extension_cli/src/main.rs | 34 ++++++++++++--------- crates/extension_host/src/extension_host.rs | 10 +++--- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index fe4477e227c17d42110e55412f8f1583859227be..6d7c3206e8811c88781348946b07fd0f81dd5c3b 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -717,7 +717,7 @@ mod tests { use indoc::indoc; use crate::{ - ExtensionManifest, + ExtensionManifest, ExtensionSnippets, extension_builder::{file_newer_than_deps, populate_defaults}, }; @@ -791,7 +791,9 @@ mod tests { assert_eq!( manifest.snippets, - Some(PathBuf::from_str("./snippets/snippets.json").unwrap()) + Some(ExtensionSnippets::Single( + PathBuf::from_str("./snippets/snippets.json").unwrap() + )) ) } @@ -826,7 +828,9 @@ mod tests { assert_eq!( manifest.snippets, - Some(PathBuf::from_str("snippets.json").unwrap()) + Some(ExtensionSnippets::Single( + PathBuf::from_str("snippets.json").unwrap() + )) ) } } diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 39b629db30d0d1cee3374dafc317bdeb0f368146..29571a24925db853bc31c8c0b5bddb1807970edc 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -53,6 +53,30 @@ impl SchemaVersion { } } +// TODO: We should change this to just always be a Vec once we bump the +// extension.toml schema version to 2 +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ExtensionSnippets { + Single(PathBuf), + Multiple(Vec), +} + +impl ExtensionSnippets { + pub fn paths(&self) -> impl Iterator { + match self { + ExtensionSnippets::Single(path) => std::slice::from_ref(path).iter(), + ExtensionSnippets::Multiple(paths) => paths.iter(), + } + } +} + +impl From<&str> for ExtensionSnippets { + fn from(value: &str) -> Self { + ExtensionSnippets::Single(value.into()) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct ExtensionManifest { pub id: Arc, @@ -86,7 +110,7 @@ pub struct ExtensionManifest { #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] - pub snippets: Option, + pub snippets: Option, #[serde(default)] pub capabilities: Vec, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 699a6b014322833a15fd593d7e5ac613a221ed24..324cf642587f17f29afaaf0cf7979ec7bf423599 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -306,22 +306,26 @@ async fn copy_extension_resources( } } - if let Some(snippets_path) = manifest.snippets.as_ref() { - let parent = snippets_path.parent(); - if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { - fs::create_dir_all(output_dir.join(parent))?; + if let Some(snippets) = manifest.snippets.as_ref() { + for snippets_path in snippets.paths() { + let parent = snippets_path.parent(); + if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { + fs::create_dir_all(output_dir.join(parent))?; + } + copy_recursive( + fs.as_ref(), + &extension_path.join(&snippets_path), + &output_dir.join(&snippets_path), + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| { + format!("failed to copy snippets from '{}'", snippets_path.display()) + })?; } - copy_recursive( - fs.as_ref(), - &extension_path.join(&snippets_path), - &output_dir.join(&snippets_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; } Ok(()) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 80b11a5229fe52e898450212b405f717394a7b56..7ad75e228917b83c15ac66d4313364e08fe3c259 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1252,10 +1252,12 @@ impl ExtensionStore { (path, icons_root_path) }, )); - snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]); - path + snippets_to_add.extend(extension.manifest.snippets.iter().flat_map(|snippets| { + snippets.paths().map(|snippets_path| { + let mut path = self.installed_dir.clone(); + path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]); + path + }) })); }