attach_modal.rs

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