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