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}