1use dap::{DapRegistry, DebugRequest};
2use futures::channel::oneshot;
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
5use gpui::{Subscription, WeakEntity};
6use picker::{Picker, PickerDelegate};
7use project::Project;
8use rpc::proto;
9use task::ZedDebugConfig;
10use util::debug_panic;
11
12use std::sync::Arc;
13
14use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
15use ui::{Context, Tooltip, prelude::*};
16use ui::{ListItem, ListItemSpacing};
17use workspace::{ModalView, Workspace};
18
19use crate::debugger_panel::DebugPanel;
20
21#[derive(Debug, Clone)]
22pub(super) struct Candidate {
23 pub(super) pid: u32,
24 pub(super) name: SharedString,
25 pub(super) command: Vec<String>,
26}
27
28pub(crate) enum ModalIntent {
29 ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
30 AttachToProcess(ZedDebugConfig),
31}
32
33pub(crate) struct AttachModalDelegate {
34 selected_index: usize,
35 matches: Vec<StringMatch>,
36 placeholder_text: Arc<str>,
37 pub(crate) intent: ModalIntent,
38 workspace: WeakEntity<Workspace>,
39 candidates: Arc<[Candidate]>,
40}
41
42impl AttachModalDelegate {
43 fn new(
44 workspace: WeakEntity<Workspace>,
45 intent: ModalIntent,
46 candidates: Arc<[Candidate]>,
47 ) -> Self {
48 Self {
49 workspace,
50 candidates,
51 intent,
52 selected_index: 0,
53 matches: Vec::default(),
54 placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
55 }
56 }
57}
58
59pub struct AttachModal {
60 _subscription: Subscription,
61 pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
62}
63
64impl AttachModal {
65 pub(crate) fn new(
66 intent: ModalIntent,
67 workspace: WeakEntity<Workspace>,
68 project: Entity<Project>,
69 modal: bool,
70 window: &mut Window,
71 cx: &mut Context<Self>,
72 ) -> Self {
73 let processes_task = get_processes_for_project(&project, cx);
74
75 let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);
76
77 cx.spawn_in(window, async move |this, cx| {
78 let processes = processes_task.await;
79 this.update_in(cx, |modal, window, cx| {
80 modal.picker.update(cx, |picker, cx| {
81 picker.delegate.candidates = processes;
82 picker.refresh(window, cx);
83 });
84 })?;
85 anyhow::Ok(())
86 })
87 .detach_and_log_err(cx);
88
89 modal
90 }
91
92 pub(super) fn with_processes(
93 workspace: WeakEntity<Workspace>,
94 processes: Arc<[Candidate]>,
95 modal: bool,
96 intent: ModalIntent,
97 window: &mut Window,
98 cx: &mut Context<Self>,
99 ) -> Self {
100 let picker = cx.new(|cx| {
101 Picker::uniform_list(
102 AttachModalDelegate::new(workspace, intent, processes),
103 window,
104 cx,
105 )
106 .modal(modal)
107 });
108 Self {
109 _subscription: cx.subscribe(&picker, |_, _, _, cx| {
110 cx.emit(DismissEvent);
111 }),
112 picker,
113 }
114 }
115}
116
117impl Render for AttachModal {
118 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
119 v_flex()
120 .key_context("AttachModal")
121 .track_focus(&self.focus_handle(cx))
122 .w(rems(34.))
123 .child(self.picker.clone())
124 }
125}
126
127impl EventEmitter<DismissEvent> for AttachModal {}
128
129impl Focusable for AttachModal {
130 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
131 self.picker.read(cx).focus_handle(cx)
132 }
133}
134
135impl ModalView for AttachModal {}
136
137impl PickerDelegate for AttachModalDelegate {
138 type ListItem = ListItem;
139
140 fn match_count(&self) -> usize {
141 self.matches.len()
142 }
143
144 fn selected_index(&self) -> usize {
145 self.selected_index
146 }
147
148 fn set_selected_index(
149 &mut self,
150 ix: usize,
151 _window: &mut Window,
152 _: &mut Context<Picker<Self>>,
153 ) {
154 self.selected_index = ix;
155 }
156
157 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
158 self.placeholder_text.clone()
159 }
160
161 fn update_matches(
162 &mut self,
163 query: String,
164 _window: &mut Window,
165 cx: &mut Context<Picker<Self>>,
166 ) -> gpui::Task<()> {
167 cx.spawn(async move |this, cx| {
168 let Some(processes) = this
169 .read_with(cx, |this, _| this.delegate.candidates.clone())
170 .ok()
171 else {
172 return;
173 };
174
175 let matches = fuzzy::match_strings(
176 &processes
177 .iter()
178 .enumerate()
179 .map(|(id, candidate)| {
180 StringMatchCandidate::new(
181 id,
182 format!(
183 "{} {} {}",
184 candidate.command.join(" "),
185 candidate.pid,
186 candidate.name
187 )
188 .as_str(),
189 )
190 })
191 .collect::<Vec<_>>(),
192 &query,
193 true,
194 true,
195 100,
196 &Default::default(),
197 cx.background_executor().clone(),
198 )
199 .await;
200
201 this.update(cx, |this, _| {
202 let delegate = &mut this.delegate;
203
204 delegate.matches = matches;
205
206 if delegate.matches.is_empty() {
207 delegate.selected_index = 0;
208 } else {
209 delegate.selected_index =
210 delegate.selected_index.min(delegate.matches.len() - 1);
211 }
212 })
213 .ok();
214 })
215 }
216
217 fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
218 let candidate = self
219 .matches
220 .get(self.selected_index())
221 .and_then(|current_match| {
222 let ix = current_match.candidate_id;
223 self.candidates.get(ix)
224 });
225
226 match &mut self.intent {
227 ModalIntent::ResolveProcessId(sender) => {
228 cx.emit(DismissEvent);
229
230 if let Some(sender) = sender.take() {
231 sender
232 .send(candidate.map(|candidate| candidate.pid as i32))
233 .ok();
234 }
235 }
236 ModalIntent::AttachToProcess(definition) => {
237 let Some(candidate) = candidate else {
238 return cx.emit(DismissEvent);
239 };
240
241 match &mut definition.request {
242 DebugRequest::Attach(config) => {
243 config.process_id = Some(candidate.pid);
244 }
245 DebugRequest::Launch(_) => {
246 debug_panic!("Debugger attach modal used on launch debug config");
247 return;
248 }
249 }
250
251 let workspace = self.workspace.clone();
252 let Some(panel) = workspace
253 .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
254 .ok()
255 .flatten()
256 else {
257 return;
258 };
259
260 let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
261 registry.adapter(&definition.adapter)
262 }) else {
263 return;
264 };
265
266 let definition = definition.clone();
267 cx.spawn_in(window, async move |this, cx| {
268 let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
269 return;
270 };
271
272 panel
273 .update_in(cx, |panel, window, cx| {
274 panel.start_session(
275 scenario,
276 Default::default(),
277 None,
278 None,
279 window,
280 cx,
281 );
282 })
283 .ok();
284 this.update(cx, |_, cx| {
285 cx.emit(DismissEvent);
286 })
287 .ok();
288 })
289 .detach();
290 }
291 }
292 }
293
294 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
295 self.selected_index = 0;
296
297 match &mut self.intent {
298 ModalIntent::ResolveProcessId(sender) => {
299 if let Some(sender) = sender.take() {
300 sender.send(None).ok();
301 }
302 }
303 ModalIntent::AttachToProcess(_) => {}
304 }
305
306 cx.emit(DismissEvent);
307 }
308
309 fn render_match(
310 &self,
311 ix: usize,
312 selected: bool,
313 _window: &mut Window,
314 _: &mut Context<Picker<Self>>,
315 ) -> Option<Self::ListItem> {
316 let hit = &self.matches.get(ix)?;
317 let candidate = self.candidates.get(hit.candidate_id)?;
318
319 Some(
320 ListItem::new(SharedString::from(format!("process-entry-{ix}")))
321 .inset(true)
322 .spacing(ListItemSpacing::Sparse)
323 .toggle_state(selected)
324 .child(
325 v_flex()
326 .items_start()
327 .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
328 .child(
329 div()
330 .id(SharedString::from(format!("process-entry-{ix}-command")))
331 .tooltip(Tooltip::text(
332 candidate
333 .command
334 .clone()
335 .into_iter()
336 .collect::<Vec<_>>()
337 .join(" "),
338 ))
339 .child(
340 Label::new(format!(
341 "{} {}",
342 candidate.name,
343 candidate
344 .command
345 .clone()
346 .into_iter()
347 .skip(1)
348 .collect::<Vec<_>>()
349 .join(" ")
350 ))
351 .size(LabelSize::Small)
352 .color(Color::Muted),
353 ),
354 ),
355 ),
356 )
357 }
358}
359
360fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
361 let project = project.read(cx);
362
363 if let Some(remote_client) = project.remote_client() {
364 let proto_client = remote_client.read(cx).proto_client();
365 cx.background_spawn(async move {
366 let response = proto_client
367 .request(proto::GetProcesses {
368 project_id: proto::REMOTE_SERVER_PROJECT_ID,
369 })
370 .await
371 .unwrap_or_else(|_| proto::GetProcessesResponse {
372 processes: Vec::new(),
373 });
374
375 let mut processes: Vec<Candidate> = response
376 .processes
377 .into_iter()
378 .map(|p| Candidate {
379 pid: p.pid,
380 name: p.name.into(),
381 command: p.command,
382 })
383 .collect();
384
385 processes.sort_by_key(|k| k.name.clone());
386 Arc::from(processes.into_boxed_slice())
387 })
388 } else {
389 let refresh_kind = RefreshKind::nothing().with_processes(
390 ProcessRefreshKind::nothing()
391 .without_tasks()
392 .with_cmd(UpdateKind::Always),
393 );
394 let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
395 .processes()
396 .values()
397 .map(|process| {
398 let name = process.name().to_string_lossy().into_owned();
399 Candidate {
400 name: name.into(),
401 pid: process.pid().as_u32(),
402 command: process
403 .cmd()
404 .iter()
405 .map(|s| s.to_string_lossy().into_owned())
406 .collect::<Vec<_>>(),
407 }
408 })
409 .collect();
410 processes.sort_by_key(|k| k.name.clone());
411 let processes = processes.into_iter().collect();
412 Task::ready(processes)
413 }
414}
415
416#[cfg(test)]
417pub(crate) fn set_candidates(
418 modal: &AttachModal,
419 candidates: Arc<[Candidate]>,
420 window: &mut Window,
421 cx: &mut Context<AttachModal>,
422) {
423 modal.picker.update(cx, |picker, cx| {
424 picker.delegate.candidates = candidates;
425 picker.refresh(window, cx);
426 });
427}
428
429#[cfg(test)]
430pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
431 modal.picker.read_with(cx, |picker, _| {
432 picker
433 .delegate
434 .matches
435 .iter()
436 .map(|hit| hit.string.clone())
437 .collect::<Vec<_>>()
438 })
439}