Add suffix based file icons

Mikayla Maki created

Change summary

crates/language/src/language.rs           | 19 ++++++++++
crates/project/src/project.rs             |  5 ++
crates/project_panel/src/project_panel.rs | 45 ++++++++++++++++++------
crates/theme/src/theme.rs                 |  3 +
crates/zed/src/languages/toml/config.toml |  1 
styles/src/style_tree/project_panel.ts    |  3 +
6 files changed, 62 insertions(+), 14 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -342,6 +342,8 @@ pub struct LanguageConfig {
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
     #[serde(default)]
     pub overrides: HashMap<String, LanguageConfigOverride>,
+    #[serde(default)]
+    pub icon_path: Option<Arc<str>>,
 }
 
 #[derive(Debug, Default)]
@@ -408,6 +410,7 @@ impl Default for LanguageConfig {
             line_comment: Default::default(),
             block_comment: Default::default(),
             overrides: Default::default(),
+            icon_path: Default::default(),
         }
     }
 }
@@ -752,6 +755,22 @@ impl LanguageRegistry {
         self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name)
     }
 
+    pub fn icon_for_suffix(
+        self: &Arc<Self>,
+        suffix: &str,
+    ) -> Option<Arc<str>> {
+        let state = self.state.read();
+        state.available_languages
+            .iter()
+            .find(|langauge| {
+                langauge.config.path_suffixes.iter().any(|s| s == suffix)
+            })
+            .map(|language| {
+                language.config.icon_path.clone()
+            })
+            .flatten()
+    }
+
     pub fn language_for_name_or_extension(
         self: &Arc<Self>,
         string: &str,

crates/project/src/project.rs 🔗

@@ -2474,6 +2474,11 @@ impl Project {
         })
     }
 
+    pub fn icon_for_path(&self, path: &Path) -> Option<Arc<str>> {
+        self.languages
+            .icon_for_suffix(path.extension()?.to_str()?)
+    }
+
     fn detect_language_for_buffer(
         &mut self,
         buffer_handle: &ModelHandle<Buffer>,

crates/project_panel/src/project_panel.rs 🔗

@@ -44,6 +44,7 @@ use workspace::{
 
 const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
 const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
+const TEXT_FILE_ASSET: &'static str = "icons/radix/file-text.svg";
 
 pub struct ProjectPanel {
     project: ModelHandle<Project>,
@@ -94,6 +95,7 @@ pub enum ClipboardEntry {
 #[derive(Debug, PartialEq, Eq)]
 pub struct EntryDetails {
     filename: String,
+    icon: Option<Arc<str>>,
     path: Arc<Path>,
     depth: usize,
     kind: EntryKind,
@@ -1180,6 +1182,15 @@ impl ProjectPanel {
                 for entry in visible_worktree_entries[entry_range].iter() {
                     let status = git_status_setting.then(|| entry.git_status).flatten();
 
+                    let icon = match entry.kind {
+                        EntryKind::File(_) => self
+                            .project
+                            .read(cx)
+                            .icon_for_path(&entry.path)
+                            .or_else(|| Some(TEXT_FILE_ASSET.into())),
+                        _ => None,
+                    };
+
                     let mut details = EntryDetails {
                         filename: entry
                             .path
@@ -1187,6 +1198,7 @@ impl ProjectPanel {
                             .unwrap_or(root_name)
                             .to_string_lossy()
                             .to_string(),
+                        icon,
                         path: entry.path.clone(),
                         depth: entry.path.components().count(),
                         kind: entry.kind,
@@ -1254,23 +1266,32 @@ impl ProjectPanel {
             .unwrap_or(style.text.color);
 
         Flex::row()
-            .with_child(
-                if kind.is_dir() {
-                    if details.is_expanded {
-                        Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
-                    } else {
-                        Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
-                    }
-                    .constrained()
+            .with_child(if kind.is_dir() {
+                if details.is_expanded {
+                    Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
+                } else {
+                    Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
+                }
+                .constrained()
+                .with_max_width(style.directory_icon_size)
+                .with_max_height(style.directory_icon_size)
+                .aligned()
+                .constrained()
+                .with_width(style.directory_icon_size)
+            } else {
+                if let Some(icon) = &details.icon {
+                    Svg::new(icon.to_string())
+                        .with_color(style.icon_color)
+                        .constrained()
                 } else {
                     Empty::new().constrained()
                 }
-                .with_max_width(style.icon_size)
-                .with_max_height(style.icon_size)
+                .with_max_width(style.file_icon_size)
+                .with_max_height(style.file_icon_size)
                 .aligned()
                 .constrained()
-                .with_width(style.icon_size),
-            )
+                .with_width(style.file_icon_size)
+            })
             .with_child(if show_editor && editor.is_some() {
                 ChildView::new(editor.as_ref().unwrap(), cx)
                     .contained()

crates/theme/src/theme.rs 🔗

@@ -481,7 +481,8 @@ pub struct ProjectPanelEntry {
     pub container: ContainerStyle,
     pub text: TextStyle,
     pub icon_color: Color,
-    pub icon_size: f32,
+    pub directory_icon_size: f32,
+    pub file_icon_size: f32,
     pub icon_spacing: f32,
     pub status: EntryStatus,
 }

styles/src/style_tree/project_panel.ts 🔗

@@ -47,7 +47,8 @@ export default function project_panel(): any {
             height: 22,
             background: background(theme.middle),
             icon_color: foreground(theme.middle, "variant"),
-            icon_size: 7,
+            directory_icon_size: 7,
+            file_icon_size: 14,
             icon_spacing: 5,
             text: text(theme.middle, "sans", "variant", { size: "sm" }),
             status: {