From 8c0dad104fb36428ced10091acb9a23066777266 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Sun, 17 Aug 2025 18:12:05 +1200 Subject: [PATCH] feat: support multiple extensions for file icons Currently most icon theme extensions already support file types like stories.tsx and stories.svelte. However within Zed itself these file type overrides are not supported yet. This change adds support for those --- crates/file_icons/src/file_icons.rs | 10 +++++++ crates/util/src/paths.rs | 42 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 2f159771b1..2eebb40385 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -42,6 +42,15 @@ impl FileIcons { } } + // handle cases where the file extension is made up of multiple important + // parts (e.g Component.stories.tsx) that refer to an alternative icon style + if let Some(suffix) = path.multiple_extensions() { + let maybe_path = get_icon_from_suffix(suffix.as_str()); + if maybe_path.is_some() { + return maybe_path; + } + } + // primary case: check if the files extension or the hidden file name // matches some icon path if let Some(suffix) = path.extension_or_hidden_file_name() { @@ -62,6 +71,7 @@ impl FileIcons { return maybe_path; } } + return this.get_icon_for_type("default", cx); } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 585f2b08aa..0a2f7ffff6 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -8,6 +8,7 @@ use std::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; +use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -22,6 +23,7 @@ pub fn home_dir() -> &'static PathBuf { pub trait PathExt { fn compact(&self) -> PathBuf; fn extension_or_hidden_file_name(&self) -> Option<&str>; + fn multiple_extensions(&self) -> Option; fn to_sanitized_string(&self) -> String; fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result where @@ -86,6 +88,27 @@ impl> PathExt for T { .or_else(|| path.file_stem()?.to_str()) } + /// Returns a file's "full" joined collection of extensions, in the case where a file does not + /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx) + /// + /// Will provide back the extensions joined together such as tar.gz or stories.tsx + fn multiple_extensions(&self) -> Option { + let path = self.as_ref(); + let file_name = path.file_name()?.to_str()?; + + let parts: Vec<&str> = file_name + .split('.') + // Skip the part with the file name extension + .skip(1) + .collect(); + + if parts.len() < 2 { + return None; + } + + Some(parts.into_iter().join(".")) + } + /// Returns a sanitized string representation of the path. /// Note, on Windows, this assumes that the path is a valid UTF-8 string and /// is not a UNC path. @@ -1012,6 +1035,25 @@ mod tests { assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js")); } + #[test] + fn test_multiple_extensions() { + // No extensions + let path = Path::new("/a/b/c/file_name"); + assert_eq!(path.multiple_extensions(), None); + + // Only one extension + let path = Path::new("/a/b/c/file_name.tsx"); + assert_eq!(path.multiple_extensions(), None); + + // Stories sample extension + let path = Path::new("/a/b/c/file_name.stories.tsx"); + assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string())); + + // Longer sample extension + let path = Path::new("/a/b/c/long.app.tar.gz"); + assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string())); + } + #[test] fn edge_of_glob() { let path = Path::new("/work/node_modules");