Detailed changes
@@ -560,6 +560,21 @@ fn populate_defaults(manifest: &mut ExtensionManifest, extension_path: &Path) ->
}
}
+ let icon_themes_dir = extension_path.join("icon_themes");
+ if icon_themes_dir.exists() {
+ for entry in fs::read_dir(&icon_themes_dir).context("failed to list icon themes dir")? {
+ let entry = entry?;
+ let icon_theme_path = entry.path();
+ if icon_theme_path.extension() == Some("json".as_ref()) {
+ let relative_icon_theme_path =
+ icon_theme_path.strip_prefix(extension_path)?.to_path_buf();
+ if !manifest.icon_themes.contains(&relative_icon_theme_path) {
+ manifest.icon_themes.push(relative_icon_theme_path);
+ }
+ }
+ }
+ }
+
let snippets_json_path = extension_path.join("snippets.json");
if snippets_json_path.exists() {
manifest.snippets = Some(snippets_json_path);
@@ -103,6 +103,21 @@ pub trait ExtensionThemeProxy: Send + Sync + 'static {
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>>;
fn reload_current_theme(&self, cx: &mut AppContext);
+
+ fn list_icon_theme_names(
+ &self,
+ icon_theme_path: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<Vec<String>>>;
+
+ fn remove_icon_themes(&self, icon_themes: Vec<SharedString>);
+
+ fn load_icon_theme(
+ &self,
+ icon_theme_path: PathBuf,
+ icons_root_dir: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<()>>;
}
impl ExtensionThemeProxy for ExtensionHostProxy {
@@ -137,6 +152,39 @@ impl ExtensionThemeProxy for ExtensionHostProxy {
proxy.reload_current_theme(cx)
}
+
+ fn list_icon_theme_names(
+ &self,
+ icon_theme_path: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<Vec<String>>> {
+ let Some(proxy) = self.theme_proxy.read().clone() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ proxy.list_icon_theme_names(icon_theme_path, fs)
+ }
+
+ fn remove_icon_themes(&self, icon_themes: Vec<SharedString>) {
+ let Some(proxy) = self.theme_proxy.read().clone() else {
+ return;
+ };
+
+ proxy.remove_icon_themes(icon_themes)
+ }
+
+ fn load_icon_theme(
+ &self,
+ icon_theme_path: PathBuf,
+ icons_root_dir: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<()>> {
+ let Some(proxy) = self.theme_proxy.read().clone() else {
+ return Task::ready(Ok(()));
+ };
+
+ proxy.load_icon_theme(icon_theme_path, icons_root_dir, fs)
+ }
}
pub trait ExtensionGrammarProxy: Send + Sync + 'static {
@@ -70,6 +70,8 @@ pub struct ExtensionManifest {
#[serde(default)]
pub themes: Vec<PathBuf>,
#[serde(default)]
+ pub icon_themes: Vec<PathBuf>,
+ #[serde(default)]
pub languages: Vec<PathBuf>,
#[serde(default)]
pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
@@ -199,6 +201,7 @@ fn manifest_from_old_manifest(
themes.dedup();
themes
},
+ icon_themes: Vec::new(),
languages: {
let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
languages.sort();
@@ -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
@@ -149,6 +149,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
authors: Vec::new(),
repository: None,
themes: Default::default(),
+ icon_themes: Vec::new(),
lib: Default::default(),
languages: vec!["languages/erb".into(), "languages/ruby".into()],
grammars: [
@@ -181,6 +182,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
"themes/monokai-pro.json".into(),
"themes/monokai.json".into(),
],
+ icon_themes: Vec::new(),
lib: Default::default(),
languages: Default::default(),
grammars: BTreeMap::default(),
@@ -258,6 +260,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
]
.into_iter()
.collect(),
+ icon_themes: BTreeMap::default(),
};
let proxy = Arc::new(ExtensionHostProxy::new());
@@ -344,6 +347,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
authors: vec![],
repository: None,
themes: vec!["themes/gruvbox.json".into()],
+ icon_themes: Vec::new(),
lib: Default::default(),
languages: Default::default(),
grammars: BTreeMap::default(),
@@ -124,14 +124,14 @@ const FILE_ICONS: &[(&str, &str)] = &[
("zig", "icons/file_icons/zig.svg"),
];
-/// The ID of the default icon theme.
-pub(crate) const DEFAULT_ICON_THEME_ID: &str = "zed";
+/// The name of the default icon theme.
+pub(crate) const DEFAULT_ICON_THEME_NAME: &str = "Zed (Default)";
/// Returns the default icon theme.
pub fn default_icon_theme() -> IconTheme {
IconTheme {
- id: DEFAULT_ICON_THEME_ID.into(),
- name: "Zed (Default)".into(),
+ id: "zed".into(),
+ name: DEFAULT_ICON_THEME_NAME.into(),
appearance: Appearance::Dark,
directory_icons: DirectoryIcons {
collapsed: Some("icons/file_icons/folder.svg".into()),
@@ -0,0 +1,44 @@
+#![allow(missing_docs)]
+
+use gpui::SharedString;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+use crate::AppearanceContent;
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct IconThemeFamilyContent {
+ pub name: String,
+ pub author: String,
+ pub themes: Vec<IconThemeContent>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct IconThemeContent {
+ pub name: String,
+ pub appearance: AppearanceContent,
+ #[serde(default)]
+ pub directory_icons: DirectoryIconsContent,
+ #[serde(default)]
+ pub chevron_icons: ChevronIconsContent,
+ #[serde(default)]
+ pub file_icons: HashMap<String, IconDefinitionContent>,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct DirectoryIconsContent {
+ pub collapsed: Option<SharedString>,
+ pub expanded: Option<SharedString>,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ChevronIconsContent {
+ pub collapsed: Option<SharedString>,
+ pub expanded: Option<SharedString>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct IconDefinitionContent {
+ pub path: SharedString,
+}
@@ -11,8 +11,9 @@ use parking_lot::RwLock;
use util::ResultExt;
use crate::{
- read_user_theme, refine_theme_family, Appearance, IconTheme, Theme, ThemeFamily,
- ThemeFamilyContent, DEFAULT_ICON_THEME_ID,
+ read_icon_theme, read_user_theme, refine_theme_family, Appearance, AppearanceContent,
+ ChevronIcons, DirectoryIcons, IconDefinition, IconTheme, Theme, ThemeFamily,
+ ThemeFamilyContent, DEFAULT_ICON_THEME_NAME,
};
/// The metadata for a theme.
@@ -80,7 +81,7 @@ impl ThemeRegistry {
let default_icon_theme = crate::default_icon_theme();
registry.state.write().icon_themes.insert(
- default_icon_theme.id.clone().into(),
+ default_icon_theme.name.clone(),
Arc::new(default_icon_theme),
);
@@ -208,7 +209,7 @@ impl ThemeRegistry {
/// Returns the default icon theme.
pub fn default_icon_theme(&self) -> Result<Arc<IconTheme>> {
- self.get_icon_theme(DEFAULT_ICON_THEME_ID)
+ self.get_icon_theme(DEFAULT_ICON_THEME_NAME)
}
/// Returns the icon theme with the specified name.
@@ -220,6 +221,67 @@ impl ThemeRegistry {
.ok_or_else(|| anyhow!("icon theme not found: {name}"))
.cloned()
}
+
+ /// Removes the icon themes with the given names from the registry.
+ pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) {
+ self.state
+ .write()
+ .icon_themes
+ .retain(|name, _| !icon_themes_to_remove.contains(name))
+ }
+
+ /// Loads the icon theme from the specified path and adds it to the registry.
+ ///
+ /// The `icons_root_dir` parameter indicates the root directory from which
+ /// the relative paths to icons in the theme should be resolved against.
+ pub async fn load_icon_theme(
+ &self,
+ icon_theme_path: &Path,
+ icons_root_dir: &Path,
+ fs: Arc<dyn Fs>,
+ ) -> Result<()> {
+ let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?;
+
+ let mut state = self.state.write();
+ for icon_theme in icon_theme_family.themes {
+ let icon_theme = IconTheme {
+ id: uuid::Uuid::new_v4().to_string(),
+ name: icon_theme.name.into(),
+ appearance: match icon_theme.appearance {
+ AppearanceContent::Light => Appearance::Light,
+ AppearanceContent::Dark => Appearance::Dark,
+ },
+ directory_icons: DirectoryIcons {
+ collapsed: icon_theme.directory_icons.collapsed,
+ expanded: icon_theme.directory_icons.expanded,
+ },
+ chevron_icons: ChevronIcons {
+ collapsed: icon_theme.chevron_icons.collapsed,
+ expanded: icon_theme.chevron_icons.expanded,
+ },
+ file_icons: icon_theme
+ .file_icons
+ .into_iter()
+ .map(|(key, icon)| {
+ let path = icons_root_dir.join(icon.path.as_ref());
+
+ (
+ key,
+ IconDefinition {
+ path: path.to_string_lossy().to_string().into(),
+ },
+ )
+ })
+ .collect(),
+ };
+
+ state
+ .icon_themes
+ .insert(icon_theme.name.clone(), Arc::new(icon_theme));
+ }
+
+ Ok(())
+ }
}
impl Default for ThemeRegistry {
@@ -1,7 +1,7 @@
use crate::fallback_themes::zed_default_dark;
use crate::{
Appearance, IconTheme, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent,
- DEFAULT_ICON_THEME_ID,
+ DEFAULT_ICON_THEME_NAME,
};
use anyhow::Result;
use derive_more::{Deref, DerefMut};
@@ -647,7 +647,7 @@ impl settings::Settings for ThemeSettings {
.icon_theme
.as_ref()
.and_then(|name| themes.get_icon_theme(name).ok())
- .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_ID).unwrap()),
+ .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
};
@@ -12,6 +12,7 @@ mod default_colors;
mod fallback_themes;
mod font_family_cache;
mod icon_theme;
+mod icon_theme_schema;
mod registry;
mod scale;
mod schema;
@@ -34,6 +35,7 @@ use uuid::Uuid;
pub use crate::default_colors::*;
pub use crate::font_family_cache::*;
pub use crate::icon_theme::*;
+pub use crate::icon_theme_schema::*;
pub use crate::registry::*;
pub use crate::scale::*;
pub use crate::schema::*;
@@ -364,3 +366,14 @@ pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<Theme
Ok(theme_family)
}
+
+/// Asynchronously reads the icon theme from the specified path.
+pub async fn read_icon_theme(
+ icon_theme_path: &Path,
+ fs: Arc<dyn Fs>,
+) -> Result<IconThemeFamilyContent> {
+ let reader = fs.open_sync(icon_theme_path).await?;
+ let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_reader(reader)?;
+
+ Ok(icon_theme_family)
+}
@@ -44,4 +44,37 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
fn reload_current_theme(&self, cx: &mut AppContext) {
ThemeSettings::reload_current_theme(cx)
}
+
+ fn list_icon_theme_names(
+ &self,
+ icon_theme_path: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<Vec<String>>> {
+ self.executor.spawn(async move {
+ let icon_theme_family = theme::read_icon_theme(&icon_theme_path, fs).await?;
+ Ok(icon_theme_family
+ .themes
+ .into_iter()
+ .map(|theme| theme.name)
+ .collect())
+ })
+ }
+
+ fn remove_icon_themes(&self, icon_themes: Vec<SharedString>) {
+ self.theme_registry.remove_icon_themes(&icon_themes);
+ }
+
+ fn load_icon_theme(
+ &self,
+ icon_theme_path: PathBuf,
+ icons_root_dir: PathBuf,
+ fs: Arc<dyn Fs>,
+ ) -> Task<Result<()>> {
+ let theme_registry = self.theme_registry.clone();
+ self.executor.spawn(async move {
+ theme_registry
+ .load_icon_theme(&icon_theme_path, &icons_root_dir, fs)
+ .await
+ })
+ }
}