attach_modal.rs

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