1use dap::DebugRequest;
2use dap::adapters::DebugTaskDefinition;
3use fuzzy::{StringMatch, StringMatchCandidate};
4use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
5use gpui::{Subscription, WeakEntity};
6use picker::{Picker, PickerDelegate};
7
8use std::sync::Arc;
9use sysinfo::System;
10use ui::{Context, Tooltip, prelude::*};
11use ui::{ListItem, ListItemSpacing};
12use util::debug_panic;
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: DebugTaskDefinition,
29 workspace: WeakEntity<Workspace>,
30 candidates: Arc<[Candidate]>,
31}
32
33impl AttachModalDelegate {
34 fn new(
35 workspace: WeakEntity<Workspace>,
36 definition: DebugTaskDefinition,
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: DebugTaskDefinition,
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: DebugTaskDefinition,
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 .update(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 100,
187 &Default::default(),
188 cx.background_executor().clone(),
189 )
190 .await;
191
192 this.update(cx, |this, _| {
193 let delegate = &mut this.delegate;
194
195 delegate.matches = matches;
196
197 if delegate.matches.is_empty() {
198 delegate.selected_index = 0;
199 } else {
200 delegate.selected_index =
201 delegate.selected_index.min(delegate.matches.len() - 1);
202 }
203 })
204 .ok();
205 })
206 }
207
208 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
209 let candidate = self
210 .matches
211 .get(self.selected_index())
212 .and_then(|current_match| {
213 let ix = current_match.candidate_id;
214 self.candidates.get(ix)
215 });
216
217 let Some(candidate) = candidate else {
218 return cx.emit(DismissEvent);
219 };
220
221 match &mut self.definition.request {
222 DebugRequest::Attach(config) => {
223 config.process_id = Some(candidate.pid);
224 }
225 DebugRequest::Launch(_) => {
226 debug_panic!("Debugger attach modal used on launch debug config");
227 return;
228 }
229 }
230
231 let scenario = self.definition.to_scenario();
232
233 let panel = self
234 .workspace
235 .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
236 .ok()
237 .flatten();
238 if let Some(panel) = panel {
239 panel.update(cx, |panel, cx| {
240 panel.start_session(scenario, Default::default(), None, None, window, cx);
241 });
242 }
243
244 cx.emit(DismissEvent);
245 }
246
247 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
248 self.selected_index = 0;
249
250 cx.emit(DismissEvent);
251 }
252
253 fn render_match(
254 &self,
255 ix: usize,
256 selected: bool,
257 _window: &mut Window,
258 _: &mut Context<Picker<Self>>,
259 ) -> Option<Self::ListItem> {
260 let hit = &self.matches[ix];
261 let candidate = self.candidates.get(hit.candidate_id)?;
262
263 Some(
264 ListItem::new(SharedString::from(format!("process-entry-{ix}")))
265 .inset(true)
266 .spacing(ListItemSpacing::Sparse)
267 .toggle_state(selected)
268 .child(
269 v_flex()
270 .items_start()
271 .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
272 .child(
273 div()
274 .id(SharedString::from(format!("process-entry-{ix}-command")))
275 .tooltip(Tooltip::text(
276 candidate
277 .command
278 .clone()
279 .into_iter()
280 .collect::<Vec<_>>()
281 .join(" "),
282 ))
283 .child(
284 Label::new(format!(
285 "{} {}",
286 candidate.name,
287 candidate
288 .command
289 .clone()
290 .into_iter()
291 .skip(1)
292 .collect::<Vec<_>>()
293 .join(" ")
294 ))
295 .size(LabelSize::Small)
296 .color(Color::Muted),
297 ),
298 ),
299 ),
300 )
301 }
302}
303
304#[cfg(any(test, feature = "test-support"))]
305pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
306 modal.picker.update(cx, |picker, _| {
307 picker
308 .delegate
309 .matches
310 .iter()
311 .map(|hit| hit.string.clone())
312 .collect::<Vec<_>>()
313 })
314}