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