1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Context as _, Result, anyhow};
6use dap::StackFrameId;
7use db::kvp::KEY_VALUE_STORE;
8use gpui::{
9 Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
10 MouseButton, Stateful, Subscription, Task, WeakEntity, list,
11};
12use util::debug_panic;
13
14use crate::{StackTraceView, ToggleUserFrames};
15use language::PointUtf16;
16use project::debugger::breakpoint_store::ActiveStackFrame;
17use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
18use project::{ProjectItem, ProjectPath};
19use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
20use workspace::{ItemHandle, Workspace};
21
22use super::RunningState;
23
24#[derive(Debug)]
25pub enum StackFrameListEvent {
26 SelectedStackFrameChanged(StackFrameId),
27 BuiltEntries,
28}
29
30/// Represents the filter applied to the stack frame list
31#[derive(PartialEq, Eq, Copy, Clone)]
32enum StackFrameFilter {
33 /// Show all frames
34 All,
35 /// Show only frames from the user's code
36 OnlyUserFrames,
37}
38
39impl StackFrameFilter {
40 fn from_str_or_default(s: impl AsRef<str>) -> Self {
41 match s.as_ref() {
42 "user" => StackFrameFilter::OnlyUserFrames,
43 "all" => StackFrameFilter::All,
44 _ => StackFrameFilter::All,
45 }
46 }
47}
48
49impl From<StackFrameFilter> for String {
50 fn from(filter: StackFrameFilter) -> Self {
51 match filter {
52 StackFrameFilter::All => "all".to_string(),
53 StackFrameFilter::OnlyUserFrames => "user".to_string(),
54 }
55 }
56}
57
58pub struct StackFrameList {
59 focus_handle: FocusHandle,
60 _subscription: Subscription,
61 session: Entity<Session>,
62 state: WeakEntity<RunningState>,
63 entries: Vec<StackFrameEntry>,
64 workspace: WeakEntity<Workspace>,
65 selected_ix: Option<usize>,
66 opened_stack_frame_id: Option<StackFrameId>,
67 scrollbar_state: ScrollbarState,
68 list_state: ListState,
69 list_filter: StackFrameFilter,
70 filter_entries_indices: Vec<usize>,
71 error: Option<SharedString>,
72 _refresh_task: Task<()>,
73}
74
75#[derive(Debug, PartialEq, Eq)]
76pub enum StackFrameEntry {
77 Normal(dap::StackFrame),
78 /// Used to indicate that the frame is artificial and is a visual label or separator
79 Label(dap::StackFrame),
80 Collapsed(Vec<dap::StackFrame>),
81}
82
83impl StackFrameList {
84 pub fn new(
85 workspace: WeakEntity<Workspace>,
86 session: Entity<Session>,
87 state: WeakEntity<RunningState>,
88 window: &mut Window,
89 cx: &mut Context<Self>,
90 ) -> Self {
91 let focus_handle = cx.focus_handle();
92
93 let _subscription =
94 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
95 SessionEvent::Threads => {
96 this.schedule_refresh(false, window, cx);
97 }
98 SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
99 this.schedule_refresh(true, window, cx);
100 }
101 _ => {}
102 });
103
104 let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
105 let scrollbar_state = ScrollbarState::new(list_state.clone());
106
107 let list_filter = KEY_VALUE_STORE
108 .read_kvp(&format!(
109 "stack-frame-list-filter-{}",
110 session.read(cx).adapter().0
111 ))
112 .ok()
113 .flatten()
114 .map(StackFrameFilter::from_str_or_default)
115 .unwrap_or(StackFrameFilter::All);
116
117 let mut this = Self {
118 session,
119 workspace,
120 focus_handle,
121 state,
122 _subscription,
123 entries: Default::default(),
124 filter_entries_indices: Vec::default(),
125 error: None,
126 selected_ix: None,
127 opened_stack_frame_id: None,
128 list_filter,
129 list_state,
130 scrollbar_state,
131 _refresh_task: Task::ready(()),
132 };
133 this.schedule_refresh(true, window, cx);
134 this
135 }
136
137 #[cfg(test)]
138 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
139 &self.entries
140 }
141
142 pub(crate) fn flatten_entries(
143 &self,
144 show_collapsed: bool,
145 show_labels: bool,
146 ) -> Vec<dap::StackFrame> {
147 self.entries
148 .iter()
149 .enumerate()
150 .filter(|(ix, _)| {
151 self.list_filter == StackFrameFilter::All
152 || self
153 .filter_entries_indices
154 .binary_search_by_key(&ix, |ix| ix)
155 .is_ok()
156 })
157 .flat_map(|(_, frame)| match frame {
158 StackFrameEntry::Normal(frame) => vec![frame.clone()],
159 StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
160 StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
161 _ => vec![],
162 })
163 .collect::<Vec<_>>()
164 }
165
166 fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
167 if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
168 self.session
169 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
170 } else {
171 Ok(Vec::default())
172 }
173 }
174
175 #[cfg(test)]
176 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
177 self.stack_frames(cx)
178 .unwrap_or_default()
179 .into_iter()
180 .enumerate()
181 .filter(|(ix, _)| {
182 self.list_filter == StackFrameFilter::All
183 || self
184 .filter_entries_indices
185 .binary_search_by_key(&ix, |ix| ix)
186 .is_ok()
187 })
188 .map(|(_, stack_frame)| stack_frame.dap)
189 .collect()
190 }
191
192 pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
193 self.opened_stack_frame_id
194 }
195
196 pub(super) fn schedule_refresh(
197 &mut self,
198 select_first: bool,
199 window: &mut Window,
200 cx: &mut Context<Self>,
201 ) {
202 const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
203
204 self._refresh_task = cx.spawn_in(window, async move |this, cx| {
205 let debounce = this
206 .update(cx, |this, cx| {
207 let new_stack_frames = this.stack_frames(cx);
208 new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty()
209 })
210 .ok()
211 .unwrap_or_default();
212
213 if debounce {
214 cx.background_executor().timer(REFRESH_DEBOUNCE).await;
215 }
216 this.update_in(cx, |this, window, cx| {
217 this.build_entries(select_first, window, cx);
218 cx.notify();
219 })
220 .ok();
221 })
222 }
223
224 pub fn build_entries(
225 &mut self,
226 open_first_stack_frame: bool,
227 window: &mut Window,
228 cx: &mut Context<Self>,
229 ) {
230 let old_selected_frame_id = self
231 .selected_ix
232 .and_then(|ix| self.entries.get(ix))
233 .and_then(|entry| match entry {
234 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
235 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
236 });
237 let mut entries = Vec::new();
238 let mut collapsed_entries = Vec::new();
239 let mut first_stack_frame = None;
240 let mut first_stack_frame_with_path = None;
241
242 let stack_frames = match self.stack_frames(cx) {
243 Ok(stack_frames) => stack_frames,
244 Err(e) => {
245 self.error = Some(format!("{}", e).into());
246 self.entries.clear();
247 self.selected_ix = None;
248 self.list_state.reset(0);
249 cx.emit(StackFrameListEvent::BuiltEntries);
250 cx.notify();
251 return;
252 }
253 };
254
255 let worktree_prefixes: Vec<_> = self
256 .workspace
257 .read_with(cx, |workspace, cx| {
258 workspace
259 .visible_worktrees(cx)
260 .map(|tree| tree.read(cx).abs_path())
261 .collect()
262 })
263 .unwrap_or_default();
264
265 let mut filter_entries_indices = Vec::default();
266 for (ix, stack_frame) in stack_frames.iter().enumerate() {
267 let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
268 source.path.as_ref().is_some_and(|path| {
269 worktree_prefixes
270 .iter()
271 .filter_map(|tree| tree.to_str())
272 .any(|tree| path.starts_with(tree))
273 })
274 });
275
276 if frame_in_visible_worktree {
277 filter_entries_indices.push(ix);
278 }
279
280 match stack_frame.dap.presentation_hint {
281 Some(dap::StackFramePresentationHint::Deemphasize)
282 | Some(dap::StackFramePresentationHint::Subtle) => {
283 collapsed_entries.push(stack_frame.dap.clone());
284 }
285 Some(dap::StackFramePresentationHint::Label) => {
286 entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
287 }
288 _ => {
289 let collapsed_entries = std::mem::take(&mut collapsed_entries);
290 if !collapsed_entries.is_empty() {
291 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
292 }
293
294 first_stack_frame.get_or_insert(entries.len());
295
296 if stack_frame
297 .dap
298 .source
299 .as_ref()
300 .is_some_and(|source| source.path.is_some())
301 {
302 first_stack_frame_with_path.get_or_insert(entries.len());
303 }
304 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
305 }
306 }
307 }
308
309 let collapsed_entries = std::mem::take(&mut collapsed_entries);
310 if !collapsed_entries.is_empty() {
311 entries.push(StackFrameEntry::Collapsed(collapsed_entries));
312 self.filter_entries_indices.push(entries.len() - 1);
313 }
314 self.entries = entries;
315 self.filter_entries_indices = filter_entries_indices;
316
317 if let Some(ix) = first_stack_frame_with_path
318 .or(first_stack_frame)
319 .filter(|_| open_first_stack_frame)
320 {
321 self.select_ix(Some(ix), cx);
322 self.activate_selected_entry(window, cx);
323 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
324 let ix = self.entries.iter().position(|entry| match entry {
325 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
326 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
327 });
328 self.selected_ix = ix;
329 }
330
331 match self.list_filter {
332 StackFrameFilter::All => {
333 self.list_state.reset(self.entries.len());
334 }
335 StackFrameFilter::OnlyUserFrames => {
336 self.list_state.reset(self.filter_entries_indices.len());
337 }
338 }
339 cx.emit(StackFrameListEvent::BuiltEntries);
340 cx.notify();
341 }
342
343 pub fn go_to_stack_frame(
344 &mut self,
345 stack_frame_id: StackFrameId,
346 window: &mut Window,
347 cx: &mut Context<Self>,
348 ) -> Task<Result<()>> {
349 let Some(stack_frame) = self
350 .entries
351 .iter()
352 .flat_map(|entry| match entry {
353 StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
354 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
355 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
356 })
357 .find(|stack_frame| stack_frame.id == stack_frame_id)
358 .cloned()
359 else {
360 return Task::ready(Err(anyhow!("No stack frame for ID")));
361 };
362 self.go_to_stack_frame_inner(stack_frame, window, cx)
363 }
364
365 fn go_to_stack_frame_inner(
366 &mut self,
367 stack_frame: dap::StackFrame,
368 window: &mut Window,
369 cx: &mut Context<Self>,
370 ) -> Task<Result<()>> {
371 let stack_frame_id = stack_frame.id;
372 self.opened_stack_frame_id = Some(stack_frame_id);
373 let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
374 return Task::ready(Err(anyhow!("Project path not found")));
375 };
376 let row = stack_frame.line.saturating_sub(1) as u32;
377 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
378 stack_frame_id,
379 ));
380 cx.spawn_in(window, async move |this, cx| {
381 let (worktree, relative_path) = this
382 .update(cx, |this, cx| {
383 this.workspace.update(cx, |workspace, cx| {
384 workspace.project().update(cx, |this, cx| {
385 this.find_or_create_worktree(&abs_path, false, cx)
386 })
387 })
388 })??
389 .await?;
390 let buffer = this
391 .update(cx, |this, cx| {
392 this.workspace.update(cx, |this, cx| {
393 this.project().update(cx, |this, cx| {
394 let worktree_id = worktree.read(cx).id();
395 this.open_buffer(
396 ProjectPath {
397 worktree_id,
398 path: relative_path.into(),
399 },
400 cx,
401 )
402 })
403 })
404 })??
405 .await?;
406 let position = buffer.read_with(cx, |this, _| {
407 this.snapshot().anchor_after(PointUtf16::new(row, 0))
408 })?;
409 this.update_in(cx, |this, window, cx| {
410 this.workspace.update(cx, |workspace, cx| {
411 let project_path = buffer
412 .read(cx)
413 .project_path(cx)
414 .context("Could not select a stack frame for unnamed buffer")?;
415
416 let open_preview = !workspace
417 .item_of_type::<StackTraceView>(cx)
418 .map(|viewer| {
419 workspace
420 .active_item(cx)
421 .is_some_and(|item| item.item_id() == viewer.item_id())
422 })
423 .unwrap_or_default();
424
425 anyhow::Ok(workspace.open_path_preview(
426 project_path,
427 None,
428 true,
429 true,
430 open_preview,
431 window,
432 cx,
433 ))
434 })
435 })???
436 .await?;
437
438 this.update(cx, |this, cx| {
439 let thread_id = this.state.read_with(cx, |state, _| {
440 state.thread_id.context("No selected thread ID found")
441 })??;
442
443 this.workspace.update(cx, |workspace, cx| {
444 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
445
446 breakpoint_store.update(cx, |store, cx| {
447 store.set_active_position(
448 ActiveStackFrame {
449 session_id: this.session.read(cx).session_id(),
450 thread_id,
451 stack_frame_id,
452 path: abs_path,
453 position,
454 },
455 cx,
456 );
457 })
458 })
459 })?
460 })
461 }
462
463 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
464 stack_frame.source.as_ref().and_then(|s| {
465 s.path
466 .as_deref()
467 .map(|path| Arc::<Path>::from(Path::new(path)))
468 .filter(|path| path.is_absolute())
469 })
470 }
471
472 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
473 self.session.update(cx, |state, cx| {
474 state.restart_stack_frame(stack_frame_id, cx)
475 });
476 }
477
478 fn render_label_entry(
479 &self,
480 stack_frame: &dap::StackFrame,
481 _cx: &mut Context<Self>,
482 ) -> AnyElement {
483 h_flex()
484 .rounded_md()
485 .justify_between()
486 .w_full()
487 .group("")
488 .id(("label-stack-frame", stack_frame.id))
489 .p_1()
490 .on_any_mouse_down(|_, _, cx| {
491 cx.stop_propagation();
492 })
493 .child(
494 v_flex().justify_center().gap_0p5().child(
495 Label::new(stack_frame.name.clone())
496 .size(LabelSize::Small)
497 .weight(FontWeight::BOLD)
498 .truncate()
499 .color(Color::Info),
500 ),
501 )
502 .into_any()
503 }
504
505 fn render_normal_entry(
506 &self,
507 ix: usize,
508 stack_frame: &dap::StackFrame,
509 cx: &mut Context<Self>,
510 ) -> AnyElement {
511 let source = stack_frame.source.clone();
512 let is_selected_frame = Some(ix) == self.selected_ix;
513
514 let path = source.and_then(|s| s.path.or(s.name));
515 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
516 let formatted_path = formatted_path.map(|path| {
517 Label::new(path)
518 .size(LabelSize::XSmall)
519 .line_height_style(LineHeightStyle::UiLabel)
520 .truncate()
521 .color(Color::Muted)
522 });
523
524 let supports_frame_restart = self
525 .session
526 .read(cx)
527 .capabilities()
528 .supports_restart_frame
529 .unwrap_or_default();
530
531 let should_deemphasize = matches!(
532 stack_frame.presentation_hint,
533 Some(
534 dap::StackFramePresentationHint::Subtle
535 | dap::StackFramePresentationHint::Deemphasize
536 )
537 );
538 h_flex()
539 .rounded_md()
540 .justify_between()
541 .w_full()
542 .group("")
543 .id(("stack-frame", stack_frame.id))
544 .p_1()
545 .when(is_selected_frame, |this| {
546 this.bg(cx.theme().colors().element_hover)
547 })
548 .on_any_mouse_down(|_, _, cx| {
549 cx.stop_propagation();
550 })
551 .on_click(cx.listener(move |this, _, window, cx| {
552 this.selected_ix = Some(ix);
553 this.activate_selected_entry(window, cx);
554 }))
555 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
556 .child(
557 v_flex()
558 .gap_0p5()
559 .child(
560 Label::new(stack_frame.name.clone())
561 .size(LabelSize::Small)
562 .truncate()
563 .when(should_deemphasize, |this| this.color(Color::Muted)),
564 )
565 .children(formatted_path),
566 )
567 .when(
568 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
569 |this| {
570 this.child(
571 h_flex()
572 .id(("restart-stack-frame", stack_frame.id))
573 .visible_on_hover("")
574 .absolute()
575 .right_2()
576 .overflow_hidden()
577 .rounded_md()
578 .border_1()
579 .border_color(cx.theme().colors().element_selected)
580 .bg(cx.theme().colors().element_background)
581 .hover(|style| {
582 style
583 .bg(cx.theme().colors().ghost_element_hover)
584 .cursor_pointer()
585 })
586 .child(
587 IconButton::new(
588 ("restart-stack-frame", stack_frame.id),
589 IconName::RotateCcw,
590 )
591 .icon_size(IconSize::Small)
592 .on_click(cx.listener({
593 let stack_frame_id = stack_frame.id;
594 move |this, _, _window, cx| {
595 this.restart_stack_frame(stack_frame_id, cx);
596 }
597 }))
598 .tooltip(move |window, cx| {
599 Tooltip::text("Restart Stack Frame")(window, cx)
600 }),
601 ),
602 )
603 },
604 )
605 .into_any()
606 }
607
608 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
609 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
610 return;
611 };
612 let entries = std::mem::take(stack_frames)
613 .into_iter()
614 .map(StackFrameEntry::Normal);
615 self.entries.splice(ix..ix + 1, entries);
616 self.selected_ix = Some(ix);
617 self.list_state.reset(self.entries.len());
618 cx.emit(StackFrameListEvent::BuiltEntries);
619 cx.notify();
620 }
621
622 fn render_collapsed_entry(
623 &self,
624 ix: usize,
625 stack_frames: &Vec<dap::StackFrame>,
626 cx: &mut Context<Self>,
627 ) -> AnyElement {
628 let first_stack_frame = &stack_frames[0];
629 let is_selected = Some(ix) == self.selected_ix;
630
631 h_flex()
632 .rounded_md()
633 .justify_between()
634 .w_full()
635 .group("")
636 .id(("stack-frame", first_stack_frame.id))
637 .p_1()
638 .when(is_selected, |this| {
639 this.bg(cx.theme().colors().element_hover)
640 })
641 .on_any_mouse_down(|_, _, cx| {
642 cx.stop_propagation();
643 })
644 .on_click(cx.listener(move |this, _, window, cx| {
645 this.selected_ix = Some(ix);
646 this.activate_selected_entry(window, cx);
647 }))
648 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
649 .child(
650 v_flex()
651 .text_ui_sm(cx)
652 .truncate()
653 .text_color(cx.theme().colors().text_muted)
654 .child(format!(
655 "Show {} more{}",
656 stack_frames.len(),
657 first_stack_frame
658 .source
659 .as_ref()
660 .and_then(|source| source.origin.as_ref())
661 .map_or(String::new(), |origin| format!(": {}", origin))
662 )),
663 )
664 .into_any()
665 }
666
667 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
668 let ix = match self.list_filter {
669 StackFrameFilter::All => ix,
670 StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
671 };
672
673 match &self.entries[ix] {
674 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
675 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
676 StackFrameEntry::Collapsed(stack_frames) => {
677 self.render_collapsed_entry(ix, stack_frames, cx)
678 }
679 }
680 }
681
682 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
683 div()
684 .occlude()
685 .id("stack-frame-list-vertical-scrollbar")
686 .on_mouse_move(cx.listener(|_, _, _, cx| {
687 cx.notify();
688 cx.stop_propagation()
689 }))
690 .on_hover(|_, _, cx| {
691 cx.stop_propagation();
692 })
693 .on_any_mouse_down(|_, _, cx| {
694 cx.stop_propagation();
695 })
696 .on_mouse_up(
697 MouseButton::Left,
698 cx.listener(|_, _, _, cx| {
699 cx.stop_propagation();
700 }),
701 )
702 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
703 cx.notify();
704 }))
705 .h_full()
706 .absolute()
707 .right_1()
708 .top_1()
709 .bottom_0()
710 .w(px(12.))
711 .cursor_default()
712 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
713 }
714
715 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
716 self.selected_ix = ix;
717 cx.notify();
718 }
719
720 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
721 let ix = match self.selected_ix {
722 _ if self.entries.is_empty() => None,
723 None => Some(0),
724 Some(ix) => {
725 if ix == self.entries.len() - 1 {
726 Some(0)
727 } else {
728 Some(ix + 1)
729 }
730 }
731 };
732 self.select_ix(ix, cx);
733 }
734
735 fn select_previous(
736 &mut self,
737 _: &menu::SelectPrevious,
738 _window: &mut Window,
739 cx: &mut Context<Self>,
740 ) {
741 let ix = match self.selected_ix {
742 _ if self.entries.is_empty() => None,
743 None => Some(self.entries.len() - 1),
744 Some(ix) => {
745 if ix == 0 {
746 Some(self.entries.len() - 1)
747 } else {
748 Some(ix - 1)
749 }
750 }
751 };
752 self.select_ix(ix, cx);
753 }
754
755 fn select_first(
756 &mut self,
757 _: &menu::SelectFirst,
758 _window: &mut Window,
759 cx: &mut Context<Self>,
760 ) {
761 let ix = if !self.entries.is_empty() {
762 Some(0)
763 } else {
764 None
765 };
766 self.select_ix(ix, cx);
767 }
768
769 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
770 let ix = if !self.entries.is_empty() {
771 Some(self.entries.len() - 1)
772 } else {
773 None
774 };
775 self.select_ix(ix, cx);
776 }
777
778 fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
779 let Some(ix) = self.selected_ix else {
780 return;
781 };
782 let Some(entry) = self.entries.get_mut(ix) else {
783 return;
784 };
785 match entry {
786 StackFrameEntry::Normal(stack_frame) => {
787 let stack_frame = stack_frame.clone();
788 self.go_to_stack_frame_inner(stack_frame, window, cx)
789 .detach_and_log_err(cx)
790 }
791 StackFrameEntry::Label(_) => {
792 debug_panic!("You should not be able to select a label stack frame")
793 }
794 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
795 }
796 cx.notify();
797 }
798
799 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
800 self.activate_selected_entry(window, cx);
801 }
802
803 pub(crate) fn toggle_frame_filter(
804 &mut self,
805 thread_status: Option<ThreadStatus>,
806 cx: &mut Context<Self>,
807 ) {
808 self.list_filter = match self.list_filter {
809 StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
810 StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
811 };
812
813 if let Some(database_id) = self
814 .workspace
815 .read_with(cx, |workspace, _| workspace.database_id())
816 .ok()
817 .flatten()
818 {
819 let database_id: i64 = database_id.into();
820 let save_task = KEY_VALUE_STORE.write_kvp(
821 format!(
822 "stack-frame-list-filter-{}-{}",
823 self.session.read(cx).adapter().0,
824 database_id,
825 ),
826 self.list_filter.into(),
827 );
828 cx.background_spawn(save_task).detach();
829 }
830
831 if let Some(ThreadStatus::Stopped) = thread_status {
832 match self.list_filter {
833 StackFrameFilter::All => {
834 self.list_state.reset(self.entries.len());
835 }
836 StackFrameFilter::OnlyUserFrames => {
837 self.list_state.reset(self.filter_entries_indices.len());
838 if !self
839 .selected_ix
840 .map(|ix| self.filter_entries_indices.contains(&ix))
841 .unwrap_or_default()
842 {
843 self.selected_ix = None;
844 }
845 }
846 }
847
848 if let Some(ix) = self.selected_ix {
849 let scroll_to = match self.list_filter {
850 StackFrameFilter::All => ix,
851 StackFrameFilter::OnlyUserFrames => self
852 .filter_entries_indices
853 .binary_search_by_key(&ix, |ix| *ix)
854 .expect("This index will always exist"),
855 };
856 self.list_state.scroll_to_reveal_item(scroll_to);
857 }
858
859 cx.emit(StackFrameListEvent::BuiltEntries);
860 cx.notify();
861 }
862 }
863
864 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
865 div().p_1().size_full().child(
866 list(
867 self.list_state.clone(),
868 cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
869 )
870 .size_full(),
871 )
872 }
873
874 pub(crate) fn render_control_strip(&self) -> AnyElement {
875 let tooltip_title = match self.list_filter {
876 StackFrameFilter::All => "Show stack frames from your project",
877 StackFrameFilter::OnlyUserFrames => "Show all stack frames",
878 };
879
880 h_flex()
881 .child(
882 IconButton::new(
883 "filter-by-visible-worktree-stack-frame-list",
884 IconName::ListFilter,
885 )
886 .tooltip(move |window, cx| {
887 Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
888 })
889 .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
890 .icon_size(IconSize::Small)
891 .on_click(|_, window, cx| {
892 window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
893 }),
894 )
895 .into_any_element()
896 }
897}
898
899impl Render for StackFrameList {
900 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
901 div()
902 .track_focus(&self.focus_handle)
903 .size_full()
904 .on_action(cx.listener(Self::select_next))
905 .on_action(cx.listener(Self::select_previous))
906 .on_action(cx.listener(Self::select_first))
907 .on_action(cx.listener(Self::select_last))
908 .on_action(cx.listener(Self::confirm))
909 .when_some(self.error.clone(), |el, error| {
910 el.child(
911 h_flex()
912 .bg(cx.theme().status().warning_background)
913 .border_b_1()
914 .border_color(cx.theme().status().warning_border)
915 .pl_1()
916 .child(Icon::new(IconName::Warning).color(Color::Warning))
917 .gap_2()
918 .child(
919 Label::new(error)
920 .size(LabelSize::Small)
921 .color(Color::Warning),
922 ),
923 )
924 })
925 .child(self.render_list(window, cx))
926 .child(self.render_vertical_scrollbar(cx))
927 }
928}
929
930impl Focusable for StackFrameList {
931 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
932 self.focus_handle.clone()
933 }
934}
935
936impl EventEmitter<StackFrameListEvent> for StackFrameList {}