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