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