diff --git a/Cargo.lock b/Cargo.lock index 44165e8a44..60ff786626 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4490,6 +4490,8 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "settings", + "theme", "util", ] diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 1f579b0da8..463d0d9c07 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -210,208 +210,5 @@ "zsh_profile": "terminal", "zshenv": "terminal", "zshrc": "terminal" - }, - "types": { - "astro": { - "icon": "icons/file_icons/astro.svg" - }, - "audio": { - "icon": "icons/file_icons/audio.svg" - }, - "bun": { - "icon": "icons/file_icons/bun.svg" - }, - "c": { - "icon": "icons/file_icons/c.svg" - }, - "code": { - "icon": "icons/file_icons/code.svg" - }, - "coffeescript": { - "icon": "icons/file_icons/coffeescript.svg" - }, - "collapsed_chevron": { - "icon": "icons/file_icons/chevron_right.svg" - }, - "collapsed_folder": { - "icon": "icons/file_icons/folder.svg" - }, - "cpp": { - "icon": "icons/file_icons/cpp.svg" - }, - "css": { - "icon": "icons/file_icons/css.svg" - }, - "dart": { - "icon": "icons/file_icons/dart.svg" - }, - "default": { - "icon": "icons/file_icons/file.svg" - }, - "diff": { - "icon": "icons/file_icons/diff.svg" - }, - "docker": { - "icon": "icons/file_icons/docker.svg" - }, - "document": { - "icon": "icons/file_icons/book.svg" - }, - "elixir": { - "icon": "icons/file_icons/elixir.svg" - }, - "elm": { - "icon": "icons/file_icons/elm.svg" - }, - "erlang": { - "icon": "icons/file_icons/erlang.svg" - }, - "eslint": { - "icon": "icons/file_icons/eslint.svg" - }, - "expanded_chevron": { - "icon": "icons/file_icons/chevron_down.svg" - }, - "expanded_folder": { - "icon": "icons/file_icons/folder_open.svg" - }, - "font": { - "icon": "icons/file_icons/font.svg" - }, - "fsharp": { - "icon": "icons/file_icons/fsharp.svg" - }, - "gleam": { - "icon": "icons/file_icons/gleam.svg" - }, - "go": { - "icon": "icons/file_icons/go.svg" - }, - "graphql": { - "icon": "icons/file_icons/graphql.svg" - }, - "haskell": { - "icon": "icons/file_icons/haskell.svg" - }, - "hcl": { - "icon": "icons/file_icons/hcl.svg" - }, - "heroku": { - "icon": "icons/file_icons/heroku.svg" - }, - "image": { - "icon": "icons/file_icons/image.svg" - }, - "java": { - "icon": "icons/file_icons/java.svg" - }, - "javascript": { - "icon": "icons/file_icons/javascript.svg" - }, - "julia": { - "icon": "icons/file_icons/julia.svg" - }, - "kotlin": { - "icon": "icons/file_icons/kotlin.svg" - }, - "lock": { - "icon": "icons/file_icons/lock.svg" - }, - "log": { - "icon": "icons/file_icons/info.svg" - }, - "lua": { - "icon": "icons/file_icons/lua.svg" - }, - "metal": { - "icon": "icons/file_icons/metal.svg" - }, - "nim": { - "icon": "icons/file_icons/nim.svg" - }, - "nix": { - "icon": "icons/file_icons/nix.svg" - }, - "ocaml": { - "icon": "icons/file_icons/ocaml.svg" - }, - "phoenix": { - "icon": "icons/file_icons/phoenix.svg" - }, - "php": { - "icon": "icons/file_icons/php.svg" - }, - "prettier": { - "icon": "icons/file_icons/prettier.svg" - }, - "prisma": { - "icon": "icons/file_icons/prisma.svg" - }, - "python": { - "icon": "icons/file_icons/python.svg" - }, - "r": { - "icon": "icons/file_icons/r.svg" - }, - "react": { - "icon": "icons/file_icons/react.svg" - }, - "roc": { - "icon": "icons/file_icons/roc.svg" - }, - "ruby": { - "icon": "icons/file_icons/ruby.svg" - }, - "rust": { - "icon": "icons/file_icons/rust.svg" - }, - "sass": { - "icon": "icons/file_icons/sass.svg" - }, - "scala": { - "icon": "icons/file_icons/scala.svg" - }, - "settings": { - "icon": "icons/file_icons/settings.svg" - }, - "storage": { - "icon": "icons/file_icons/database.svg" - }, - "swift": { - "icon": "icons/file_icons/swift.svg" - }, - "tcl": { - "icon": "icons/file_icons/tcl.svg" - }, - "template": { - "icon": "icons/file_icons/html.svg" - }, - "terminal": { - "icon": "icons/file_icons/terminal.svg" - }, - "terraform": { - "icon": "icons/file_icons/terraform.svg" - }, - "toml": { - "icon": "icons/file_icons/toml.svg" - }, - "typescript": { - "icon": "icons/file_icons/typescript.svg" - }, - "v": { - "icon": "icons/file_icons/v.svg" - }, - "vcs": { - "icon": "icons/file_icons/git.svg" - }, - "video": { - "icon": "icons/file_icons/video.svg" - }, - "vue": { - "icon": "icons/file_icons/vue.svg" - }, - "zig": { - "icon": "icons/file_icons/zig.svg" - } } } diff --git a/crates/file_icons/Cargo.toml b/crates/file_icons/Cargo.toml index c3e9268997..c0847cd888 100644 --- a/crates/file_icons/Cargo.toml +++ b/crates/file_icons/Cargo.toml @@ -13,9 +13,11 @@ path = "src/file_icons.rs" doctest = false [dependencies] +collections.workspace = true gpui.workspace = true -util.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -collections.workspace = true +settings.workspace = true +theme.workspace = true +util.workspace = true diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index a0b39fb763..62ddb98f69 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -4,18 +4,14 @@ use collections::HashMap; use gpui::{AppContext, AssetSource, Global, SharedString}; use serde_derive::Deserialize; +use settings::Settings; +use theme::ThemeSettings; use util::{maybe, paths::PathExt}; -#[derive(Deserialize, Debug)] -struct TypeConfig { - icon: SharedString, -} - #[derive(Deserialize, Debug)] pub struct FileIcons { stems: HashMap, suffixes: HashMap, - types: HashMap, } impl Global for FileIcons {} @@ -37,14 +33,13 @@ impl FileIcons { pub fn new(assets: impl AssetSource) -> Self { assets - .load("icons/file_icons/file_types.json") + .load(FILE_TYPES_ASSET) .ok() .flatten() .and_then(|file| serde_json::from_str::(str::from_utf8(&file).unwrap()).ok()) .unwrap_or_else(|| FileIcons { stems: HashMap::default(), suffixes: HashMap::default(), - types: HashMap::default(), }) } @@ -57,20 +52,24 @@ impl FileIcons { let suffix = path.icon_stem_or_suffix()?; if let Some(type_str) = this.stems.get(suffix) { - return this.get_type_icon(type_str); + return this.get_icon_for_type(type_str, cx); } this.suffixes .get(suffix) - .and_then(|type_str| this.get_type_icon(type_str)) + .and_then(|type_str| this.get_icon_for_type(type_str, cx)) }) - .or_else(|| this.get_type_icon("default")) + .or_else(|| this.get_icon_for_type("default", cx)) } - pub fn get_type_icon(&self, typ: &str) -> Option { - self.types + pub fn get_icon_for_type(&self, typ: &str, cx: &AppContext) -> Option { + let theme_settings = ThemeSettings::get_global(cx); + + theme_settings + .active_icon_theme + .file_icons .get(typ) - .map(|type_config| type_config.icon.clone()) + .map(|icon_definition| icon_definition.path.clone()) } pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option { @@ -82,7 +81,7 @@ impl FileIcons { COLLAPSED_DIRECTORY_TYPE }; - this.get_type_icon(key) + this.get_icon_for_type(key, cx) } pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option { @@ -94,6 +93,6 @@ impl FileIcons { COLLAPSED_CHEVRON_TYPE }; - this.get_type_icon(key) + this.get_icon_for_type(key, cx) } } diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index e829b1946c..ddd3cebf01 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -69,7 +69,7 @@ impl KernelSpecification { }; file_icons::FileIcons::get(cx) - .get_type_icon(&lang_name.to_lowercase()) + .get_icon_for_type(&lang_name.to_lowercase(), cx) .map(Icon::from_path) .unwrap_or(Icon::new(IconName::ReplNeutral)) } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index ead9a01396..1dc63c4852 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -350,7 +350,7 @@ impl PickerDelegate for TasksModalDelegate { TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)), TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)), TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx) - .get_type_icon(&name.to_lowercase()) + .get_icon_for_type(&name.to_lowercase(), cx) .map(Icon::from_path), } .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs new file mode 100644 index 0000000000..1a08a93f08 --- /dev/null +++ b/crates/theme/src/icon_theme.rs @@ -0,0 +1,127 @@ +use collections::HashMap; +use gpui::SharedString; + +use crate::Appearance; + +/// A family of icon themes. +pub struct IconThemeFamily { + /// The unique ID for the icon theme family. + pub id: String, + /// The name of the icon theme family. + pub name: SharedString, + /// The author of the icon theme family. + pub author: SharedString, + /// The list of icon themes in the family. + pub themes: Vec, +} + +/// An icon theme. +#[derive(Debug, PartialEq)] +pub struct IconTheme { + /// The unique ID for the icon theme. + pub id: String, + /// The name of the icon theme. + pub name: SharedString, + /// The appearance of the icon theme (e.g., light or dark). + pub appearance: Appearance, + /// The mapping of file types to icon definitions. + pub file_icons: HashMap, +} + +/// An icon definition. +#[derive(Debug, PartialEq)] +pub struct IconDefinition { + /// The path to the icon file. + pub path: SharedString, +} + +/// A mapping of a file type identifier to its corresponding icon. +const FILE_ICONS: &[(&str, &str)] = &[ + ("astro", "icons/file_icons/astro.svg"), + ("audio", "icons/file_icons/audio.svg"), + ("bun", "icons/file_icons/bun.svg"), + ("c", "icons/file_icons/c.svg"), + ("code", "icons/file_icons/code.svg"), + ("coffeescript", "icons/file_icons/coffeescript.svg"), + ("collapsed_chevron", "icons/file_icons/chevron_right.svg"), + ("collapsed_folder", "icons/file_icons/folder.svg"), + ("cpp", "icons/file_icons/cpp.svg"), + ("css", "icons/file_icons/css.svg"), + ("dart", "icons/file_icons/dart.svg"), + ("default", "icons/file_icons/file.svg"), + ("diff", "icons/file_icons/diff.svg"), + ("docker", "icons/file_icons/docker.svg"), + ("document", "icons/file_icons/book.svg"), + ("elixir", "icons/file_icons/elixir.svg"), + ("elm", "icons/file_icons/elm.svg"), + ("erlang", "icons/file_icons/erlang.svg"), + ("eslint", "icons/file_icons/eslint.svg"), + ("expanded_chevron", "icons/file_icons/chevron_down.svg"), + ("expanded_folder", "icons/file_icons/folder_open.svg"), + ("font", "icons/file_icons/font.svg"), + ("fsharp", "icons/file_icons/fsharp.svg"), + ("gleam", "icons/file_icons/gleam.svg"), + ("go", "icons/file_icons/go.svg"), + ("graphql", "icons/file_icons/graphql.svg"), + ("haskell", "icons/file_icons/haskell.svg"), + ("hcl", "icons/file_icons/hcl.svg"), + ("heroku", "icons/file_icons/heroku.svg"), + ("image", "icons/file_icons/image.svg"), + ("java", "icons/file_icons/java.svg"), + ("javascript", "icons/file_icons/javascript.svg"), + ("julia", "icons/file_icons/julia.svg"), + ("kotlin", "icons/file_icons/kotlin.svg"), + ("lock", "icons/file_icons/lock.svg"), + ("log", "icons/file_icons/info.svg"), + ("lua", "icons/file_icons/lua.svg"), + ("metal", "icons/file_icons/metal.svg"), + ("nim", "icons/file_icons/nim.svg"), + ("nix", "icons/file_icons/nix.svg"), + ("ocaml", "icons/file_icons/ocaml.svg"), + ("phoenix", "icons/file_icons/phoenix.svg"), + ("php", "icons/file_icons/php.svg"), + ("prettier", "icons/file_icons/prettier.svg"), + ("prisma", "icons/file_icons/prisma.svg"), + ("python", "icons/file_icons/python.svg"), + ("r", "icons/file_icons/r.svg"), + ("react", "icons/file_icons/react.svg"), + ("roc", "icons/file_icons/roc.svg"), + ("ruby", "icons/file_icons/ruby.svg"), + ("rust", "icons/file_icons/rust.svg"), + ("sass", "icons/file_icons/sass.svg"), + ("scala", "icons/file_icons/scala.svg"), + ("settings", "icons/file_icons/settings.svg"), + ("storage", "icons/file_icons/database.svg"), + ("swift", "icons/file_icons/swift.svg"), + ("tcl", "icons/file_icons/tcl.svg"), + ("template", "icons/file_icons/html.svg"), + ("terminal", "icons/file_icons/terminal.svg"), + ("terraform", "icons/file_icons/terraform.svg"), + ("toml", "icons/file_icons/toml.svg"), + ("typescript", "icons/file_icons/typescript.svg"), + ("v", "icons/file_icons/v.svg"), + ("vcs", "icons/file_icons/git.svg"), + ("video", "icons/file_icons/video.svg"), + ("vue", "icons/file_icons/vue.svg"), + ("zig", "icons/file_icons/zig.svg"), +]; + +/// The ID of the default icon theme. +pub(crate) const DEFAULT_ICON_THEME_ID: &str = "zed"; + +/// Returns the default icon theme. +pub fn default_icon_theme() -> IconTheme { + IconTheme { + id: DEFAULT_ICON_THEME_ID.into(), + name: "Zed (Default)".into(), + appearance: Appearance::Dark, + file_icons: HashMap::from_iter(FILE_ICONS.into_iter().map(|(ty, path)| { + ( + ty.to_string(), + IconDefinition { + path: (*path).into(), + }, + ) + })), + } +} diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index a511fb9da3..ad5ee9c86e 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -11,7 +11,8 @@ use parking_lot::RwLock; use util::ResultExt; use crate::{ - read_user_theme, refine_theme_family, Appearance, Theme, ThemeFamily, ThemeFamilyContent, + read_user_theme, refine_theme_family, Appearance, IconTheme, Theme, ThemeFamily, + ThemeFamilyContent, }; /// The metadata for a theme. @@ -36,6 +37,7 @@ impl Global for GlobalThemeRegistry {} struct ThemeRegistryState { themes: HashMap>, + icon_themes: HashMap>, } /// The registry for themes. @@ -67,6 +69,7 @@ impl ThemeRegistry { let registry = Self { state: RwLock::new(ThemeRegistryState { themes: HashMap::default(), + icon_themes: HashMap::default(), }), assets, }; @@ -75,6 +78,12 @@ impl ThemeRegistry { // for tests. registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]); + let default_icon_theme = crate::default_icon_theme(); + registry.state.write().icon_themes.insert( + default_icon_theme.id.clone().into(), + Arc::new(default_icon_theme), + ); + registry } @@ -196,6 +205,16 @@ impl ThemeRegistry { Ok(()) } + + /// Returns the icon theme with the specified name. + pub fn get_icon_theme(&self, name: &str) -> Result> { + self.state + .read() + .icon_themes + .get(name) + .ok_or_else(|| anyhow!("icon theme not found: {name}")) + .cloned() + } } impl Default for ThemeRegistry { diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index cebfea84bc..618e3f0268 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -1,5 +1,8 @@ use crate::fallback_themes::zed_default_dark; -use crate::{Appearance, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent}; +use crate::{ + Appearance, IconTheme, SyntaxTheme, Theme, ThemeRegistry, ThemeStyleContent, + DEFAULT_ICON_THEME_ID, +}; use anyhow::Result; use derive_more::{Deref, DerefMut}; use gpui::{ @@ -118,6 +121,8 @@ pub struct ThemeSettings { /// /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) pub theme_overrides: Option, + /// The active icon theme. + pub active_icon_theme: Arc, /// The density of the UI. /// Note: This setting is still experimental. See [this tracking issue]( pub ui_density: UiDensity, @@ -324,6 +329,12 @@ pub struct ThemeSettingsContent { /// The name of the Zed theme to use. #[serde(default)] pub theme: Option, + /// The name of the icon theme to use. + /// + /// Currently not exposed to the user. + #[serde(skip)] + #[serde(default)] + pub icon_theme: Option, /// UNSTABLE: Expect many elements to be broken. /// @@ -632,6 +643,11 @@ impl settings::Settings for ThemeSettings { .or(themes.get(&zed_default_dark().name)) .unwrap(), theme_overrides: None, + active_icon_theme: defaults + .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()), ui_density: defaults.ui_density.unwrap_or(UiDensity::Default), unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0), }; @@ -685,6 +701,12 @@ impl settings::Settings for ThemeSettings { this.theme_overrides.clone_from(&value.theme_overrides); this.apply_theme_overrides(); + if let Some(value) = &value.icon_theme { + if let Some(icon_theme) = themes.get_icon_theme(value).log_err() { + this.active_icon_theme = icon_theme.clone(); + } + } + merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into)); this.ui_font_size = this.ui_font_size.clamp(px(6.), px(100.)); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 2a4802b4eb..4f395696aa 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -11,6 +11,7 @@ mod default_colors; mod fallback_themes; mod font_family_cache; +mod icon_theme; mod registry; mod scale; mod schema; @@ -32,6 +33,7 @@ use uuid::Uuid; pub use crate::default_colors::*; pub use crate::font_family_cache::*; +pub use crate::icon_theme::*; pub use crate::registry::*; pub use crate::scale::*; pub use crate::schema::*; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cc4e98f38c..4283c8de04 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1217,7 +1217,7 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { use gpui::UpdateGlobal; let path = { - let p = Path::new("assets/icons/file_icons/file_types.json"); + let p = Path::new("assets").join(file_icons::FILE_TYPES_ASSET); let Ok(full_path) = p.canonicalize() else { return; };