1use dap::{DapRegistry, DebugRequest};
2use fuzzy::{StringMatch, StringMatchCandidate};
3use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
4use gpui::{Subscription, WeakEntity};
5use picker::{Picker, PickerDelegate};
6use task::ZedDebugConfig;
7use util::debug_panic;
8
9use std::sync::Arc;
10use sysinfo::System;
11use ui::{Context, Tooltip, prelude::*};
12use ui::{ListItem, ListItemSpacing};
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: ZedDebugConfig,
29 workspace: WeakEntity<Workspace>,
30 candidates: Arc<[Candidate]>,
31}
32
33impl AttachModalDelegate {
34 fn new(
35 workspace: WeakEntity<Workspace>,
36 definition: ZedDebugConfig,
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: ZedDebugConfig,
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: ZedDebugConfig,
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 .read_with(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 Some(scenario) = cx.read_global::<DapRegistry, _>(|registry, _| {
232 registry
233 .adapter(&self.definition.adapter)
234 .and_then(|adapter| adapter.config_from_zed_format(self.definition.clone()).ok())
235 }) else {
236 return;
237 };
238
239 let panel = self
240 .workspace
241 .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
242 .ok()
243 .flatten();
244 if let Some(panel) = panel {
245 panel.update(cx, |panel, cx| {
246 panel.start_session(scenario, Default::default(), None, None, window, cx);
247 });
248 }
249
250 cx.emit(DismissEvent);
251 }
252
253 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
254 self.selected_index = 0;
255
256 cx.emit(DismissEvent);
257 }
258
259 fn render_match(
260 &self,
261 ix: usize,
262 selected: bool,
263 _window: &mut Window,
264 _: &mut Context<Picker<Self>>,
265 ) -> Option<Self::ListItem> {
266 let hit = &self.matches[ix];
267 let candidate = self.candidates.get(hit.candidate_id)?;
268
269 Some(
270 ListItem::new(SharedString::from(format!("process-entry-{ix}")))
271 .inset(true)
272 .spacing(ListItemSpacing::Sparse)
273 .toggle_state(selected)
274 .child(
275 v_flex()
276 .items_start()
277 .child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
278 .child(
279 div()
280 .id(SharedString::from(format!("process-entry-{ix}-command")))
281 .tooltip(Tooltip::text(
282 candidate
283 .command
284 .clone()
285 .into_iter()
286 .collect::<Vec<_>>()
287 .join(" "),
288 ))
289 .child(
290 Label::new(format!(
291 "{} {}",
292 candidate.name,
293 candidate
294 .command
295 .clone()
296 .into_iter()
297 .skip(1)
298 .collect::<Vec<_>>()
299 .join(" ")
300 ))
301 .size(LabelSize::Small)
302 .color(Color::Muted),
303 ),
304 ),
305 ),
306 )
307 }
308}
309
310#[cfg(any(test, feature = "test-support"))]
311pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
312 modal.picker.read_with(cx, |picker, _| {
313 picker
314 .delegate
315 .matches
316 .iter()
317 .map(|hit| hit.string.clone())
318 .collect::<Vec<_>>()
319 })
320}