debugger: Run locators on LSP tasks for the new process modal (#32097)

Cole Miller and Anthony Eid created

- [x] pass LSP tasks into list_debug_scenarios
- [x] load LSP tasks only once for both modals
- [x] align ordering
- [x] improve appearance of LSP debug task icons
- [ ] reconsider how `add_current_language_tasks` works
- [ ] add a test

Release Notes:

- Debugger Beta: Added debuggable LSP tasks to the "Debug" tab of the
new process modal.

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

Cargo.lock                                  |   1 
crates/debugger_ui/Cargo.toml               |   1 
crates/debugger_ui/src/new_process_modal.rs | 117 +++++++++++++++++++---
crates/project/src/task_inventory.rs        |  13 ++
crates/tasks_ui/src/modal.rs                |  25 ++++
5 files changed, 135 insertions(+), 22 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4234,6 +4234,7 @@ dependencies = [
  "futures 0.3.31",
  "fuzzy",
  "gpui",
+ "itertools 0.14.0",
  "language",
  "log",
  "menu",

crates/debugger_ui/Cargo.toml 🔗

@@ -39,6 +39,7 @@ file_icons.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
+itertools.workspace = true
 language.workspace = true
 log.workspace = true
 menu.workspace = true

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -19,6 +19,7 @@ use gpui::{
     InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
     TextStyle, UnderlineStyle, WeakEntity,
 };
+use itertools::Itertools as _;
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
 use settings::{Settings, initial_local_debug_tasks_content};
@@ -49,7 +50,7 @@ pub(super) struct NewProcessModal {
     mode: NewProcessMode,
     debug_picker: Entity<Picker<DebugDelegate>>,
     attach_mode: Entity<AttachMode>,
-    launch_mode: Entity<LaunchMode>,
+    launch_mode: Entity<ConfigureMode>,
     task_mode: TaskMode,
     debugger: Option<DebugAdapterName>,
     // save_scenario_state: Option<SaveScenarioState>,
@@ -97,13 +98,13 @@ impl NewProcessModal {
                 workspace.toggle_modal(window, cx, |window, cx| {
                     let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
 
-                    let launch_picker = cx.new(|cx| {
+                    let debug_picker = cx.new(|cx| {
                         let delegate =
                             DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
                         Picker::uniform_list(delegate, window, cx).modal(false)
                     });
 
-                    let configure_mode = LaunchMode::new(window, cx);
+                    let configure_mode = ConfigureMode::new(window, cx);
 
                     let task_overrides = Some(TaskOverrides { reveal_target });
 
@@ -122,7 +123,7 @@ impl NewProcessModal {
                     };
 
                     let _subscriptions = [
-                        cx.subscribe(&launch_picker, |_, _, _, cx| {
+                        cx.subscribe(&debug_picker, |_, _, _, cx| {
                             cx.emit(DismissEvent);
                         }),
                         cx.subscribe(
@@ -137,19 +138,76 @@ impl NewProcessModal {
                     ];
 
                     cx.spawn_in(window, {
-                        let launch_picker = launch_picker.downgrade();
+                        let debug_picker = debug_picker.downgrade();
                         let configure_mode = configure_mode.downgrade();
                         let task_modal = task_mode.task_modal.downgrade();
+                        let workspace = workspace_handle.clone();
 
                         async move |this, cx| {
                             let task_contexts = task_contexts.await;
                             let task_contexts = Arc::new(task_contexts);
-                            launch_picker
+                            let lsp_task_sources = task_contexts.lsp_task_sources.clone();
+                            let task_position = task_contexts.latest_selection;
+                            // Get LSP tasks and filter out based on language vs lsp preference
+                            let (lsp_tasks, prefer_lsp) =
+                                workspace.update(cx, |workspace, cx| {
+                                    let lsp_tasks = editor::lsp_tasks(
+                                        workspace.project().clone(),
+                                        &lsp_task_sources,
+                                        task_position,
+                                        cx,
+                                    );
+                                    let prefer_lsp = workspace
+                                        .active_item(cx)
+                                        .and_then(|item| item.downcast::<Editor>())
+                                        .map(|editor| {
+                                            editor
+                                                .read(cx)
+                                                .buffer()
+                                                .read(cx)
+                                                .language_settings(cx)
+                                                .tasks
+                                                .prefer_lsp
+                                        })
+                                        .unwrap_or(false);
+                                    (lsp_tasks, prefer_lsp)
+                                })?;
+
+                            let lsp_tasks = lsp_tasks.await;
+                            let add_current_language_tasks = !prefer_lsp || lsp_tasks.is_empty();
+
+                            let lsp_tasks = lsp_tasks
+                                .into_iter()
+                                .flat_map(|(kind, tasks_with_locations)| {
+                                    tasks_with_locations
+                                        .into_iter()
+                                        .sorted_by_key(|(location, task)| {
+                                            (location.is_none(), task.resolved_label.clone())
+                                        })
+                                        .map(move |(_, task)| (kind.clone(), task))
+                                })
+                                .collect::<Vec<_>>();
+
+                            let Some(task_inventory) = task_store
+                                .update(cx, |task_store, _| task_store.task_inventory().cloned())?
+                            else {
+                                return Ok(());
+                            };
+
+                            let (used_tasks, current_resolved_tasks) =
+                                task_inventory.update(cx, |task_inventory, cx| {
+                                    task_inventory
+                                        .used_and_current_resolved_tasks(&task_contexts, cx)
+                                })?;
+
+                            debug_picker
                                 .update_in(cx, |picker, window, cx| {
-                                    picker.delegate.task_contexts_loaded(
+                                    picker.delegate.tasks_loaded(
                                         task_contexts.clone(),
                                         languages,
-                                        window,
+                                        lsp_tasks.clone(),
+                                        current_resolved_tasks.clone(),
+                                        add_current_language_tasks,
                                         cx,
                                     );
                                     picker.refresh(window, cx);
@@ -170,7 +228,15 @@ impl NewProcessModal {
 
                             task_modal
                                 .update_in(cx, |task_modal, window, cx| {
-                                    task_modal.task_contexts_loaded(task_contexts, window, cx);
+                                    task_modal.tasks_loaded(
+                                        task_contexts,
+                                        lsp_tasks,
+                                        used_tasks,
+                                        current_resolved_tasks,
+                                        add_current_language_tasks,
+                                        window,
+                                        cx,
+                                    );
                                 })
                                 .ok();
 
@@ -178,12 +244,14 @@ impl NewProcessModal {
                                 cx.notify();
                             })
                             .ok();
+
+                            anyhow::Ok(())
                         }
                     })
                     .detach();
 
                     Self {
-                        debug_picker: launch_picker,
+                        debug_picker,
                         attach_mode,
                         launch_mode: configure_mode,
                         task_mode,
@@ -820,14 +888,14 @@ impl RenderOnce for AttachMode {
 }
 
 #[derive(Clone)]
-pub(super) struct LaunchMode {
+pub(super) struct ConfigureMode {
     program: Entity<Editor>,
     cwd: Entity<Editor>,
     stop_on_entry: ToggleState,
     // save_to_debug_json: ToggleState,
 }
 
-impl LaunchMode {
+impl ConfigureMode {
     pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
         let program = cx.new(|cx| Editor::single_line(window, cx));
         program.update(cx, |this, cx| {
@@ -1067,21 +1135,29 @@ impl DebugDelegate {
         (language, scenario)
     }
 
-    pub fn task_contexts_loaded(
+    pub fn tasks_loaded(
         &mut self,
         task_contexts: Arc<TaskContexts>,
         languages: Arc<LanguageRegistry>,
-        _window: &mut Window,
+        lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        add_current_language_tasks: bool,
         cx: &mut Context<Picker<Self>>,
     ) {
-        self.task_contexts = Some(task_contexts);
+        self.task_contexts = Some(task_contexts.clone());
 
         let (recent, scenarios) = self
             .task_store
             .update(cx, |task_store, cx| {
                 task_store.task_inventory().map(|inventory| {
                     inventory.update(cx, |inventory, cx| {
-                        inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
+                        inventory.list_debug_scenarios(
+                            &task_contexts,
+                            lsp_tasks,
+                            current_resolved_tasks,
+                            add_current_language_tasks,
+                            cx,
+                        )
                     })
                 })
             })
@@ -1257,12 +1333,17 @@ impl PickerDelegate for DebugDelegate {
         .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),
+                Icon::new(IconName::BoltFilled)
+                    .color(Color::Muted)
+                    .size(IconSize::Small),
             ))
         } else {
             None
         };
-        let icon = icon.map(|icon| IconWithIndicator::new(icon, indicator));
+        let icon = icon.map(|icon| {
+            IconWithIndicator::new(icon, indicator)
+                .indicator_border_color(Some(cx.theme().colors().border_transparent))
+        });
 
         Some(
             ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))

crates/project/src/task_inventory.rs 🔗

@@ -243,6 +243,9 @@ impl Inventory {
     pub fn list_debug_scenarios(
         &self,
         task_contexts: &TaskContexts,
+        lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        add_current_language_tasks: bool,
         cx: &mut App,
     ) -> (Vec<DebugScenario>, Vec<(TaskSourceKind, DebugScenario)>) {
         let mut scenarios = Vec::new();
@@ -258,7 +261,6 @@ impl Inventory {
         }
         scenarios.extend(self.global_debug_scenarios_from_settings());
 
-        let (_, new) = self.used_and_current_resolved_tasks(task_contexts, cx);
         if let Some(location) = task_contexts.location() {
             let file = location.buffer.read(cx).file();
             let language = location.buffer.read(cx).language();
@@ -271,7 +273,14 @@ impl Inventory {
                     language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
                 });
             if let Some(adapter) = adapter {
-                for (kind, task) in new {
+                for (kind, task) in
+                    lsp_tasks
+                        .into_iter()
+                        .chain(current_resolved_tasks.into_iter().filter(|(kind, _)| {
+                            add_current_language_tasks
+                                || !matches!(kind, TaskSourceKind::Language { .. })
+                        }))
+                {
                     if let Some(scenario) =
                         DapRegistry::global(cx)
                             .locators()

crates/tasks_ui/src/modal.rs 🔗

@@ -162,15 +162,33 @@ impl TasksModal {
         }
     }
 
-    pub fn task_contexts_loaded(
+    pub fn tasks_loaded(
         &mut self,
         task_contexts: Arc<TaskContexts>,
+        lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        used_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
+        add_current_language_tasks: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let last_used_candidate_index = if used_tasks.is_empty() {
+            None
+        } else {
+            Some(used_tasks.len() - 1)
+        };
+        let mut new_candidates = used_tasks;
+        new_candidates.extend(lsp_tasks);
+        // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false
+        // We should move the filter to new_candidates instead of on current
+        // and add a test for this
+        new_candidates.extend(current_resolved_tasks.into_iter().filter(|(task_kind, _)| {
+            add_current_language_tasks || !matches!(task_kind, TaskSourceKind::Language { .. })
+        }));
         self.picker.update(cx, |picker, cx| {
             picker.delegate.task_contexts = task_contexts;
-            picker.delegate.candidates = None;
+            picker.delegate.last_used_candidate_index = last_used_candidate_index;
+            picker.delegate.candidates = Some(new_candidates);
             picker.refresh(window, cx);
             cx.notify();
         })
@@ -296,6 +314,9 @@ impl PickerDelegate for TasksModalDelegate {
                                             .map(move |(_, task)| (kind.clone(), task))
                                     },
                                 ));
+                                // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false
+                                // We should move the filter to new_candidates instead of on current
+                                // and add a test for this
                                 new_candidates.extend(current.into_iter().filter(
                                     |(task_kind, _)| {
                                         add_current_language_tasks