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