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