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::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}