stack_frame_list.rs

  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 {}