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