tab_switcher: Use git-aware colors for file icons (#18733)

Daste and Marshall Bowers created

Release Notes:

- Fixed tab switcher icons not respecting the `tabs.git_status` setting.

Fixes an issue mentioned in
https://github.com/zed-industries/zed/pull/17115#issuecomment-2378966170
- file icons in the tab switcher weren't colored according to git
status, even if `tabs.git_status` was set to true.

I used a similar approach I saw in other places of the project to get
the project entry and its git status, but maybe we could move the
coloring logic entirely to `tab_icon()`? Wouldn't this break anything?

---------

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

Change summary

Cargo.lock                              |  1 
crates/tab_switcher/Cargo.toml          |  5 +-
crates/tab_switcher/src/tab_switcher.rs | 41 +++++++++++++++++++++++---
3 files changed, 39 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11412,6 +11412,7 @@ dependencies = [
  "project",
  "serde",
  "serde_json",
+ "settings",
  "theme",
  "ui",
  "util",

crates/tab_switcher/Cargo.toml 🔗

@@ -14,10 +14,13 @@ doctest = false
 
 [dependencies]
 collections.workspace = true
+editor.workspace = true
 gpui.workspace = true
 menu.workspace = true
 picker.workspace = true
+project.workspace = true
 serde.workspace = true
+settings.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
@@ -25,11 +28,9 @@ workspace.workspace = true
 [dev-dependencies]
 anyhow.workspace = true
 ctor.workspace = true
-editor.workspace = true
 env_logger.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
-project.workspace = true
 serde_json.workspace = true
 theme = { workspace = true, features = ["test-support"] }
 workspace = { workspace = true, features = ["test-support"] }

crates/tab_switcher/src/tab_switcher.rs 🔗

@@ -2,18 +2,21 @@
 mod tab_switcher_tests;
 
 use collections::HashMap;
+use editor::items::entry_git_aware_label_color;
 use gpui::{
     actions, impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EntityId,
-    EventEmitter, FocusHandle, FocusableView, Modifiers, ModifiersChangedEvent, MouseButton,
+    EventEmitter, FocusHandle, FocusableView, Model, Modifiers, ModifiersChangedEvent, MouseButton,
     MouseUpEvent, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
+use project::Project;
 use serde::Deserialize;
+use settings::Settings;
 use std::sync::Arc;
 use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
 use util::ResultExt;
 use workspace::{
-    item::{ItemHandle, TabContentParams},
+    item::{ItemHandle, ItemSettings, TabContentParams},
     pane::{render_item_indicator, tab_details, Event as PaneEvent},
     ModalView, Pane, SaveIntent, Workspace,
 };
@@ -76,8 +79,10 @@ impl TabSwitcher {
             })
         }
 
+        let project = workspace.project().clone();
         workspace.toggle_modal(cx, |cx| {
-            let delegate = TabSwitcherDelegate::new(action, cx.view().downgrade(), weak_pane, cx);
+            let delegate =
+                TabSwitcherDelegate::new(project, action, cx.view().downgrade(), weak_pane, cx);
             TabSwitcher::new(delegate, cx)
         });
     }
@@ -147,11 +152,13 @@ pub struct TabSwitcherDelegate {
     tab_switcher: WeakView<TabSwitcher>,
     selected_index: usize,
     pane: WeakView<Pane>,
+    project: Model<Project>,
     matches: Vec<TabMatch>,
 }
 
 impl TabSwitcherDelegate {
     fn new(
+        project: Model<Project>,
         action: &Toggle,
         tab_switcher: WeakView<TabSwitcher>,
         pane: WeakView<Pane>,
@@ -163,6 +170,7 @@ impl TabSwitcherDelegate {
             tab_switcher,
             selected_index: 0,
             pane,
+            project,
             matches: Vec::new(),
         }
     }
@@ -341,6 +349,29 @@ impl PickerDelegate for TabSwitcherDelegate {
             preview: tab_match.preview,
         };
         let label = tab_match.item.tab_content(params, cx);
+
+        let icon = tab_match.item.tab_icon(cx).map(|icon| {
+            let git_status_color = ItemSettings::get_global(cx)
+                .git_status
+                .then(|| {
+                    tab_match
+                        .item
+                        .project_path(cx)
+                        .as_ref()
+                        .and_then(|path| self.project.read(cx).entry_for_path(path, cx))
+                        .map(|entry| {
+                            entry_git_aware_label_color(
+                                entry.git_status,
+                                entry.is_ignored,
+                                selected,
+                            )
+                        })
+                })
+                .flatten();
+
+            icon.color(git_status_color.unwrap_or_default())
+        });
+
         let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx);
         let indicator_color = if let Some(ref indicator) = indicator {
             indicator.color
@@ -378,9 +409,7 @@ impl PickerDelegate for TabSwitcherDelegate {
                 .inset(true)
                 .selected(selected)
                 .child(h_flex().w_full().child(label))
-                .when_some(tab_match.item.tab_icon(cx), |el, icon| {
-                    el.start_slot(div().child(icon))
-                })
+                .start_slot::<Icon>(icon)
                 .map(|el| {
                     if self.selected_index == ix {
                         el.end_slot::<AnyElement>(close_button)