1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use dap::StackFrameId;
7use gpui::{
8 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
9 Subscription, Task, WeakEntity, list,
10};
11
12use language::PointUtf16;
13use project::debugger::breakpoint_store::ActiveStackFrame;
14use project::debugger::session::{Session, SessionEvent, StackFrame};
15use project::{ProjectItem, ProjectPath};
16use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
17use util::ResultExt;
18use workspace::Workspace;
19
20use super::RunningState;
21
22#[derive(Debug)]
23pub enum StackFrameListEvent {
24 SelectedStackFrameChanged(StackFrameId),
25}
26
27pub struct StackFrameList {
28 list: ListState,
29 focus_handle: FocusHandle,
30 _subscription: Subscription,
31 session: Entity<Session>,
32 state: WeakEntity<RunningState>,
33 entries: Vec<StackFrameEntry>,
34 workspace: WeakEntity<Workspace>,
35 selected_stack_frame_id: Option<StackFrameId>,
36 scrollbar_state: ScrollbarState,
37 _refresh_task: Task<()>,
38}
39
40#[allow(clippy::large_enum_variant)]
41#[derive(Debug, PartialEq, Eq)]
42pub enum StackFrameEntry {
43 Normal(dap::StackFrame),
44 Collapsed(Vec<dap::StackFrame>),
45}
46
47impl StackFrameList {
48 pub fn new(
49 workspace: WeakEntity<Workspace>,
50 session: Entity<Session>,
51 state: WeakEntity<RunningState>,
52 window: &mut Window,
53 cx: &mut Context<Self>,
54 ) -> Self {
55 let weak_entity = cx.weak_entity();
56 let focus_handle = cx.focus_handle();
57
58 let list = ListState::new(
59 0,
60 gpui::ListAlignment::Top,
61 px(1000.),
62 move |ix, _window, cx| {
63 weak_entity
64 .upgrade()
65 .map(|stack_frame_list| {
66 stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
67 })
68 .unwrap_or(div().into_any())
69 },
70 );
71
72 let _subscription =
73 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
74 SessionEvent::Threads => {
75 this.schedule_refresh(false, window, cx);
76 }
77 SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
78 this.schedule_refresh(true, window, cx);
79 }
80 _ => {}
81 });
82
83 let mut this = Self {
84 scrollbar_state: ScrollbarState::new(list.clone()),
85 list,
86 session,
87 workspace,
88 focus_handle,
89 state,
90 _subscription,
91 entries: Default::default(),
92 selected_stack_frame_id: None,
93 _refresh_task: Task::ready(()),
94 };
95 this.schedule_refresh(true, window, cx);
96 this
97 }
98
99 #[cfg(test)]
100 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
101 &self.entries
102 }
103
104 #[cfg(test)]
105 pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
106 self.entries
107 .iter()
108 .flat_map(|frame| match frame {
109 StackFrameEntry::Normal(frame) => vec![frame.clone()],
110 StackFrameEntry::Collapsed(frames) => frames.clone(),
111 })
112 .collect::<Vec<_>>()
113 }
114
115 fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
116 self.state
117 .read_with(cx, |state, _| state.thread_id)
118 .log_err()
119 .flatten()
120 .map(|thread_id| {
121 self.session
122 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
123 })
124 .unwrap_or_default()
125 }
126
127 #[cfg(test)]
128 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
129 self.stack_frames(cx)
130 .into_iter()
131 .map(|stack_frame| stack_frame.dap.clone())
132 .collect()
133 }
134
135 pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
136 self.stack_frames(cx)
137 .first()
138 .map(|stack_frame| stack_frame.dap.id)
139 .unwrap_or(0)
140 }
141
142 pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
143 self.selected_stack_frame_id
144 }
145
146 pub(super) fn schedule_refresh(
147 &mut self,
148 select_first: bool,
149 window: &mut Window,
150 cx: &mut Context<Self>,
151 ) {
152 const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
153
154 self._refresh_task = cx.spawn_in(window, async move |this, cx| {
155 let debounce = this
156 .update(cx, |this, cx| {
157 let new_stack_frames = this.stack_frames(cx);
158 new_stack_frames.is_empty() && !this.entries.is_empty()
159 })
160 .ok()
161 .unwrap_or_default();
162
163 if debounce {
164 cx.background_executor().timer(REFRESH_DEBOUNCE).await;
165 }
166 this.update_in(cx, |this, window, cx| {
167 this.build_entries(select_first, window, cx);
168 cx.notify();
169 })
170 .ok();
171 })
172 }
173
174 pub fn build_entries(
175 &mut self,
176 select_first_stack_frame: bool,
177 window: &mut Window,
178 cx: &mut Context<Self>,
179 ) {
180 let mut entries = Vec::new();
181 let mut collapsed_entries = Vec::new();
182 let mut current_stack_frame = None;
183
184 let stack_frames = self.stack_frames(cx);
185 for stack_frame in &stack_frames {
186 match stack_frame.dap.presentation_hint {
187 Some(dap::StackFramePresentationHint::Deemphasize) => {
188 collapsed_entries.push(stack_frame.dap.clone());
189 }
190 _ => {
191 let collapsed_entries = std::mem::take(&mut collapsed_entries);
192 if !collapsed_entries.is_empty() {
193 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
194 }
195
196 current_stack_frame.get_or_insert(&stack_frame.dap);
197 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
198 }
199 }
200 }
201
202 let collapsed_entries = std::mem::take(&mut collapsed_entries);
203 if !collapsed_entries.is_empty() {
204 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
205 }
206
207 std::mem::swap(&mut self.entries, &mut entries);
208 self.list.reset(self.entries.len());
209
210 if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
211 {
212 self.select_stack_frame(current_stack_frame, true, window, cx)
213 .detach_and_log_err(cx);
214 }
215
216 cx.notify();
217 }
218
219 pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
220 if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
221 let frame = self
222 .entries
223 .iter()
224 .find_map(|entry| match entry {
225 StackFrameEntry::Normal(dap) => {
226 if dap.id == selected_stack_frame_id {
227 Some(dap)
228 } else {
229 None
230 }
231 }
232 StackFrameEntry::Collapsed(daps) => {
233 daps.iter().find(|dap| dap.id == selected_stack_frame_id)
234 }
235 })
236 .cloned();
237
238 if let Some(frame) = frame.as_ref() {
239 self.select_stack_frame(frame, true, window, cx)
240 .detach_and_log_err(cx);
241 }
242 }
243 }
244
245 pub fn select_stack_frame(
246 &mut self,
247 stack_frame: &dap::StackFrame,
248 go_to_stack_frame: bool,
249 window: &Window,
250 cx: &mut Context<Self>,
251 ) -> Task<Result<()>> {
252 self.selected_stack_frame_id = Some(stack_frame.id);
253
254 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
255 stack_frame.id,
256 ));
257 cx.notify();
258
259 if !go_to_stack_frame {
260 return Task::ready(Ok(()));
261 };
262
263 let row = (stack_frame.line.saturating_sub(1)) as u32;
264
265 let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
266 return Task::ready(Err(anyhow!("Project path not found")));
267 };
268
269 let stack_frame_id = stack_frame.id;
270 cx.spawn_in(window, async move |this, cx| {
271 let (worktree, relative_path) = this
272 .update(cx, |this, cx| {
273 this.workspace.update(cx, |workspace, cx| {
274 workspace.project().update(cx, |this, cx| {
275 this.find_or_create_worktree(&abs_path, false, cx)
276 })
277 })
278 })??
279 .await?;
280 let buffer = this
281 .update(cx, |this, cx| {
282 this.workspace.update(cx, |this, cx| {
283 this.project().update(cx, |this, cx| {
284 let worktree_id = worktree.read(cx).id();
285 this.open_buffer(
286 ProjectPath {
287 worktree_id,
288 path: relative_path.into(),
289 },
290 cx,
291 )
292 })
293 })
294 })??
295 .await?;
296 let position = buffer.update(cx, |this, _| {
297 this.snapshot().anchor_after(PointUtf16::new(row, 0))
298 })?;
299 this.update_in(cx, |this, window, cx| {
300 this.workspace.update(cx, |workspace, cx| {
301 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
302 anyhow!("Could not select a stack frame for unnamed buffer")
303 })?;
304 anyhow::Ok(workspace.open_path_preview(
305 project_path,
306 None,
307 false,
308 true,
309 true,
310 window,
311 cx,
312 ))
313 })
314 })???
315 .await?;
316
317 this.update(cx, |this, cx| {
318 let Some(thread_id) = this.state.read_with(cx, |state, _| state.thread_id)? else {
319 return Err(anyhow!("No selected thread ID found"));
320 };
321
322 this.workspace.update(cx, |workspace, cx| {
323 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
324
325 breakpoint_store.update(cx, |store, cx| {
326 store.set_active_position(
327 ActiveStackFrame {
328 session_id: this.session.read(cx).session_id(),
329 thread_id,
330 stack_frame_id,
331 path: abs_path,
332 position,
333 },
334 cx,
335 );
336 })
337 })
338 })?
339 })
340 }
341
342 fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
343 stack_frame.source.as_ref().and_then(|s| {
344 s.path
345 .as_deref()
346 .map(|path| Arc::<Path>::from(Path::new(path)))
347 })
348 }
349
350 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
351 self.session.update(cx, |state, cx| {
352 state.restart_stack_frame(stack_frame_id, cx)
353 });
354 }
355
356 fn render_normal_entry(
357 &self,
358 stack_frame: &dap::StackFrame,
359 cx: &mut Context<Self>,
360 ) -> AnyElement {
361 let source = stack_frame.source.clone();
362 let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
363
364 let path = source.clone().and_then(|s| s.path.or(s.name));
365 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
366 let formatted_path = formatted_path.map(|path| {
367 Label::new(path)
368 .size(LabelSize::XSmall)
369 .line_height_style(LineHeightStyle::UiLabel)
370 .truncate()
371 .color(Color::Muted)
372 });
373
374 let supports_frame_restart = self
375 .session
376 .read(cx)
377 .capabilities()
378 .supports_restart_frame
379 .unwrap_or_default();
380
381 let should_deemphasize = matches!(
382 stack_frame.presentation_hint,
383 Some(
384 dap::StackFramePresentationHint::Subtle
385 | dap::StackFramePresentationHint::Deemphasize
386 )
387 );
388 h_flex()
389 .rounded_md()
390 .justify_between()
391 .w_full()
392 .group("")
393 .id(("stack-frame", stack_frame.id))
394 .p_1()
395 .when(is_selected_frame, |this| {
396 this.bg(cx.theme().colors().element_hover)
397 })
398 .on_click(cx.listener({
399 let stack_frame = stack_frame.clone();
400 move |this, _, window, cx| {
401 this.select_stack_frame(&stack_frame, true, window, cx)
402 .detach_and_log_err(cx);
403 }
404 }))
405 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
406 .child(
407 v_flex()
408 .gap_0p5()
409 .child(
410 Label::new(stack_frame.name.clone())
411 .size(LabelSize::Small)
412 .truncate()
413 .when(should_deemphasize, |this| this.color(Color::Muted)),
414 )
415 .children(formatted_path),
416 )
417 .when(
418 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
419 |this| {
420 this.child(
421 h_flex()
422 .id(("restart-stack-frame", stack_frame.id))
423 .visible_on_hover("")
424 .absolute()
425 .right_2()
426 .overflow_hidden()
427 .rounded_md()
428 .border_1()
429 .border_color(cx.theme().colors().element_selected)
430 .bg(cx.theme().colors().element_background)
431 .hover(|style| {
432 style
433 .bg(cx.theme().colors().ghost_element_hover)
434 .cursor_pointer()
435 })
436 .child(
437 IconButton::new(
438 ("restart-stack-frame", stack_frame.id),
439 IconName::DebugRestart,
440 )
441 .icon_size(IconSize::Small)
442 .on_click(cx.listener({
443 let stack_frame_id = stack_frame.id;
444 move |this, _, _window, cx| {
445 this.restart_stack_frame(stack_frame_id, cx);
446 }
447 }))
448 .tooltip(move |window, cx| {
449 Tooltip::text("Restart Stack Frame")(window, cx)
450 }),
451 ),
452 )
453 },
454 )
455 .into_any()
456 }
457
458 pub fn expand_collapsed_entry(
459 &mut self,
460 ix: usize,
461 stack_frames: &Vec<dap::StackFrame>,
462 cx: &mut Context<Self>,
463 ) {
464 self.entries.splice(
465 ix..ix + 1,
466 stack_frames
467 .iter()
468 .map(|frame| StackFrameEntry::Normal(frame.clone())),
469 );
470 self.list.reset(self.entries.len());
471 cx.notify();
472 }
473
474 fn render_collapsed_entry(
475 &self,
476 ix: usize,
477 stack_frames: &Vec<dap::StackFrame>,
478 cx: &mut Context<Self>,
479 ) -> AnyElement {
480 let first_stack_frame = &stack_frames[0];
481
482 h_flex()
483 .rounded_md()
484 .justify_between()
485 .w_full()
486 .group("")
487 .id(("stack-frame", first_stack_frame.id))
488 .p_1()
489 .on_click(cx.listener({
490 let stack_frames = stack_frames.clone();
491 move |this, _, _window, cx| {
492 this.expand_collapsed_entry(ix, &stack_frames, cx);
493 }
494 }))
495 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
496 .child(
497 v_flex()
498 .text_ui_sm(cx)
499 .truncate()
500 .text_color(cx.theme().colors().text_muted)
501 .child(format!(
502 "Show {} more{}",
503 stack_frames.len(),
504 first_stack_frame
505 .source
506 .as_ref()
507 .and_then(|source| source.origin.as_ref())
508 .map_or(String::new(), |origin| format!(": {}", origin))
509 )),
510 )
511 .into_any()
512 }
513
514 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
515 match &self.entries[ix] {
516 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
517 StackFrameEntry::Collapsed(stack_frames) => {
518 self.render_collapsed_entry(ix, stack_frames, cx)
519 }
520 }
521 }
522
523 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
524 div()
525 .occlude()
526 .id("stack-frame-list-vertical-scrollbar")
527 .on_mouse_move(cx.listener(|_, _, _, cx| {
528 cx.notify();
529 cx.stop_propagation()
530 }))
531 .on_hover(|_, _, cx| {
532 cx.stop_propagation();
533 })
534 .on_any_mouse_down(|_, _, cx| {
535 cx.stop_propagation();
536 })
537 .on_mouse_up(
538 MouseButton::Left,
539 cx.listener(|_, _, _, cx| {
540 cx.stop_propagation();
541 }),
542 )
543 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
544 cx.notify();
545 }))
546 .h_full()
547 .absolute()
548 .right_1()
549 .top_1()
550 .bottom_0()
551 .w(px(12.))
552 .cursor_default()
553 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
554 }
555}
556
557impl Render for StackFrameList {
558 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
559 div()
560 .size_full()
561 .p_1()
562 .child(list(self.list.clone()).size_full())
563 .child(self.render_vertical_scrollbar(cx))
564 }
565}
566
567impl Focusable for StackFrameList {
568 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
569 self.focus_handle.clone()
570 }
571}
572
573impl EventEmitter<StackFrameListEvent> for StackFrameList {}