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