attach_modal.rs

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