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