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