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 window: &mut Window,
58 cx: &mut Context<Self>,
59 ) -> Self {
60 let mut processes: Vec<_> = System::new_all()
61 .processes()
62 .values()
63 .map(|process| {
64 let name = process.name().to_string_lossy().into_owned();
65 Candidate {
66 name: name.into(),
67 pid: process.pid().as_u32(),
68 command: process
69 .cmd()
70 .iter()
71 .map(|s| s.to_string_lossy().to_string())
72 .collect::<Vec<_>>(),
73 }
74 })
75 .collect();
76 processes.sort_by_key(|k| k.name.clone());
77 Self::with_processes(project, debug_config, processes, window, cx)
78 }
79
80 pub(super) fn with_processes(
81 project: Entity<project::Project>,
82 debug_config: task::DebugTaskDefinition,
83 processes: Vec<Candidate>,
84 window: &mut Window,
85 cx: &mut Context<Self>,
86 ) -> Self {
87 let adapter = project
88 .read(cx)
89 .debug_adapters()
90 .adapter(&debug_config.adapter);
91 let filter = LazyCell::new(|| adapter.map(|adapter| adapter.attach_processes_filter()));
92 let processes = processes
93 .into_iter()
94 .filter(|process| {
95 filter
96 .as_ref()
97 .map_or(false, |filter| filter.is_match(&process.name))
98 })
99 .collect();
100 let picker = cx.new(|cx| {
101 Picker::uniform_list(
102 AttachModalDelegate::new(project, debug_config, processes),
103 window,
104 cx,
105 )
106 });
107 Self {
108 _subscription: cx.subscribe(&picker, |_, _, _, cx| {
109 cx.emit(DismissEvent);
110 }),
111 picker,
112 }
113 }
114}
115
116impl Render for AttachModal {
117 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
118 v_flex()
119 .key_context("AttachModal")
120 .w(rems(34.))
121 .child(self.picker.clone())
122 }
123}
124
125impl EventEmitter<DismissEvent> for AttachModal {}
126
127impl Focusable for AttachModal {
128 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
129 self.picker.read(cx).focus_handle(cx)
130 }
131}
132
133impl ModalView for AttachModal {}
134
135impl PickerDelegate for AttachModalDelegate {
136 type ListItem = ListItem;
137
138 fn match_count(&self) -> usize {
139 self.matches.len()
140 }
141
142 fn selected_index(&self) -> usize {
143 self.selected_index
144 }
145
146 fn set_selected_index(
147 &mut self,
148 ix: usize,
149 _window: &mut Window,
150 _: &mut Context<Picker<Self>>,
151 ) {
152 self.selected_index = ix;
153 }
154
155 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
156 self.placeholder_text.clone()
157 }
158
159 fn update_matches(
160 &mut self,
161 query: String,
162 _window: &mut Window,
163 cx: &mut Context<Picker<Self>>,
164 ) -> gpui::Task<()> {
165 cx.spawn(async move |this, cx| {
166 let Some(processes) = this
167 .update(cx, |this, _| this.delegate.candidates.clone())
168 .ok()
169 else {
170 return;
171 };
172
173 let matches = fuzzy::match_strings(
174 &processes
175 .iter()
176 .enumerate()
177 .map(|(id, candidate)| {
178 StringMatchCandidate::new(
179 id,
180 format!(
181 "{} {} {}",
182 candidate.command.join(" "),
183 candidate.pid,
184 candidate.name
185 )
186 .as_str(),
187 )
188 })
189 .collect::<Vec<_>>(),
190 &query,
191 true,
192 100,
193 &Default::default(),
194 cx.background_executor().clone(),
195 )
196 .await;
197
198 this.update(cx, |this, _| {
199 let delegate = &mut this.delegate;
200
201 delegate.matches = matches;
202
203 if delegate.matches.is_empty() {
204 delegate.selected_index = 0;
205 } else {
206 delegate.selected_index =
207 delegate.selected_index.min(delegate.matches.len() - 1);
208 }
209 })
210 .ok();
211 })
212 }
213
214 fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
215 let candidate = self
216 .matches
217 .get(self.selected_index())
218 .and_then(|current_match| {
219 let ix = current_match.candidate_id;
220 self.candidates.get(ix)
221 });
222
223 let Some(candidate) = candidate else {
224 return cx.emit(DismissEvent);
225 };
226
227 match &mut self.debug_config.request {
228 DebugRequestType::Attach(config) => {
229 config.process_id = Some(candidate.pid);
230 }
231 DebugRequestType::Launch(_) => {
232 debug_panic!("Debugger attach modal used on launch debug config");
233 return;
234 }
235 }
236
237 let config = self.debug_config.clone();
238 self.project
239 .update(cx, |project, cx| {
240 #[cfg(any(test, feature = "test-support"))]
241 let ret = project.fake_debug_session(config.request, None, false, cx);
242 #[cfg(not(any(test, feature = "test-support")))]
243 let ret = project.start_debug_session(config.into(), cx);
244 ret
245 })
246 .detach_and_log_err(cx);
247
248 cx.emit(DismissEvent);
249 }
250
251 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
252 self.selected_index = 0;
253
254 cx.emit(DismissEvent);
255 }
256
257 fn render_match(
258 &self,
259 ix: usize,
260 selected: bool,
261 _window: &mut Window,
262 _: &mut Context<Picker<Self>>,
263 ) -> Option<Self::ListItem> {
264 let hit = &self.matches[ix];
265 let candidate = self.candidates.get(hit.candidate_id)?;
266
267 Some(
268 ListItem::new(SharedString::from(format!("process-entry-{ix}")))
269 .inset(true)
270 .spacing(ListItemSpacing::Sparse)
271 .toggle_state(selected)
272 .child(
273 v_flex()
274 .items_start()
275 .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
276 .child(
277 div()
278 .id(SharedString::from(format!("process-entry-{ix}-command")))
279 .tooltip(Tooltip::text(
280 candidate
281 .command
282 .clone()
283 .into_iter()
284 .collect::<Vec<_>>()
285 .join(" "),
286 ))
287 .child(
288 Label::new(format!(
289 "{} {}",
290 candidate.name,
291 candidate
292 .command
293 .clone()
294 .into_iter()
295 .skip(1)
296 .collect::<Vec<_>>()
297 .join(" ")
298 ))
299 .size(LabelSize::Small)
300 .color(Color::Muted),
301 ),
302 ),
303 ),
304 )
305 }
306}
307
308#[cfg(any(test, feature = "test-support"))]
309pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
310 modal.picker.update(cx, |picker, _| {
311 picker
312 .delegate
313 .matches
314 .iter()
315 .map(|hit| hit.string.clone())
316 .collect::<Vec<_>>()
317 })
318}