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:
parent
3b8a5c9647
commit
f53915c711
11 changed files with 303 additions and 10 deletions
|
@ -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()),
|
||||
|
|
44
crates/theme/src/icon_theme_schema.rs
Normal file
44
crates/theme/src/icon_theme_schema.rs
Normal file
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue