Add infrastructure for loading icon themes from extensions (#23203)

This PR adds the supporting infrastructure to support loading icon
themes defined by extensions.

Here's an example icon theme:

```json
{
  "name": "My Icon Theme",
  "author": "Me <me@example.com>",
  "themes": [
    {
      "name": "My Icon Theme",
      "appearance": "dark",
      "file_icons": {
        "gleam": { "path": "./icons/file_type_gleam.svg" },
        "toml": { "path": "./icons/file_type_toml.svg" }
      }
    }
  ]
}
```

The icon paths are resolved relative to the root of the extension
directory.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-01-15 18:33:47 -05:00 committed by GitHub
parent 3b8a5c9647
commit f53915c711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 303 additions and 10 deletions

View file

@ -142,6 +142,8 @@ impl Global for GlobalExtensionStore {}
pub struct ExtensionIndex {
pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>,
pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>,
#[serde(default)]
pub icon_themes: BTreeMap<Arc<str>, ExtensionIndexIconThemeEntry>,
pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>,
}
@ -157,6 +159,12 @@ pub struct ExtensionIndexThemeEntry {
pub path: PathBuf,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexIconThemeEntry {
pub extension: Arc<str>,
pub path: PathBuf,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexLanguageEntry {
pub extension: Arc<str>,
@ -1022,6 +1030,17 @@ impl ExtensionStore {
}
})
.collect::<Vec<_>>();
let icon_themes_to_remove = old_index
.icon_themes
.iter()
.filter_map(|(name, entry)| {
if extensions_to_unload.contains(&entry.extension) {
Some(name.clone().into())
} else {
None
}
})
.collect::<Vec<_>>();
let languages_to_remove = old_index
.languages
.iter()
@ -1050,6 +1069,7 @@ impl ExtensionStore {
self.wasm_extensions
.retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
self.proxy.remove_user_themes(themes_to_remove);
self.proxy.remove_icon_themes(icon_themes_to_remove);
self.proxy
.remove_languages(&languages_to_remove, &grammars_to_remove);
@ -1060,6 +1080,7 @@ impl ExtensionStore {
.collect::<Vec<_>>();
let mut grammars_to_add = Vec::new();
let mut themes_to_add = Vec::new();
let mut icon_themes_to_add = Vec::new();
let mut snippets_to_add = Vec::new();
for extension_id in &extensions_to_load {
let Some(extension) = new_index.extensions.get(extension_id) else {
@ -1078,6 +1099,17 @@ impl ExtensionStore {
path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
path
}));
icon_themes_to_add.extend(extension.manifest.icon_themes.iter().map(
|icon_theme_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), icon_theme_path.as_path()]);
let mut icons_root_path = self.installed_dir.clone();
icons_root_path.extend([Path::new(extension_id.as_ref())]);
(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()]);
@ -1146,6 +1178,13 @@ impl ExtensionStore {
.log_err();
}
for (icon_theme_path, icons_root_path) in icon_themes_to_add.into_iter() {
proxy
.load_icon_theme(icon_theme_path, icons_root_path, fs.clone())
.await
.log_err();
}
for snippets_path in &snippets_to_add {
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
{
@ -1364,6 +1403,38 @@ impl ExtensionStore {
}
}
if let Ok(mut icon_theme_paths) = fs.read_dir(&extension_dir.join("icon_themes")).await {
while let Some(icon_theme_path) = icon_theme_paths.next().await {
let icon_theme_path = icon_theme_path?;
let Ok(relative_path) = icon_theme_path.strip_prefix(&extension_dir) else {
continue;
};
let Some(icon_theme_families) = proxy
.list_icon_theme_names(icon_theme_path.clone(), fs.clone())
.await
.log_err()
else {
continue;
};
let relative_path = relative_path.to_path_buf();
if !extension_manifest.icon_themes.contains(&relative_path) {
extension_manifest.icon_themes.push(relative_path.clone());
}
for icon_theme_name in icon_theme_families {
index.icon_themes.insert(
icon_theme_name.into(),
ExtensionIndexIconThemeEntry {
extension: extension_id.clone(),
path: relative_path.clone(),
},
);
}
}
}
let extension_wasm_path = extension_dir.join("extension.wasm");
if fs.is_file(&extension_wasm_path).await {
extension_manifest