diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 42c00fb12d..a3d5240508 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() { @@ -72,6 +81,7 @@ impl FileIcons { return maybe_path; } } + this.get_icon_for_type("default", cx) } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 1192b14812..eddc9fe8cf 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; @@ -20,6 +21,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 @@ -84,6 +86,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. @@ -1171,6 +1194,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");