From 39bd03b92d24b3f2b145bdd0c2f638e8fc053daf Mon Sep 17 00:00:00 2001 From: Jacob <33708767+jacobtread@users.noreply.github.com> Date: Fri, 3 Oct 2025 22:41:59 +1300 Subject: [PATCH] file_icons: Add support for multiple file extensions (#36342) 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 Release Notes: - Added support for icons on file extensions such as stories.tsx and stories.svelte --- crates/file_icons/src/file_icons.rs | 9 +++++++ crates/util/src/paths.rs | 42 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 327b1451523748aa91356bdb5854e2ef165a5085..b7322a717d20f232cad7b9239a46a5eb0e124abd 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -52,6 +52,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() { diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f9fe0ab33da2dc97b7bb59f9a0c109d0f96a4536..3fe6a526c866ff132b8f77d7efb4673bcc2a65fa 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,4 +1,5 @@ use globset::{Glob, GlobSet, GlobSetBuilder}; +use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; @@ -60,6 +61,7 @@ pub trait PathExt { } } fn local_to_wsl(&self) -> Option; + fn multiple_extensions(&self) -> Option; } impl> PathExt for T { @@ -130,6 +132,27 @@ impl> PathExt for T { Some(new_path.into()) } + + /// 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(".")) + } } /// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On @@ -1727,4 +1750,23 @@ mod tests { assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less); assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater); } + + #[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())); + } }