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                true,
187                100,
188                &Default::default(),
189                cx.background_executor().clone(),
190            )
191            .await;
192
193            this.update(cx, |this, _| {
194                let delegate = &mut this.delegate;
195
196                delegate.matches = matches;
197
198                if delegate.matches.is_empty() {
199                    delegate.selected_index = 0;
200                } else {
201                    delegate.selected_index =
202                        delegate.selected_index.min(delegate.matches.len() - 1);
203                }
204            })
205            .ok();
206        })
207    }
208
209    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
210        let candidate = self
211            .matches
212            .get(self.selected_index())
213            .and_then(|current_match| {
214                let ix = current_match.candidate_id;
215                self.candidates.get(ix)
216            });
217
218        let Some(candidate) = candidate else {
219            return cx.emit(DismissEvent);
220        };
221
222        match &mut self.definition.request {
223            DebugRequest::Attach(config) => {
224                config.process_id = Some(candidate.pid);
225            }
226            DebugRequest::Launch(_) => {
227                debug_panic!("Debugger attach modal used on launch debug config");
228                return;
229            }
230        }
231
232        let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
233            registry.adapter(&self.definition.adapter)
234        }) else {
235            return;
236        };
237
238        let workspace = self.workspace.clone();
239        let definition = self.definition.clone();
240        cx.spawn_in(window, async move |this, cx| {
241            let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
242                return;
243            };
244
245            let panel = workspace
246                .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
247                .ok()
248                .flatten();
249            if let Some(panel) = panel {
250                panel
251                    .update_in(cx, |panel, window, cx| {
252                        panel.start_session(scenario, Default::default(), None, None, window, cx);
253                    })
254                    .ok();
255            }
256            this.update(cx, |_, cx| {
257                cx.emit(DismissEvent);
258            })
259            .ok();
260        })
261        .detach();
262    }
263
264    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
265        self.selected_index = 0;
266
267        cx.emit(DismissEvent);
268    }
269
270    fn render_match(
271        &self,
272        ix: usize,
273        selected: bool,
274        _window: &mut Window,
275        _: &mut Context<Picker<Self>>,
276    ) -> Option<Self::ListItem> {
277        let hit = &self.matches[ix];
278        let candidate = self.candidates.get(hit.candidate_id)?;
279
280        Some(
281            ListItem::new(SharedString::from(format!("process-entry-{ix}")))
282                .inset(true)
283                .spacing(ListItemSpacing::Sparse)
284                .toggle_state(selected)
285                .child(
286                    v_flex()
287                        .items_start()
288                        .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
289                        .child(
290                            div()
291                                .id(SharedString::from(format!("process-entry-{ix}-command")))
292                                .tooltip(Tooltip::text(
293                                    candidate
294                                        .command
295                                        .clone()
296                                        .into_iter()
297                                        .collect::<Vec<_>>()
298                                        .join(" "),
299                                ))
300                                .child(
301                                    Label::new(format!(
302                                        "{} {}",
303                                        candidate.name,
304                                        candidate
305                                            .command
306                                            .clone()
307                                            .into_iter()
308                                            .skip(1)
309                                            .collect::<Vec<_>>()
310                                            .join(" ")
311                                    ))
312                                    .size(LabelSize::Small)
313                                    .color(Color::Muted),
314                                ),
315                        ),
316                ),
317        )
318    }
319}
320
321#[cfg(any(test, feature = "test-support"))]
322pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
323    modal.picker.read_with(cx, |picker, _| {
324        picker
325            .delegate
326            .matches
327            .iter()
328            .map(|hit| hit.string.clone())
329            .collect::<Vec<_>>()
330    })
331}