debugger: Allow users to include PickProcessId in debug tasks and resolve Pid (#42913)

Closes #33286

This PR adds support for Zed's `$ZED_PICK_PID` command in debug
configurations, which allows users to select a process to attach to at
debug time. When this variable is present in a debug configuration, Zed
automatically opens a process picker modal.

Follow up for this will be integrating this variable in the task system
instead of just the debug configuration system.

Release Notes:

- Added `$ZED_PICK_PID` variable for debug configurations, allowing
users to select which process to attach the debugger to at runtime

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
Anthony Eid
2025-11-20 10:12:59 -05:00
committed by GitHub
co-authored by Remco Smits
parent e033829ef2
commit 56401fc99c
6 changed files with 413 additions and 90 deletions
+106 -69
View File
@@ -1,4 +1,5 @@
use dap::{DapRegistry, DebugRequest};
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
@@ -9,6 +10,7 @@ use task::ZedDebugConfig;
use util::debug_panic;
use std::sync::Arc;
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
use ui::{Context, Tooltip, prelude::*};
use ui::{ListItem, ListItemSpacing};
@@ -23,11 +25,16 @@ pub(super) struct Candidate {
pub(super) command: Vec<String>,
}
pub(crate) enum ModalIntent {
ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
AttachToProcess(ZedDebugConfig),
}
pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
pub(crate) definition: ZedDebugConfig,
pub(crate) intent: ModalIntent,
workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>,
}
@@ -35,13 +42,13 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate {
fn new(
workspace: WeakEntity<Workspace>,
definition: ZedDebugConfig,
intent: ModalIntent,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
workspace,
definition,
candidates,
intent,
selected_index: 0,
matches: Vec::default(),
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
@@ -55,8 +62,8 @@ pub struct AttachModal {
}
impl AttachModal {
pub fn new(
definition: ZedDebugConfig,
pub(crate) fn new(
intent: ModalIntent,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
modal: bool,
@@ -65,7 +72,7 @@ impl AttachModal {
) -> Self {
let processes_task = get_processes_for_project(&project, cx);
let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);
cx.spawn_in(window, async move |this, cx| {
let processes = processes_task.await;
@@ -84,15 +91,15 @@ impl AttachModal {
pub(super) fn with_processes(
workspace: WeakEntity<Workspace>,
definition: ZedDebugConfig,
processes: Arc<[Candidate]>,
modal: bool,
intent: ModalIntent,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
AttachModalDelegate::new(workspace, definition, processes),
AttachModalDelegate::new(workspace, intent, processes),
window,
cx,
)
@@ -207,7 +214,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -216,69 +223,86 @@ impl PickerDelegate for AttachModalDelegate {
self.candidates.get(ix)
});
let Some(candidate) = candidate else {
return cx.emit(DismissEvent);
};
match &mut self.definition.request {
DebugRequest::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequest::Launch(_) => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}
}
let workspace = self.workspace.clone();
let Some(panel) = workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten()
else {
return;
};
if secondary {
// let Some(id) = worktree_id else { return };
// cx.spawn_in(window, async move |_, cx| {
// panel
// .update_in(cx, |debug_panel, window, cx| {
// debug_panel.save_scenario(&debug_scenario, id, window, cx)
// })?
// .await?;
// anyhow::Ok(())
// })
// .detach_and_log_err(cx);
}
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry.adapter(&self.definition.adapter)
}) else {
return;
};
let definition = self.definition.clone();
cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return;
};
panel
.update_in(cx, |panel, window, cx| {
panel.start_session(scenario, Default::default(), None, None, window, cx);
})
.ok();
this.update(cx, |_, cx| {
match &mut self.intent {
ModalIntent::ResolveProcessId(sender) => {
cx.emit(DismissEvent);
})
.ok();
})
.detach();
if let Some(sender) = sender.take() {
sender
.send(candidate.map(|candidate| candidate.pid as i32))
.ok();
}
}
ModalIntent::AttachToProcess(definition) => {
let Some(candidate) = candidate else {
return cx.emit(DismissEvent);
};
match &mut definition.request {
DebugRequest::Attach(config) => {
config.process_id = Some(candidate.pid);
}
DebugRequest::Launch(_) => {
debug_panic!("Debugger attach modal used on launch debug config");
return;
}
}
let workspace = self.workspace.clone();
let Some(panel) = workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten()
else {
return;
};
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry.adapter(&definition.adapter)
}) else {
return;
};
let definition = definition.clone();
cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return;
};
panel
.update_in(cx, |panel, window, cx| {
panel.start_session(
scenario,
Default::default(),
None,
None,
window,
cx,
);
})
.ok();
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
})
.detach();
}
}
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = 0;
match &mut self.intent {
ModalIntent::ResolveProcessId(sender) => {
if let Some(sender) = sender.take() {
sender.send(None).ok();
}
}
ModalIntent::AttachToProcess(_) => {}
}
cx.emit(DismissEvent);
}
@@ -338,7 +362,7 @@ fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Ar
if let Some(remote_client) = project.remote_client() {
let proto_client = remote_client.read(cx).proto_client();
cx.spawn(async move |_cx| {
cx.background_spawn(async move {
let response = proto_client
.request(proto::GetProcesses {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
@@ -389,8 +413,21 @@ fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Ar
}
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
#[cfg(test)]
pub(crate) fn set_candidates(
modal: &AttachModal,
candidates: Arc<[Candidate]>,
window: &mut Window,
cx: &mut Context<AttachModal>,
) {
modal.picker.update(cx, |picker, cx| {
picker.delegate.candidates = candidates;
picker.refresh(window, cx);
});
}
#[cfg(test)]
pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {
picker
.delegate