tasks: Use icons instead of secondary text in a modal (#10264)

Piotr Osiewicz created

Before:

![image](https://github.com/zed-industries/zed/assets/24362066/feae9c98-37d4-437d-965a-047d2e089a7b)
After:

![image](https://github.com/zed-industries/zed/assets/24362066/43e48985-5aba-44d9-9128-cfafb9b61fd4)

Release Notes:

- N/A

Change summary

Cargo.lock                          |  1 
crates/file_icons/src/file_icons.rs | 24 +++++-------
crates/tasks_ui/Cargo.toml          |  2 +
crates/tasks_ui/src/modal.rs        | 57 +++++++++++++++---------------
4 files changed, 42 insertions(+), 42 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9496,6 +9496,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "editor",
+ "file_icons",
  "fuzzy",
  "gpui",
  "itertools 0.11.0",

crates/file_icons/src/file_icons.rs 🔗

@@ -54,18 +54,20 @@ impl FileIcons {
             let suffix = path.icon_stem_or_suffix()?;
 
             if let Some(type_str) = this.stems.get(suffix) {
-                return this
-                    .types
-                    .get(type_str)
-                    .map(|type_config| type_config.icon.clone());
+                return this.get_type_icon(type_str);
             }
 
             this.suffixes
                 .get(suffix)
-                .and_then(|type_str| this.types.get(type_str))
-                .map(|type_config| type_config.icon.clone())
+                .and_then(|type_str| this.get_type_icon(type_str))
         })
-        .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+        .or_else(|| this.get_type_icon("default"))
+    }
+
+    pub fn get_type_icon(&self, typ: &str) -> Option<Arc<str>> {
+        self.types
+            .get(typ)
+            .map(|type_config| type_config.icon.clone())
     }
 
     pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
@@ -77,9 +79,7 @@ impl FileIcons {
             COLLAPSED_DIRECTORY_TYPE
         };
 
-        this.types
-            .get(key)
-            .map(|type_config| type_config.icon.clone())
+        this.get_type_icon(key)
     }
 
     pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
@@ -91,8 +91,6 @@ impl FileIcons {
             COLLAPSED_CHEVRON_TYPE
         };
 
-        this.types
-            .get(key)
-            .map(|type_config| type_config.icon.clone())
+        this.get_type_icon(key)
     }
 }

crates/tasks_ui/Cargo.toml 🔗

@@ -11,6 +11,7 @@ workspace = true
 [dependencies]
 anyhow.workspace = true
 editor.workspace = true
+file_icons.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 picker.workspace = true
@@ -23,6 +24,7 @@ workspace.workspace = true
 language.workspace = true
 itertools.workspace = true
 
+
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }

crates/tasks_ui/src/modal.rs 🔗

@@ -3,22 +3,19 @@ use std::sync::Arc;
 use crate::{active_item_selection_properties, schedule_task};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
-    Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
-    VisualContext, WeakView,
-};
-use picker::{
-    highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
-    Picker, PickerDelegate,
+    impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, Global,
+    InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, View,
+    ViewContext, VisualContext, WeakView,
 };
+use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
 use project::{Inventory, TaskSourceKind};
 use task::{oneshot_source::OneshotSource, Task, TaskContext};
 use ui::{
-    div, v_flex, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, IconButton,
+    div, v_flex, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton,
     IconButtonShape, IconName, IconSize, ListItem, ListItemSpacing, RenderOnce, Selectable,
     Tooltip, WindowContext,
 };
-use util::{paths::PathExt, ResultExt};
+use util::ResultExt;
 use workspace::{ModalView, Workspace};
 
 use serde::Deserialize;
@@ -285,34 +282,31 @@ impl PickerDelegate for TasksModalDelegate {
         cx: &mut ViewContext<picker::Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let candidates = self.candidates.as_ref()?;
-        let hit = &self.matches.get(ix)?;
-        let (source_kind, _) = &candidates.get(hit.candidate_id)?;
-        let details = match source_kind {
-            TaskSourceKind::UserInput => "user input".to_string(),
-            TaskSourceKind::Language { name } => format!("{name} language"),
-            TaskSourceKind::Worktree { abs_path, .. } | TaskSourceKind::AbsPath(abs_path) => {
-                abs_path.compact().to_string_lossy().to_string()
-            }
+        let hit = &self.matches[ix];
+        let (source_kind, _) = &candidates[hit.candidate_id];
+        let language_name = if let TaskSourceKind::Language { name } = source_kind {
+            Some(name)
+        } else {
+            None
         };
 
-        let highlighted_location = HighlightedMatchWithPaths {
-            match_label: HighlightedText {
-                text: hit.string.clone(),
-                highlight_positions: hit.positions.clone(),
-                char_count: hit.string.chars().count(),
-            },
-            paths: vec![HighlightedText {
-                char_count: details.chars().count(),
-                highlight_positions: Vec::new(),
-                text: details,
-            }],
+        let highlighted_location = HighlightedText {
+            text: hit.string.clone(),
+            highlight_positions: hit.positions.clone(),
+            char_count: hit.string.chars().count(),
         };
+        let language_icon = language_name
+            .and_then(|language| {
+                let language = language.to_lowercase();
+                file_icons::FileIcons::get(cx).get_type_icon(&language)
+            })
+            .map(|icon_path| Icon::from_path(icon_path));
         Some(
             ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
                 .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .map(|this| {
-                    if matches!(source_kind, TaskSourceKind::UserInput) {
+                    let this = if matches!(source_kind, TaskSourceKind::UserInput) {
                         let task_index = hit.candidate_id;
                         let delete_button = div().child(
                             IconButton::new("delete", IconName::Close)
@@ -332,6 +326,11 @@ impl PickerDelegate for TasksModalDelegate {
                         this.end_hover_slot(delete_button)
                     } else {
                         this
+                    };
+                    if let Some(icon) = language_icon {
+                        this.end_slot(icon)
+                    } else {
+                        this
                     }
                 })
                 .selected(selected)