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