attach_modal.rs

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