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