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

- [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>
This commit is contained in:
Cole Miller 2025-06-05 13:25:51 -04:00 committed by GitHub
parent 8730d317a8
commit f36143a461
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 22 deletions

1
Cargo.lock generated
View file

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

View file

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

View file

@ -19,6 +19,7 @@ use gpui::{
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
TextStyle, UnderlineStyle, WeakEntity, TextStyle, UnderlineStyle, WeakEntity,
}; };
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore}; use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::{Settings, initial_local_debug_tasks_content}; use settings::{Settings, initial_local_debug_tasks_content};
@ -49,7 +50,7 @@ pub(super) struct NewProcessModal {
mode: NewProcessMode, mode: NewProcessMode,
debug_picker: Entity<Picker<DebugDelegate>>, debug_picker: Entity<Picker<DebugDelegate>>,
attach_mode: Entity<AttachMode>, attach_mode: Entity<AttachMode>,
launch_mode: Entity<LaunchMode>, launch_mode: Entity<ConfigureMode>,
task_mode: TaskMode, task_mode: TaskMode,
debugger: Option<DebugAdapterName>, debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>, // save_scenario_state: Option<SaveScenarioState>,
@ -97,13 +98,13 @@ impl NewProcessModal {
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
let attach_mode = AttachMode::new(None, workspace_handle.clone(), 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 = let delegate =
DebugDelegate::new(debug_panel.downgrade(), task_store.clone()); DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
Picker::uniform_list(delegate, window, cx).modal(false) 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 }); let task_overrides = Some(TaskOverrides { reveal_target });
@ -122,7 +123,7 @@ impl NewProcessModal {
}; };
let _subscriptions = [ let _subscriptions = [
cx.subscribe(&launch_picker, |_, _, _, cx| { cx.subscribe(&debug_picker, |_, _, _, cx| {
cx.emit(DismissEvent); cx.emit(DismissEvent);
}), }),
cx.subscribe( cx.subscribe(
@ -137,19 +138,76 @@ impl NewProcessModal {
]; ];
cx.spawn_in(window, { cx.spawn_in(window, {
let launch_picker = launch_picker.downgrade(); let debug_picker = debug_picker.downgrade();
let configure_mode = configure_mode.downgrade(); let configure_mode = configure_mode.downgrade();
let task_modal = task_mode.task_modal.downgrade(); let task_modal = task_mode.task_modal.downgrade();
let workspace = workspace_handle.clone();
async move |this, cx| { async move |this, cx| {
let task_contexts = task_contexts.await; let task_contexts = task_contexts.await;
let task_contexts = Arc::new(task_contexts); 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| { .update_in(cx, |picker, window, cx| {
picker.delegate.task_contexts_loaded( picker.delegate.tasks_loaded(
task_contexts.clone(), task_contexts.clone(),
languages, languages,
window, lsp_tasks.clone(),
current_resolved_tasks.clone(),
add_current_language_tasks,
cx, cx,
); );
picker.refresh(window, cx); picker.refresh(window, cx);
@ -170,7 +228,15 @@ impl NewProcessModal {
task_modal task_modal
.update_in(cx, |task_modal, window, cx| { .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(); .ok();
@ -178,12 +244,14 @@ impl NewProcessModal {
cx.notify(); cx.notify();
}) })
.ok(); .ok();
anyhow::Ok(())
} }
}) })
.detach(); .detach();
Self { Self {
debug_picker: launch_picker, debug_picker,
attach_mode, attach_mode,
launch_mode: configure_mode, launch_mode: configure_mode,
task_mode, task_mode,
@ -820,14 +888,14 @@ impl RenderOnce for AttachMode {
} }
#[derive(Clone)] #[derive(Clone)]
pub(super) struct LaunchMode { pub(super) struct ConfigureMode {
program: Entity<Editor>, program: Entity<Editor>,
cwd: Entity<Editor>, cwd: Entity<Editor>,
stop_on_entry: ToggleState, stop_on_entry: ToggleState,
// save_to_debug_json: ToggleState, // save_to_debug_json: ToggleState,
} }
impl LaunchMode { impl ConfigureMode {
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> { pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let program = cx.new(|cx| Editor::single_line(window, cx)); let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| { program.update(cx, |this, cx| {
@ -1067,21 +1135,29 @@ impl DebugDelegate {
(language, scenario) (language, scenario)
} }
pub fn task_contexts_loaded( pub fn tasks_loaded(
&mut self, &mut self,
task_contexts: Arc<TaskContexts>, task_contexts: Arc<TaskContexts>,
languages: Arc<LanguageRegistry>, 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>>, cx: &mut Context<Picker<Self>>,
) { ) {
self.task_contexts = Some(task_contexts); self.task_contexts = Some(task_contexts.clone());
let (recent, scenarios) = self let (recent, scenarios) = self
.task_store .task_store
.update(cx, |task_store, cx| { .update(cx, |task_store, cx| {
task_store.task_inventory().map(|inventory| { task_store.task_inventory().map(|inventory| {
inventory.update(cx, |inventory, cx| { 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)); .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) { let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
Some(Indicator::icon( Some(Indicator::icon(
Icon::new(IconName::BoltFilled).color(Color::Muted), Icon::new(IconName::BoltFilled)
.color(Color::Muted)
.size(IconSize::Small),
)) ))
} else { } else {
None 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( Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}"))) ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))

View file

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

View file

@ -162,15 +162,33 @@ impl TasksModal {
} }
} }
pub fn task_contexts_loaded( pub fn tasks_loaded(
&mut self, &mut self,
task_contexts: Arc<TaskContexts>, 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, window: &mut Window,
cx: &mut Context<Self>, 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| { self.picker.update(cx, |picker, cx| {
picker.delegate.task_contexts = task_contexts; 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); picker.refresh(window, cx);
cx.notify(); cx.notify();
}) })
@ -296,6 +314,9 @@ impl PickerDelegate for TasksModalDelegate {
.map(move |(_, task)| (kind.clone(), task)) .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( new_candidates.extend(current.into_iter().filter(
|(task_kind, _)| { |(task_kind, _)| {
add_current_language_tasks add_current_language_tasks