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