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.
This commit is contained in:
Finn Evers 2026-01-23 14:49:44 +01:00 committed by GitHub
parent 515a840e76
commit da3847af7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 23 deletions

View file

@ -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()
))
)
}
}

View file

@ -53,6 +53,30 @@ impl SchemaVersion {
}
}
// TODO: We should change this to just always be a Vec<PathBuf> 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<PathBuf>),
}
impl ExtensionSnippets {
pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
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<str>,
@ -86,7 +110,7 @@ pub struct ExtensionManifest {
#[serde(default)]
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
#[serde(default)]
pub snippets: Option<PathBuf>,
pub snippets: Option<ExtensionSnippets>,
#[serde(default)]
pub capabilities: Vec<ExtensionCapability>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]

View file

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

View file

@ -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
})
}));
}