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, _: 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 Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
233 registry.adapter(&self.definition.adapter)
234 }) else {
235 return;
236 };
237
238 let workspace = self.workspace.clone();
239 let definition = self.definition.clone();
240 cx.spawn_in(window, async move |this, cx| {
241 let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
242 return;
243 };
244
245 let panel = workspace
246 .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
247 .ok()
248 .flatten();
249 if let Some(panel) = panel {
250 panel
251 .update_in(cx, |panel, window, cx| {
252 panel.start_session(scenario, Default::default(), None, None, window, cx);
253 })
254 .ok();
255 }
256 this.update(cx, |_, cx| {
257 cx.emit(DismissEvent);
258 })
259 .ok();
260 })
261 .detach();
262 }
263
264 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
265 self.selected_index = 0;
266
267 cx.emit(DismissEvent);
268 }
269
270 fn render_match(
271 &self,
272 ix: usize,
273 selected: bool,
274 _window: &mut Window,
275 _: &mut Context<Picker<Self>>,
276 ) -> Option<Self::ListItem> {
277 let hit = &self.matches[ix];
278 let candidate = self.candidates.get(hit.candidate_id)?;
279
280 Some(
281 ListItem::new(SharedString::from(format!("process-entry-{ix}")))
282 .inset(true)
283 .spacing(ListItemSpacing::Sparse)
284 .toggle_state(selected)
285 .child(
286 v_flex()
287 .items_start()
288 .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
289 .child(
290 div()
291 .id(SharedString::from(format!("process-entry-{ix}-command")))
292 .tooltip(Tooltip::text(
293 candidate
294 .command
295 .clone()
296 .into_iter()
297 .collect::<Vec<_>>()
298 .join(" "),
299 ))
300 .child(
301 Label::new(format!(
302 "{} {}",
303 candidate.name,
304 candidate
305 .command
306 .clone()
307 .into_iter()
308 .skip(1)
309 .collect::<Vec<_>>()
310 .join(" ")
311 ))
312 .size(LabelSize::Small)
313 .color(Color::Muted),
314 ),
315 ),
316 ),
317 )
318 }
319}
320
321#[cfg(any(test, feature = "test-support"))]
322pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
323 modal.picker.read_with(cx, |picker, _| {
324 picker
325 .delegate
326 .matches
327 .iter()
328 .map(|hit| hit.string.clone())
329 .collect::<Vec<_>>()
330 })
331}