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",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",

View file

@ -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

View file

@ -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}")))

View file

@ -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()

View file

@ -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