From 95befb652c43697d819ab2315f0d1dae0082710c Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Sun, 17 Aug 2025 23:21:22 +1200 Subject: [PATCH 1/2] feat: named folder icons support Adds a "named_directory_icons" field to the icon theme that can be used to specify a collection of icons for collapsed and expanded folders based on the folder name the value specified is the key into "file_icons" for the icon to use --- .../agent_ui/src/acp/completion_provider.rs | 3 +- crates/agent_ui/src/acp/thread_view.rs | 2 +- .../src/context_picker/completion_provider.rs | 3 +- .../src/context_picker/file_context_picker.rs | 2 +- crates/file_finder/src/open_path_prompt.rs | 10 ++++- crates/file_icons/src/file_icons.rs | 39 +++++++++++++++++-- crates/outline_panel/src/outline_panel.rs | 4 +- crates/project_panel/src/project_panel.rs | 7 +++- crates/theme/src/icon_theme.rs | 15 +++++++ crates/theme/src/icon_theme_schema.rs | 10 +++++ crates/theme/src/registry.rs | 16 +++++++- 11 files changed, 96 insertions(+), 15 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index d8f452afa5..2f165d5139 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -82,7 +82,8 @@ impl ContextPickerCompletionProvider { }; let crease_icon_path = if is_directory { - FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into()) + FileIcons::get_folder_icon(false, &file_name, cx) + .unwrap_or_else(|| IconName::Folder.path().into()) } else { FileIcons::get_icon(Path::new(&full_path), cx) .unwrap_or_else(|| IconName::File.path().into()) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c811878c21..75dafc8fd4 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -594,7 +594,7 @@ impl AcpThreadView { for (range, project_path, filename) in mentions { let crease_icon_path = if project_path.path.is_dir() { - FileIcons::get_folder_icon(false, cx) + FileIcons::get_folder_icon(false, &filename, cx) .unwrap_or_else(|| IconName::Folder.path().into()) } else { FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx) diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index 8123b3437d..4a685dad53 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -605,7 +605,8 @@ impl ContextPickerCompletionProvider { }; let crease_icon_path = if is_directory { - FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into()) + FileIcons::get_folder_icon(false, &file_name, cx) + .unwrap_or_else(|| IconName::Folder.path().into()) } else { FileIcons::get_icon(Path::new(&full_path), cx) .unwrap_or_else(|| IconName::File.path().into()) diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index eaf9ed16d6..b2bc0f9b37 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -332,7 +332,7 @@ pub fn render_file_context_entry( }); let file_icon = if is_directory { - FileIcons::get_folder_icon(false, cx) + FileIcons::get_folder_icon(false, &file_name, cx) } else { FileIcons::get_icon(&path, cx) } diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index 68ba7a78b5..10f237ac8c 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -633,10 +633,16 @@ impl PickerDelegate for OpenPathDelegate { if !settings.file_icons { return None; } + let path = path::Path::new(&candidate.path.string); + let icon = if candidate.is_dir { - FileIcons::get_folder_icon(false, cx)? + let name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + + FileIcons::get_folder_icon(false, name, cx)? } else { - let path = path::Path::new(&candidate.path.string); FileIcons::get_icon(&path, cx)? }; Some(Icon::from_path(icon).color(Color::Muted)) diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 2f159771b1..64361f6dec 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -83,18 +83,49 @@ impl FileIcons { }) } - pub fn get_folder_icon(expanded: bool, cx: &App) -> Option { - fn get_folder_icon(icon_theme: &Arc, expanded: bool) -> Option { + pub fn get_folder_icon(expanded: bool, name: &str, cx: &App) -> Option { + fn get_folder_icon( + icon_theme: &Arc, + name: &str, + expanded: bool, + ) -> Option { if expanded { + let named_icon = icon_theme + .named_directory_icons + .expanded + .get(name) + .and_then(|key| icon_theme.file_icons.get(key)) + .map(|icon_definition| icon_definition.path.clone()); + + if let Some(named_icon) = named_icon { + return Some(named_icon); + } + icon_theme.directory_icons.expanded.clone() } else { + let named_icon = icon_theme + .named_directory_icons + .collapsed + .get(name) + .and_then(|key| icon_theme.file_icons.get(key)) + .map(|icon_definition| icon_definition.path.clone()); + + if let Some(named_icon) = named_icon { + return Some(named_icon); + } + icon_theme.directory_icons.collapsed.clone() } } - get_folder_icon(&ThemeSettings::get_global(cx).active_icon_theme, expanded).or_else(|| { + get_folder_icon( + &ThemeSettings::get_global(cx).active_icon_theme, + name, + expanded, + ) + .or_else(|| { Self::default_icon_theme(cx) - .and_then(|icon_theme| get_folder_icon(&icon_theme, expanded)) + .and_then(|icon_theme| get_folder_icon(&icon_theme, name, expanded)) }) } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 1cda3897ec..d56a413ee5 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2325,7 +2325,7 @@ impl OutlinePanel { is_active, ); let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, cx) + FileIcons::get_folder_icon(is_expanded, &name, cx) } else { FileIcons::get_chevron_icon(is_expanded, cx) } @@ -2422,7 +2422,7 @@ impl OutlinePanel { .unwrap_or_default(); let color = entry_git_aware_label_color(git_status, is_ignored, is_active); let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, cx) + FileIcons::get_folder_icon(is_expanded, &name, cx) } else { FileIcons::get_chevron_icon(is_expanded, cx) } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 967df41e23..f4ec171ca9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4662,7 +4662,12 @@ impl ProjectPanel { } _ => { if show_folder_icons { - FileIcons::get_folder_icon(is_expanded, cx) + let name = entry + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + FileIcons::get_folder_icon(is_expanded, name, cx) } else { FileIcons::get_chevron_icon(is_expanded, cx) } diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 5bd69c1733..b52870f9b6 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -28,6 +28,8 @@ pub struct IconTheme { pub appearance: Appearance, /// The icons used for directories. pub directory_icons: DirectoryIcons, + /// The icons used for named directories. + pub named_directory_icons: NamedDirectoryIcons, /// The icons used for chevrons. pub chevron_icons: ChevronIcons, /// The mapping of file stems to their associated icon keys. @@ -47,6 +49,15 @@ pub struct DirectoryIcons { pub expanded: Option, } +/// The icons used for directories with specific names +#[derive(Debug, PartialEq)] +pub struct NamedDirectoryIcons { + /// The paths for icons to use for collapsed directories. + pub collapsed: HashMap, + /// The paths for icons to use for expanded directories. + pub expanded: HashMap, +} + /// The icons used for chevrons. #[derive(Debug, PartialEq)] pub struct ChevronIcons { @@ -392,6 +403,10 @@ static DEFAULT_ICON_THEME: LazyLock> = LazyLock::new(|| { collapsed: Some("icons/file_icons/folder.svg".into()), expanded: Some("icons/file_icons/folder_open.svg".into()), }, + named_directory_icons: NamedDirectoryIcons { + collapsed: Default::default(), + expanded: Default::default(), + }, chevron_icons: ChevronIcons { collapsed: Some("icons/file_icons/chevron_right.svg".into()), expanded: Some("icons/file_icons/chevron_down.svg".into()), diff --git a/crates/theme/src/icon_theme_schema.rs b/crates/theme/src/icon_theme_schema.rs index f73938bc5c..eb53c185dd 100644 --- a/crates/theme/src/icon_theme_schema.rs +++ b/crates/theme/src/icon_theme_schema.rs @@ -21,6 +21,8 @@ pub struct IconThemeContent { #[serde(default)] pub directory_icons: DirectoryIconsContent, #[serde(default)] + pub named_directory_icons: NamedDirectoryIconsContent, + #[serde(default)] pub chevron_icons: ChevronIconsContent, #[serde(default)] pub file_stems: HashMap, @@ -36,6 +38,14 @@ pub struct DirectoryIconsContent { pub expanded: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct NamedDirectoryIconsContent { + #[serde(default)] + pub collapsed: HashMap, + #[serde(default)] + pub expanded: HashMap, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ChevronIconsContent { pub collapsed: Option, diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index f0f0776582..0c369a38ff 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -13,8 +13,8 @@ use util::ResultExt; use crate::{ Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons, - IconDefinition, IconTheme, Theme, ThemeFamily, ThemeFamilyContent, default_icon_theme, - read_icon_theme, read_user_theme, refine_theme_family, + IconDefinition, IconTheme, NamedDirectoryIcons, Theme, ThemeFamily, ThemeFamilyContent, + default_icon_theme, read_icon_theme, read_user_theme, refine_theme_family, }; /// The metadata for a theme. @@ -298,6 +298,14 @@ impl ThemeRegistry { let mut file_suffixes = default_icon_theme.file_suffixes.clone(); file_suffixes.extend(icon_theme.file_suffixes); + let mut named_directory_icons_expanded = + default_icon_theme.named_directory_icons.expanded.clone(); + named_directory_icons_expanded.extend(icon_theme.named_directory_icons.expanded); + + let mut named_directory_icons_collapsed = + default_icon_theme.named_directory_icons.collapsed.clone(); + named_directory_icons_collapsed.extend(icon_theme.named_directory_icons.collapsed); + let icon_theme = IconTheme { id: uuid::Uuid::new_v4().to_string(), name: icon_theme.name.into(), @@ -309,6 +317,10 @@ impl ThemeRegistry { collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path), expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path), }, + named_directory_icons: NamedDirectoryIcons { + collapsed: named_directory_icons_collapsed, + expanded: named_directory_icons_expanded, + }, chevron_icons: ChevronIcons { collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path), expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path), From 965725e08a29ecef4cb3deea6449c21e842c4964 Mon Sep 17 00:00:00 2001 From: Jacobtread Date: Sun, 17 Aug 2025 23:58:02 +1200 Subject: [PATCH 2/2] fix: change removed by merge conflict Add back a change that was removed when merging to get past a conflict that changed some code significantly --- crates/acp_thread/src/mention.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index b9b021c4ca..07bc527ff8 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -143,7 +143,12 @@ impl MentionUri { is_directory, } => { if *is_directory { - FileIcons::get_folder_icon(false, cx) + let name = abs_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + + FileIcons::get_folder_icon(false, name, cx) .unwrap_or_else(|| IconName::Folder.path().into()) } else { FileIcons::get_icon(&abs_path, cx)