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