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