Allow icon themes to provide their own file associations (#24926)

This PR adds the ability for icon themes to provide their own file
associations.

The old `file_types.json` that was previously used to make these
associations has been removed in favor of storing them on the default
theme.

Icon themes have two new fields on them:

- `file_stems`: A mapping of file stems to icon keys.
- `file_suffixes`: A mapping of file suffixes to icon keys.

These mappings produce icon keys which can then be used in `file_icons`
to associate them to a particular icon:

```json
{
  "file_stems": {
    "Makefile": "make"
  },
  "file_suffixes": {
    "idr": "idris"
  },
  "file_icons": {
    "idris": { "path": "./icons/idris.svg" },
    "make": { "path": "./icons/make.svg" }
  }
}
```

When loading an icon theme, the `file_stems` and `file_icons` fields
will be merged with the ones from the base icon theme, with the values
from the icon theme being loaded overriding ones in the base theme.

Release Notes:

- Added the ability for icon themes to provide their own file
associations.
This commit is contained in:
Marshall Bowers 2025-02-14 19:35:13 -05:00 committed by GitHub
parent f2776099ab
commit e60123bbdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 286 additions and 370 deletions

View file

@ -1,3 +1,5 @@
use std::sync::{Arc, LazyLock};
use collections::HashMap;
use gpui::SharedString;
@ -28,7 +30,11 @@ pub struct IconTheme {
pub directory_icons: DirectoryIcons,
/// The icons used for chevrons.
pub chevron_icons: ChevronIcons,
/// The mapping of file types to icon definitions.
/// The mapping of file stems to their associated icon keys.
pub file_stems: HashMap<String, String>,
/// The mapping of file suffixes to their associated icon keys.
pub file_suffixes: HashMap<String, String>,
/// The mapping of icon keys to icon definitions.
pub file_icons: HashMap<String, IconDefinition>,
}
@ -57,6 +63,209 @@ pub struct IconDefinition {
pub path: SharedString,
}
const FILE_STEMS_BY_ICON_KEY: &[(&str, &[&str])] = &[
("docker", &["Dockerfile"]),
("ruby", &["Podfile"]),
("heroku", &["Procfile"]),
];
const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[
("astro", &["astro"]),
(
"audio",
&[
"aac", "flac", "m4a", "mka", "mp3", "ogg", "opus", "wav", "wma", "wv",
],
),
("backup", &["bak"]),
("bicep", &["bicep"]),
("bun", &["lockb"]),
("c", &["c", "h"]),
("code", &["handlebars", "metadata", "rkt", "scm"]),
("coffeescript", &["coffee"]),
(
"cpp",
&["c++", "cc", "cpp", "cxx", "hh", "hpp", "hxx", "inl"],
),
("crystal", &["cr", "ecr"]),
("csharp", &["cs"]),
("csproj", &["csproj"]),
("css", &["css", "pcss", "postcss"]),
("cue", &["cue"]),
("dart", &["dart"]),
("diff", &["diff"]),
(
"document",
&[
"doc", "docx", "mdx", "odp", "ods", "odt", "pdf", "ppt", "pptx", "rtf", "txt", "xls",
"xlsx",
],
),
("elixir", &["eex", "ex", "exs", "heex"]),
("elm", &["elm"]),
(
"erlang",
&[
"Emakefile",
"app.src",
"erl",
"escript",
"hrl",
"rebar.config",
"xrl",
"yrl",
],
),
(
"eslint",
&[
"eslint.config.cjs",
"eslint.config.cts",
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.mts",
"eslint.config.ts",
"eslintrc",
"eslintrc.js",
"eslintrc.json",
],
),
("font", &["otf", "ttf", "woff", "woff2"]),
("fsharp", &["fs"]),
("fsproj", &["fsproj"]),
("gitlab", &["gitlab-ci.yml"]),
("gleam", &["gleam"]),
("go", &["go", "mod", "work"]),
("graphql", &["gql", "graphql", "graphqls"]),
("haskell", &["hs"]),
("hcl", &["hcl"]),
("html", &["htm", "html"]),
(
"image",
&[
"avif", "bmp", "gif", "heic", "heif", "ico", "j2k", "jfif", "jp2", "jpeg", "jpg",
"jxl", "png", "psd", "qoi", "svg", "tiff", "webp",
],
),
("java", &["java"]),
("javascript", &["cjs", "js", "mjs"]),
("json", &["json"]),
("julia", &["jl"]),
("kotlin", &["kt"]),
("lock", &["lock"]),
("log", &["log"]),
("lua", &["lua"]),
("luau", &["luau"]),
("markdown", &["markdown", "md"]),
("metal", &["metal"]),
("nim", &["nim"]),
("nix", &["nix"]),
("ocaml", &["ml", "mli"]),
("php", &["php"]),
(
"prettier",
&[
"prettier.config.cjs",
"prettier.config.js",
"prettier.config.mjs",
"prettierignore",
"prettierrc",
"prettierrc.cjs",
"prettierrc.js",
"prettierrc.json",
"prettierrc.json5",
"prettierrc.mjs",
"prettierrc.toml",
"prettierrc.yaml",
"prettierrc.yml",
],
),
("prisma", &["prisma"]),
("python", &["py"]),
("r", &["r", "R"]),
("react", &["cjsx", "ctsx", "jsx", "mjsx", "mtsx", "tsx"]),
("roc", &["roc"]),
("ruby", &["rb"]),
("rust", &["rs"]),
("sass", &["sass", "scss"]),
("scala", &["scala", "sc"]),
("settings", &["conf", "ini", "yaml", "yml"]),
("solidity", &["sol"]),
(
"storage",
&[
"accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "jsonc",
"ldf", "mdb", "mdf", "myd", "myi", "pdb", "sav", "sdf", "sql", "sqlite", "tsv",
],
),
(
"stylelint",
&[
"stylelint.config.cjs",
"stylelint.config.js",
"stylelint.config.mjs",
"stylelintignore",
"stylelintrc",
"stylelintrc.cjs",
"stylelintrc.js",
"stylelintrc.json",
"stylelintrc.mjs",
"stylelintrc.yaml",
"stylelintrc.yml",
],
),
("svelte", &["svelte"]),
("swift", &["swift"]),
("tcl", &["tcl"]),
("template", &["hbs", "plist", "xml"]),
(
"terminal",
&[
"bash",
"bash_aliases",
"bash_logout",
"bash_profile",
"bashrc",
"fish",
"nu",
"profile",
"ps1",
"sh",
"zlogin",
"zsh",
"zsh_aliases",
"zsh_histfile",
"zsh_profile",
"zshenv",
"zshrc",
],
),
("terraform", &["tf", "tfvars"]),
("toml", &["toml"]),
("typescript", &["cts", "mts", "ts"]),
("v", &["v", "vsh", "vv"]),
(
"vcs",
&[
"COMMIT_EDITMSG",
"EDIT_DESCRIPTION",
"MERGE_MSG",
"NOTES_EDITMSG",
"TAG_EDITMSG",
"gitattributes",
"gitignore",
"gitkeep",
"gitmodules",
],
),
("vbproj", &["vbproj"]),
("video", &["avi", "m4v", "mkv", "mov", "mp4", "webm", "wmv"]),
("vs_sln", &["sln"]),
("vs_suo", &["suo"]),
("vue", &["vue"]),
("zig", &["zig"]),
];
/// A mapping of a file type identifier to its corresponding icon.
const FILE_ICONS: &[(&str, &str)] = &[
("astro", "icons/file_icons/astro.svg"),
@ -141,12 +350,25 @@ const FILE_ICONS: &[(&str, &str)] = &[
("zig", "icons/file_icons/zig.svg"),
];
/// Returns a mapping of file associations to icon keys.
fn icon_keys_by_association(
associations_by_icon_key: &[(&str, &[&str])],
) -> HashMap<String, String> {
let mut icon_keys_by_association = HashMap::default();
for (icon_key, associations) in associations_by_icon_key {
for association in *associations {
icon_keys_by_association.insert(association.to_string(), icon_key.to_string());
}
}
icon_keys_by_association
}
/// 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 {
static DEFAULT_ICON_THEME: LazyLock<Arc<IconTheme>> = LazyLock::new(|| {
Arc::new(IconTheme {
id: "zed".into(),
name: DEFAULT_ICON_THEME_NAME.into(),
appearance: Appearance::Dark,
@ -158,6 +380,8 @@ pub fn default_icon_theme() -> IconTheme {
collapsed: Some("icons/file_icons/chevron_right.svg".into()),
expanded: Some("icons/file_icons/chevron_down.svg".into()),
},
file_stems: icon_keys_by_association(FILE_STEMS_BY_ICON_KEY),
file_suffixes: icon_keys_by_association(FILE_SUFFIXES_BY_ICON_KEY),
file_icons: HashMap::from_iter(FILE_ICONS.into_iter().map(|(ty, path)| {
(
ty.to_string(),
@ -166,5 +390,10 @@ pub fn default_icon_theme() -> IconTheme {
},
)
})),
}
})
});
/// Returns the default icon theme.
pub fn default_icon_theme() -> Arc<IconTheme> {
DEFAULT_ICON_THEME.clone()
}

View file

@ -23,6 +23,10 @@ pub struct IconThemeContent {
#[serde(default)]
pub chevron_icons: ChevronIconsContent,
#[serde(default)]
pub file_stems: HashMap<String, String>,
#[serde(default)]
pub file_suffixes: HashMap<String, String>,
#[serde(default)]
pub file_icons: HashMap<String, IconDefinitionContent>,
}

View file

@ -11,8 +11,8 @@ use parking_lot::RwLock;
use util::ResultExt;
use crate::{
read_icon_theme, read_user_theme, refine_theme_family, Appearance, AppearanceContent,
ChevronIcons, DirectoryIcons, IconDefinition, IconTheme, Theme, ThemeFamily,
default_icon_theme, read_icon_theme, read_user_theme, refine_theme_family, Appearance,
AppearanceContent, ChevronIcons, DirectoryIcons, IconDefinition, IconTheme, Theme, ThemeFamily,
ThemeFamilyContent, DEFAULT_ICON_THEME_NAME,
};
@ -80,10 +80,11 @@ impl ThemeRegistry {
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.name.clone(),
Arc::new(default_icon_theme),
);
registry
.state
.write()
.icon_themes
.insert(default_icon_theme.name.clone(), default_icon_theme);
registry
}
@ -263,8 +264,16 @@ impl ThemeRegistry {
.into()
};
let default_icon_theme = default_icon_theme();
let mut state = self.state.write();
for icon_theme in icon_theme_family.themes {
let mut file_stems = default_icon_theme.file_stems.clone();
file_stems.extend(icon_theme.file_stems);
let mut file_suffixes = default_icon_theme.file_suffixes.clone();
file_suffixes.extend(icon_theme.file_suffixes);
let icon_theme = IconTheme {
id: uuid::Uuid::new_v4().to_string(),
name: icon_theme.name.into(),
@ -280,6 +289,8 @@ impl ThemeRegistry {
collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path),
expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path),
},
file_stems,
file_suffixes,
file_icons: icon_theme
.file_icons
.into_iter()