file_finder: Display file icons (#18091)

Daste and Marshall Bowers created

This PR adds file icons (like in tabs, the project panel and tab
switcher) to the file finder popup.

It's similar to [tab_switcher
icons](https://github.com/zed-industries/zed/pull/17115), but simpler,
because we're only dealing with actual files.

Release Notes:

- Added icons to the file finder.

Screenshot:

![image](https://github.com/user-attachments/assets/bd6a54c1-cdbd-415a-9a82-0cc7a0bb6ca2)

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

Cargo.lock                                     |  3 ++
assets/settings/default.json                   |  5 +++
crates/file_finder/Cargo.toml                  |  3 ++
crates/file_finder/src/file_finder.rs          | 21 ++++++++++++++-
crates/file_finder/src/file_finder_settings.rs | 27 ++++++++++++++++++++
5 files changed, 57 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4326,6 +4326,7 @@ dependencies = [
  "ctor",
  "editor",
  "env_logger",
+ "file_icons",
  "futures 0.3.30",
  "fuzzy",
  "gpui",
@@ -4333,7 +4334,9 @@ dependencies = [
  "menu",
  "picker",
  "project",
+ "schemars",
  "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "text",

assets/settings/default.json 🔗

@@ -496,6 +496,11 @@
     // Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
     "enable_preview_from_code_navigation": false
   },
+  // Settings related to the file finder.
+  "file_finder": {
+    // Whether to show file icons in the file finder.
+    "file_icons": true
+  },
   // Whether or not to remove any trailing whitespace from lines of a buffer
   // before saving it.
   "remove_trailing_whitespace_on_save": true,

crates/file_finder/Cargo.toml 🔗

@@ -16,14 +16,17 @@ doctest = false
 anyhow.workspace = true
 collections.workspace = true
 editor.workspace = true
+file_icons.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 menu.workspace = true
 picker.workspace = true
 project.workspace = true
+schemars.workspace = true
 settings.workspace = true
 serde.workspace = true
+serde_derive.workspace = true
 text.workspace = true
 theme.workspace = true
 ui.workspace = true

crates/file_finder/src/file_finder.rs 🔗

@@ -1,11 +1,14 @@
 #[cfg(test)]
 mod file_finder_tests;
 
+mod file_finder_settings;
 mod new_path_prompt;
 mod open_path_prompt;
 
 use collections::HashMap;
 use editor::{scroll::Autoscroll, Bias, Editor};
+use file_finder_settings::FileFinderSettings;
+use file_icons::FileIcons;
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
     actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
@@ -39,7 +42,12 @@ pub struct FileFinder {
     init_modifiers: Option<Modifiers>,
 }
 
+pub fn init_settings(cx: &mut AppContext) {
+    FileFinderSettings::register(cx);
+}
+
 pub fn init(cx: &mut AppContext) {
+    init_settings(cx);
     cx.observe_new_views(FileFinder::register).detach();
     cx.observe_new_views(NewPathPrompt::register).detach();
     cx.observe_new_views(OpenPathPrompt::register).detach();
@@ -1041,12 +1049,14 @@ impl PickerDelegate for FileFinderDelegate {
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Option<Self::ListItem> {
+        let settings = FileFinderSettings::get_global(cx);
+
         let path_match = self
             .matches
             .get(ix)
             .expect("Invalid matches state: no element for index {ix}");
 
-        let icon = match &path_match {
+        let history_icon = match &path_match {
             Match::History { .. } => Icon::new(IconName::HistoryRerun)
                 .color(Color::Muted)
                 .size(IconSize::Small)
@@ -1059,10 +1069,17 @@ impl PickerDelegate for FileFinderDelegate {
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match, cx, ix);
 
+        let file_icon = if settings.file_icons {
+            FileIcons::get_icon(Path::new(&file_name), cx).map(Icon::from_path)
+        } else {
+            None
+        };
+
         Some(
             ListItem::new(ix)
                 .spacing(ListItemSpacing::Sparse)
-                .end_slot::<AnyElement>(Some(icon))
+                .start_slot::<Icon>(file_icon)
+                .end_slot::<AnyElement>(history_icon)
                 .inset(true)
                 .selected(selected)
                 .child(

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -0,0 +1,27 @@
+use anyhow::Result;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
+
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
+pub struct FileFinderSettings {
+    pub file_icons: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct FileFinderSettingsContent {
+    /// Whether to show file icons in the file finder.
+    ///
+    /// Default: true
+    pub file_icons: Option<bool>,
+}
+
+impl Settings for FileFinderSettings {
+    const KEY: Option<&'static str> = Some("file_finder");
+
+    type FileContent = FileFinderSettingsContent;
+
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::AppContext) -> Result<Self> {
+        sources.json_merge()
+    }
+}