attach_modal.rs

  1use dap::{DapRegistry, DebugRequest};
  2use futures::channel::oneshot;
  3use fuzzy::{StringMatch, StringMatchCandidate};
  4use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
  5use gpui::{Subscription, WeakEntity};
  6use picker::{Picker, PickerDelegate};
  7use project::Project;
  8use rpc::proto;
  9use task::ZedDebugConfig;
 10use util::debug_panic;
 11
 12use std::sync::Arc;
 13
 14use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
 15use ui::{Context, Tooltip, prelude::*};
 16use ui::{ListItem, ListItemSpacing};
 17use workspace::{ModalView, Workspace};
 18
 19use crate::debugger_panel::DebugPanel;
 20
 21#[derive(Debug, Clone)]
 22pub(super) struct Candidate {
 23    pub(super) pid: u32,
 24    pub(super) name: SharedString,
 25    pub(super) command: Vec<String>,
 26}
 27
 28pub(crate) enum ModalIntent {
 29    ResolveProcessId(Option<oneshot::Sender<Option<i32>>>),
 30    AttachToProcess(ZedDebugConfig),
 31}
 32
 33pub(crate) struct AttachModalDelegate {
 34    selected_index: usize,
 35    matches: Vec<StringMatch>,
 36    placeholder_text: Arc<str>,
 37    pub(crate) intent: ModalIntent,
 38    workspace: WeakEntity<Workspace>,
 39    candidates: Arc<[Candidate]>,
 40}
 41
 42impl AttachModalDelegate {
 43    fn new(
 44        workspace: WeakEntity<Workspace>,
 45        intent: ModalIntent,
 46        candidates: Arc<[Candidate]>,
 47    ) -> Self {
 48        Self {
 49            workspace,
 50            candidates,
 51            intent,
 52            selected_index: 0,
 53            matches: Vec::default(),
 54            placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
 55        }
 56    }
 57}
 58
 59pub struct AttachModal {
 60    _subscription: Subscription,
 61    pub(crate) picker: Entity<Picker<AttachModalDelegate>>,
 62}
 63
 64impl AttachModal {
 65    pub(crate) fn new(
 66        intent: ModalIntent,
 67        workspace: WeakEntity<Workspace>,
 68        project: Entity<Project>,
 69        modal: bool,
 70        window: &mut Window,
 71        cx: &mut Context<Self>,
 72    ) -> Self {
 73        let processes_task = get_processes_for_project(&project, cx);
 74
 75        let modal = Self::with_processes(workspace, Arc::new([]), modal, intent, window, cx);
 76
 77        cx.spawn_in(window, async move |this, cx| {
 78            let processes = processes_task.await;
 79            this.update_in(cx, |modal, window, cx| {
 80                modal.picker.update(cx, |picker, cx| {
 81                    picker.delegate.candidates = processes;
 82                    picker.refresh(window, cx);
 83                });
 84            })?;
 85            anyhow::Ok(())
 86        })
 87        .detach_and_log_err(cx);
 88
 89        modal
 90    }
 91
 92    pub(super) fn with_processes(
 93        workspace: WeakEntity<Workspace>,
 94        processes: Arc<[Candidate]>,
 95        modal: bool,
 96        intent: ModalIntent,
 97        window: &mut Window,
 98        cx: &mut Context<Self>,
 99    ) -> Self {
100        let picker = cx.new(|cx| {
101            Picker::uniform_list(
102                AttachModalDelegate::new(workspace, intent, processes),
103                window,
104                cx,
105            )
106            .modal(modal)
107        });
108        Self {
109            _subscription: cx.subscribe(&picker, |_, _, _, cx| {
110                cx.emit(DismissEvent);
111            }),
112            picker,
113        }
114    }
115}
116
117impl Render for AttachModal {
118    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
119        v_flex()
120            .key_context("AttachModal")
121            .track_focus(&self.focus_handle(cx))
122            .w(rems(34.))
123            .child(self.picker.clone())
124    }
125}
126
127impl EventEmitter<DismissEvent> for AttachModal {}
128
129impl Focusable for AttachModal {
130    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
131        self.picker.read(cx).focus_handle(cx)
132    }
133}
134
135impl ModalView for AttachModal {}
136
137impl PickerDelegate for AttachModalDelegate {
138    type ListItem = ListItem;
139
140    fn match_count(&self) -> usize {
141        self.matches.len()
142    }
143
144    fn selected_index(&self) -> usize {
145        self.selected_index
146    }
147
148    fn set_selected_index(
149        &mut self,
150        ix: usize,
151        _window: &mut Window,
152        _: &mut Context<Picker<Self>>,
153    ) {
154        self.selected_index = ix;
155    }
156
157    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
158        self.placeholder_text.clone()
159    }
160
161    fn update_matches(
162        &mut self,
163        query: String,
164        _window: &mut Window,
165        cx: &mut Context<Picker<Self>>,
166    ) -> gpui::Task<()> {
167        cx.spawn(async move |this, cx| {
168            let Some(processes) = this
169                .read_with(cx, |this, _| this.delegate.candidates.clone())
170                .ok()
171            else {
172                return;
173            };
174
175            let matches = fuzzy::match_strings(
176                &processes
177                    .iter()
178                    .enumerate()
179                    .map(|(id, candidate)| {
180                        StringMatchCandidate::new(
181                            id,
182                            format!(
183                                "{} {} {}",
184                                candidate.command.join(" "),
185                                candidate.pid,
186                                candidate.name
187                            )
188                            .as_str(),
189                        )
190                    })
191                    .collect::<Vec<_>>(),
192                &query,
193                true,
194                true,
195                100,
196                &Default::default(),
197                cx.background_executor().clone(),
198            )
199            .await;
200
201            this.update(cx, |this, _| {
202                let delegate = &mut this.delegate;
203
204                delegate.matches = matches;
205
206                if delegate.matches.is_empty() {
207                    delegate.selected_index = 0;
208                } else {
209                    delegate.selected_index =
210                        delegate.selected_index.min(delegate.matches.len() - 1);
211                }
212            })
213            .ok();
214        })
215    }
216
217    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
218        let candidate = self
219            .matches
220            .get(self.selected_index())
221            .and_then(|current_match| {
222                let ix = current_match.candidate_id;
223                self.candidates.get(ix)
224            });
225
226        match &mut self.intent {
227            ModalIntent::ResolveProcessId(sender) => {
228                cx.emit(DismissEvent);
229
230                if let Some(sender) = sender.take() {
231                    sender
232                        .send(candidate.map(|candidate| candidate.pid as i32))
233                        .ok();
234                }
235            }
236            ModalIntent::AttachToProcess(definition) => {
237                let Some(candidate) = candidate else {
238                    return cx.emit(DismissEvent);
239                };
240
241                match &mut definition.request {
242                    DebugRequest::Attach(config) => {
243                        config.process_id = Some(candidate.pid);
244                    }
245                    DebugRequest::Launch(_) => {
246                        debug_panic!("Debugger attach modal used on launch debug config");
247                        return;
248                    }
249                }
250
251                let workspace = self.workspace.clone();
252                let Some(panel) = workspace
253                    .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
254                    .ok()
255                    .flatten()
256                else {
257                    return;
258                };
259
260                let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
261                    registry.adapter(&definition.adapter)
262                }) else {
263                    return;
264                };
265
266                let definition = definition.clone();
267                cx.spawn_in(window, async move |this, cx| {
268                    let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
269                        return;
270                    };
271
272                    panel
273                        .update_in(cx, |panel, window, cx| {
274                            panel.start_session(
275                                scenario,
276                                Default::default(),
277                                None,
278                                None,
279                                window,
280                                cx,
281                            );
282                        })
283                        .ok();
284                    this.update(cx, |_, cx| {
285                        cx.emit(DismissEvent);
286                    })
287                    .ok();
288                })
289                .detach();
290            }
291        }
292    }
293
294    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
295        self.selected_index = 0;
296
297        match &mut self.intent {
298            ModalIntent::ResolveProcessId(sender) => {
299                if let Some(sender) = sender.take() {
300                    sender.send(None).ok();
301                }
302            }
303            ModalIntent::AttachToProcess(_) => {}
304        }
305
306        cx.emit(DismissEvent);
307    }
308
309    fn render_match(
310        &self,
311        ix: usize,
312        selected: bool,
313        _window: &mut Window,
314        _: &mut Context<Picker<Self>>,
315    ) -> Option<Self::ListItem> {
316        let hit = &self.matches.get(ix)?;
317        let candidate = self.candidates.get(hit.candidate_id)?;
318
319        Some(
320            ListItem::new(SharedString::from(format!("process-entry-{ix}")))
321                .inset(true)
322                .spacing(ListItemSpacing::Sparse)
323                .toggle_state(selected)
324                .child(
325                    v_flex()
326                        .items_start()
327                        .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
328                        .child(
329                            div()
330                                .id(SharedString::from(format!("process-entry-{ix}-command")))
331                                .tooltip(Tooltip::text(
332                                    candidate
333                                        .command
334                                        .clone()
335                                        .into_iter()
336                                        .collect::<Vec<_>>()
337                                        .join(" "),
338                                ))
339                                .child(
340                                    Label::new(format!(
341                                        "{} {}",
342                                        candidate.name,
343                                        candidate
344                                            .command
345                                            .clone()
346                                            .into_iter()
347                                            .skip(1)
348                                            .collect::<Vec<_>>()
349                                            .join(" ")
350                                    ))
351                                    .size(LabelSize::Small)
352                                    .color(Color::Muted),
353                                ),
354                        ),
355                ),
356        )
357    }
358}
359
360fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
361    let project = project.read(cx);
362
363    if let Some(remote_client) = project.remote_client() {
364        let proto_client = remote_client.read(cx).proto_client();
365        cx.background_spawn(async move {
366            let response = proto_client
367                .request(proto::GetProcesses {
368                    project_id: proto::REMOTE_SERVER_PROJECT_ID,
369                })
370                .await
371                .unwrap_or_else(|_| proto::GetProcessesResponse {
372                    processes: Vec::new(),
373                });
374
375            let mut processes: Vec<Candidate> = response
376                .processes
377                .into_iter()
378                .map(|p| Candidate {
379                    pid: p.pid,
380                    name: p.name.into(),
381                    command: p.command,
382                })
383                .collect();
384
385            processes.sort_by_key(|k| k.name.clone());
386            Arc::from(processes.into_boxed_slice())
387        })
388    } else {
389        let refresh_kind = RefreshKind::nothing().with_processes(
390            ProcessRefreshKind::nothing()
391                .without_tasks()
392                .with_cmd(UpdateKind::Always),
393        );
394        let mut processes: Box<[_]> = System::new_with_specifics(refresh_kind)
395            .processes()
396            .values()
397            .map(|process| {
398                let name = process.name().to_string_lossy().into_owned();
399                Candidate {
400                    name: name.into(),
401                    pid: process.pid().as_u32(),
402                    command: process
403                        .cmd()
404                        .iter()
405                        .map(|s| s.to_string_lossy().into_owned())
406                        .collect::<Vec<_>>(),
407                }
408            })
409            .collect();
410        processes.sort_by_key(|k| k.name.clone());
411        let processes = processes.into_iter().collect();
412        Task::ready(processes)
413    }
414}
415
416#[cfg(test)]
417pub(crate) fn set_candidates(
418    modal: &AttachModal,
419    candidates: Arc<[Candidate]>,
420    window: &mut Window,
421    cx: &mut Context<AttachModal>,
422) {
423    modal.picker.update(cx, |picker, cx| {
424        picker.delegate.candidates = candidates;
425        picker.refresh(window, cx);
426    });
427}
428
429#[cfg(test)]
430pub(crate) fn process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
431    modal.picker.read_with(cx, |picker, _| {
432        picker
433            .delegate
434            .matches
435            .iter()
436            .map(|hit| hit.string.clone())
437            .collect::<Vec<_>>()
438    })
439}