stack_frame_list.rs

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