Files
oak-gpui/crates/debugger_ui/src/attach_modal.rs
T
feeiyu f213f4bcc8 Fix duplicate process entries in WSL debug attach list (#40591)
Closes #40589

Replaced `System::new_all()` with `System::new_with_specifics` to fetch
only essential process information and exclude non-main threads from the
process list

after fix:
<img width="641" height="474" alt="image"
src="https://github.com/user-attachments/assets/32335552-2f7a-4317-8c01-f37b2eadfdc1"
/>


Release Notes:

- Fix duplicate process entries in WSL debug attach list
2025-10-24 11:49:30 -04:00

403 lines
12 KiB
Rust

use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::Project;
use rpc::proto;
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};
use workspace::{ModalView, Workspace};
use crate::debugger_panel::DebugPanel;
#[derive(Debug, Clone)]
pub(super) struct Candidate {
pub(super) pid: u32,
pub(super) name: SharedString,
pub(super) command: Vec<String>,
}
pub(crate) struct AttachModalDelegate {
selected_index: usize,
matches: Vec<StringMatch>,
placeholder_text: Arc<str>,
pub(crate) definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
candidates: Arc<[Candidate]>,
}
impl AttachModalDelegate {
fn new(
workspace: WeakEntity<Workspace>,
definition: ZedDebugConfig,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
workspace,
definition,
candidates,
selected_index: 0,
matches: Vec::default(),
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
}
}
}
pub struct AttachModal {
_subscription: Subscription,
pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
}
impl AttachModal {
pub fn new(
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let processes_task = get_processes_for_project(&project, cx);
let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
cx.spawn_in(window, async move |this, cx| {
let processes = processes_task.await;
this.update_in(cx, |modal, window, cx| {
modal.picker.update(cx, |picker, cx| {
picker.delegate.candidates = processes;
picker.refresh(window, cx);
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
modal
}
pub(super) fn with_processes(
workspace: WeakEntity<Workspace>,
definition: ZedDebugConfig,
processes: Arc<[Candidate]>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| {
Picker::uniform_list(
AttachModalDelegate::new(workspace, definition, processes),
window,
cx,
)
.modal(modal)
});
Self {
_subscription: cx.subscribe(&picker, |_, _, _, cx| {
cx.emit(DismissEvent);
}),
picker,
}
}
}
impl Render for AttachModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
v_flex()
.key_context("AttachModal")
.track_focus(&self.focus_handle(cx))
.w(rems(34.))
.child(self.picker.clone())
}
}
impl EventEmitter<DismissEvent> for AttachModal {}
impl Focusable for AttachModal {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.read(cx).focus_handle(cx)
}
}
impl ModalView for AttachModal {}
impl PickerDelegate for AttachModalDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
self.placeholder_text.clone()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> gpui::Task<()> {
cx.spawn(async move |this, cx| {
let Some(processes) = this
.read_with(cx, |this, _| this.delegate.candidates.clone())
.ok()
else {
return;
};
let matches = fuzzy::match_strings(
&processes
.iter()
.enumerate()
.map(|(id, candidate)| {
StringMatchCandidate::new(
id,
format!(
"{} {} {}",
candidate.command.join(" "),
candidate.pid,
candidate.name
)
.as_str(),
)
})
.collect::<Vec<_>>(),
&query,
true,
true,
100,
&Default::default(),
cx.background_executor().clone(),
)
.await;
this.update(cx, |this, _| {
let delegate = &mut this.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
delegate.selected_index.min(delegate.matches.len() - 1);
}
})
.ok();
})
}
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
.and_then(|current_match| {
let ix = current_match.candidate_id;
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| {
cx.emit(DismissEvent);
})
.ok();
})
.detach();
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = 0;
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches.get(ix)?;
let candidate = self.candidates.get(hit.candidate_id)?;
Some(
ListItem::new(SharedString::from(format!("process-entry-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.items_start()
.child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
.child(
div()
.id(SharedString::from(format!("process-entry-{ix}-command")))
.tooltip(Tooltip::text(
candidate
.command
.clone()
.into_iter()
.collect::<Vec<_>>()
.join(" "),
))
.child(
Label::new(format!(
"{} {}",
candidate.name,
candidate
.command
.clone()
.into_iter()
.skip(1)
.collect::<Vec<_>>()
.join(" ")
))
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
}
}
fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
let project = project.read(cx);
if let Some(remote_client) = project.remote_client() {
let proto_client = remote_client.read(cx).proto_client();
cx.spawn(async move |_cx| {
let response = proto_client
.request(proto::GetProcesses {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
})
.await
.unwrap_or_else(|_| proto::GetProcessesResponse {
processes: Vec::new(),
});
let mut processes: Vec<Candidate> = response
.processes
.into_iter()
.map(|p| Candidate {
pid: p.pid,
name: p.name.into(),
command: p.command,
})
.collect();
processes.sort_by_key(|k| k.name.clone());
Arc::from(processes.into_boxed_slice())
})
} else {
let refresh_kind = RefreshKind::nothing().with_processes(
ProcessRefreshKind::nothing()
.without_tasks()
.with_cmd(UpdateKind::Always),
);
let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Task::ready(processes)
}
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {
picker
.delegate
.matches
.iter()
.map(|hit| hit.string.clone())
.collect::<Vec<_>>()
})
}