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