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, secondary: 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 workspace = self.workspace.clone();
233        let Some(panel) = workspace
234            .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
235            .ok()
236            .flatten()
237        else {
238            return;
239        };
240
241        if secondary {
242            // let Some(id) = worktree_id else { return };
243            // cx.spawn_in(window, async move |_, cx| {
244            //     panel
245            //         .update_in(cx, |debug_panel, window, cx| {
246            //             debug_panel.save_scenario(&debug_scenario, id, window, cx)
247            //         })?
248            //         .await?;
249            //     anyhow::Ok(())
250            // })
251            // .detach_and_log_err(cx);
252        }
253        let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
254            registry.adapter(&self.definition.adapter)
255        }) else {
256            return;
257        };
258
259        let definition = self.definition.clone();
260        cx.spawn_in(window, async move |this, cx| {
261            let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
262                return;
263            };
264
265            panel
266                .update_in(cx, |panel, window, cx| {
267                    panel.start_session(scenario, Default::default(), None, None, window, cx);
268                })
269                .ok();
270            this.update(cx, |_, cx| {
271                cx.emit(DismissEvent);
272            })
273            .ok();
274        })
275        .detach();
276    }
277
278    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
279        self.selected_index = 0;
280
281        cx.emit(DismissEvent);
282    }
283
284    fn render_match(
285        &self,
286        ix: usize,
287        selected: bool,
288        _window: &mut Window,
289        _: &mut Context<Picker<Self>>,
290    ) -> Option<Self::ListItem> {
291        let hit = &self.matches[ix];
292        let candidate = self.candidates.get(hit.candidate_id)?;
293
294        Some(
295            ListItem::new(SharedString::from(format!("process-entry-{ix}")))
296                .inset(true)
297                .spacing(ListItemSpacing::Sparse)
298                .toggle_state(selected)
299                .child(
300                    v_flex()
301                        .items_start()
302                        .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
303                        .child(
304                            div()
305                                .id(SharedString::from(format!("process-entry-{ix}-command")))
306                                .tooltip(Tooltip::text(
307                                    candidate
308                                        .command
309                                        .clone()
310                                        .into_iter()
311                                        .collect::<Vec<_>>()
312                                        .join(" "),
313                                ))
314                                .child(
315                                    Label::new(format!(
316                                        "{} {}",
317                                        candidate.name,
318                                        candidate
319                                            .command
320                                            .clone()
321                                            .into_iter()
322                                            .skip(1)
323                                            .collect::<Vec<_>>()
324                                            .join(" ")
325                                    ))
326                                    .size(LabelSize::Small)
327                                    .color(Color::Muted),
328                                ),
329                        ),
330                ),
331        )
332    }
333}
334
335#[cfg(any(test, feature = "test-support"))]
336pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
337    modal.picker.read_with(cx, |picker, _| {
338        picker
339            .delegate
340            .matches
341            .iter()
342            .map(|hit| hit.string.clone())
343            .collect::<Vec<_>>()
344    })
345}