attach_modal.rs

  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}