Use both language and LSP icons for LSP tasks (#31773)

Kirill Bulatov created

Make more explicit which language LSP tasks are used.

Before:

![image](https://github.com/user-attachments/assets/27f93c5f-942e-47a0-9b74-2c6d4d6248de)

After:

![image
(1)](https://github.com/user-attachments/assets/5a29fb0a-2e16-4c35-9dda-ae7925eaa034)


![image](https://github.com/user-attachments/assets/d1bf518e-63d1-4ebf-af3d-3c9d464c6532)


Release Notes:

- N/A

Change summary

crates/debugger_ui/src/new_session_modal.rs | 25 ++++++++++++++++------
crates/editor/src/lsp_ext.rs                | 23 ++++++++++++++++----
crates/project/src/task_inventory.rs        | 12 ++++++++--
crates/tasks_ui/src/modal.rs                | 25 ++++++++++++++++++----
4 files changed, 65 insertions(+), 20 deletions(-)

Detailed changes

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -27,9 +27,9 @@ use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
     ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
-    InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
-    ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
-    Toggleable, Window, div, h_flex, relative, rems, v_flex,
+    IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
+    ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
+    ToggleButton, ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
 };
 use util::ResultExt;
 use workspace::{ModalView, Workspace, pane};
@@ -1222,21 +1222,32 @@ impl PickerDelegate for DebugScenarioDelegate {
         let task_kind = &self.candidates[hit.candidate_id].0;
 
         let icon = match task_kind {
-            Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::BoltFilled)),
             Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
             Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
             Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
-            Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
+            Some(TaskSourceKind::Lsp {
+                language_name: name,
+                ..
+            })
+            | Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
                 .get_icon_for_type(&name.to_lowercase(), cx)
                 .map(Icon::from_path),
             None => Some(Icon::new(IconName::HistoryRerun)),
         }
-        .map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
+        .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
+        let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
+            Some(Indicator::icon(
+                Icon::new(IconName::BoltFilled).color(Color::Muted),
+            ))
+        } else {
+            None
+        };
+        let icon = icon.map(|icon| IconWithIndicator::new(icon, indicator));
 
         Some(
             ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
                 .inset(true)
-                .start_slot::<Icon>(icon)
+                .start_slot::<IconWithIndicator>(icon)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
                 .child(highlighted_location.render(window, cx)),

crates/editor/src/lsp_ext.rs 🔗

@@ -22,6 +22,7 @@ use smol::stream::StreamExt;
 use task::ResolvedTask;
 use task::TaskContext;
 use text::BufferId;
+use ui::SharedString;
 use util::ResultExt as _;
 
 pub(crate) fn find_specific_language_server_in_selection<F>(
@@ -133,13 +134,22 @@ pub fn lsp_tasks(
 
     cx.spawn(async move |cx| {
         cx.spawn(async move |cx| {
-            let mut lsp_tasks = Vec::new();
+            let mut lsp_tasks = HashMap::default();
             while let Some(server_to_query) = lsp_task_sources.next().await {
                 if let Some((server_id, buffers)) = server_to_query {
-                    let source_kind = TaskSourceKind::Lsp(server_id);
-                    let id_base = source_kind.to_id_base();
                     let mut new_lsp_tasks = Vec::new();
                     for buffer in buffers {
+                        let source_kind = match buffer.update(cx, |buffer, _| {
+                            buffer.language().map(|language| language.name())
+                        }) {
+                            Ok(Some(language_name)) => TaskSourceKind::Lsp {
+                                server: server_id,
+                                language_name: SharedString::from(language_name),
+                            },
+                            Ok(None) => continue,
+                            Err(_) => return Vec::new(),
+                        };
+                        let id_base = source_kind.to_id_base();
                         let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
                             .await
                             .unwrap_or_default();
@@ -168,11 +178,14 @@ pub fn lsp_tasks(
                                 );
                             }
                         }
+                        lsp_tasks
+                            .entry(source_kind)
+                            .or_insert_with(Vec::new)
+                            .append(&mut new_lsp_tasks);
                     }
-                    lsp_tasks.push((source_kind, new_lsp_tasks));
                 }
             }
-            lsp_tasks
+            lsp_tasks.into_iter().collect()
         })
         .race({
             // `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast

crates/project/src/task_inventory.rs 🔗

@@ -132,7 +132,10 @@ pub enum TaskSourceKind {
     /// Languages-specific tasks coming from extensions.
     Language { name: SharedString },
     /// Language-specific tasks coming from LSP servers.
-    Lsp(LanguageServerId),
+    Lsp {
+        language_name: SharedString,
+        server: LanguageServerId,
+    },
 }
 
 /// A collection of task contexts, derived from the current state of the workspace.
@@ -211,7 +214,10 @@ impl TaskSourceKind {
                 format!("{id_base}_{id}_{}", directory_in_worktree.display())
             }
             Self::Language { name } => format!("language_{name}"),
-            Self::Lsp(server_id) => format!("lsp_{server_id}"),
+            Self::Lsp {
+                server,
+                language_name,
+            } => format!("lsp_{language_name}_{server}"),
         }
     }
 }
@@ -712,7 +718,7 @@ fn task_lru_comparator(
 
 fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
     match kind {
-        TaskSourceKind::Lsp(..) => 0,
+        TaskSourceKind::Lsp { .. } => 0,
         TaskSourceKind::Language { .. } => 1,
         TaskSourceKind::UserInput => 2,
         TaskSourceKind::Worktree { .. } => 3,

crates/tasks_ui/src/modal.rs 🔗

@@ -14,8 +14,9 @@ use project::{TaskSourceKind, task_store::TaskStore};
 use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskTemplate};
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
-    IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
-    ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, h_flex, v_flex,
+    IconButton, IconButtonShape, IconName, IconSize, IconWithIndicator, Indicator, IntoElement,
+    KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div,
+    h_flex, v_flex,
 };
 
 use util::{ResultExt, truncate_and_trailoff};
@@ -448,15 +449,29 @@ impl PickerDelegate for TasksModalDelegate {
             color: Color::Default,
         };
         let icon = match source_kind {
-            TaskSourceKind::Lsp(..) => Some(Icon::new(IconName::BoltFilled)),
             TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)),
             TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)),
             TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)),
-            TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx)
+            TaskSourceKind::Lsp {
+                language_name: name,
+                ..
+            }
+            | TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx)
                 .get_icon_for_type(&name.to_lowercase(), cx)
                 .map(Icon::from_path),
         }
         .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
+        let indicator = if matches!(source_kind, TaskSourceKind::Lsp { .. }) {
+            Some(Indicator::icon(
+                Icon::new(IconName::Bolt).size(IconSize::Small),
+            ))
+        } else {
+            None
+        };
+        let icon = icon.map(|icon| {
+            IconWithIndicator::new(icon, indicator)
+                .indicator_border_color(Some(cx.theme().colors().border_transparent))
+        });
         let history_run_icon = if Some(ix) <= self.divider_index {
             Some(
                 Icon::new(IconName::HistoryRerun)
@@ -476,7 +491,7 @@ impl PickerDelegate for TasksModalDelegate {
         Some(
             ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
                 .inset(true)
-                .start_slot::<Icon>(icon)
+                .start_slot::<IconWithIndicator>(icon)
                 .end_slot::<AnyElement>(
                     h_flex()
                         .gap_1()