debugger: Add stack frame multibuffer (#30395)

Anthony Eid and Remco Smits created

This PR adds the ability to expand a debugger stack trace into a multi
buffer and view each frame as it's own excerpt.

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

crates/debugger_ui/src/debugger_panel.rs                   |  19 
crates/debugger_ui/src/debugger_ui.rs                      |  37 
crates/debugger_ui/src/session.rs                          |  36 
crates/debugger_ui/src/session/running.rs                  |   3 
crates/debugger_ui/src/session/running/console.rs          |   4 
crates/debugger_ui/src/session/running/stack_frame_list.rs |  52 
crates/debugger_ui/src/session/running/variable_list.rs    |   1 
crates/debugger_ui/src/stack_trace_view.rs                 | 453 ++++++++
crates/debugger_ui/src/tests/variable_list.rs              |  10 
crates/editor/src/editor.rs                                |  26 
10 files changed, 604 insertions(+), 37 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -2,8 +2,9 @@ use crate::persistence::DebuggerPaneItem;
 use crate::session::DebugSession;
 use crate::{
     ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
-    FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
-    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
+    FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
+    ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
+    persistence,
 };
 use anyhow::{Result, anyhow};
 use command_palette_hooks::CommandPaletteFilter;
@@ -67,11 +68,7 @@ pub struct DebugPanel {
 }
 
 impl DebugPanel {
-    pub fn new(
-        workspace: &Workspace,
-        _window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) -> Entity<Self> {
+    pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
         cx.new(|cx| {
             let project = workspace.project().clone();
 
@@ -119,6 +116,7 @@ impl DebugPanel {
             TypeId::of::<StepOver>(),
             TypeId::of::<StepInto>(),
             TypeId::of::<StepOut>(),
+            TypeId::of::<ShowStackTrace>(),
             TypeId::of::<editor::actions::DebuggerRunToCursor>(),
             TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
         ];
@@ -170,8 +168,8 @@ impl DebugPanel {
         cx: &mut AsyncWindowContext,
     ) -> Task<Result<Entity<Self>>> {
         cx.spawn(async move |cx| {
-            workspace.update_in(cx, |workspace, window, cx| {
-                let debug_panel = DebugPanel::new(workspace, window, cx);
+            workspace.update(cx, |workspace, cx| {
+                let debug_panel = DebugPanel::new(workspace, cx);
 
                 workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
                     workspace.project().read(cx).breakpoint_store().update(
@@ -421,6 +419,7 @@ impl DebugPanel {
     pub fn active_session(&self) -> Option<Entity<DebugSession>> {
         self.active_session.clone()
     }
+
     fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
         let Some(session) = self
             .sessions
@@ -999,7 +998,7 @@ impl DebugPanel {
                 this.go_to_selected_stack_frame(window, cx);
             });
         });
-        self.active_session = Some(session_item);
+        self.active_session = Some(session_item.clone());
         cx.notify();
     }
 

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -7,14 +7,16 @@ use new_session_modal::NewSessionModal;
 use project::debugger::{self, breakpoint_store::SourceBreakpoint};
 use session::DebugSession;
 use settings::Settings;
+use stack_trace_view::StackTraceView;
 use util::maybe;
-use workspace::{ShutdownDebugAdapters, Workspace};
+use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
 
 pub mod attach_modal;
 pub mod debugger_panel;
 mod new_session_modal;
 mod persistence;
 pub(crate) mod session;
+mod stack_trace_view;
 
 #[cfg(any(test, feature = "test-support"))]
 pub mod tests;
@@ -41,6 +43,7 @@ actions!(
         FocusModules,
         FocusLoadedSources,
         FocusTerminal,
+        ShowStackTrace,
     ]
 );
 
@@ -146,6 +149,38 @@ pub fn init(cx: &mut App) {
                         })
                     },
                 )
+                .register_action(
+                    |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
+                        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+                            return;
+                        };
+
+                        if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
+                            let is_active = workspace
+                                .active_item(cx)
+                                .is_some_and(|item| item.item_id() == existing.item_id());
+                            workspace.activate_item(&existing, true, !is_active, window, cx);
+                        } else {
+                            let Some(active_session) = debug_panel.read(cx).active_session() else {
+                                return;
+                            };
+
+                            let project = workspace.project();
+
+                            let stack_trace_view = active_session.update(cx, |session, cx| {
+                                session.stack_trace_view(project, window, cx).clone()
+                            });
+
+                            workspace.add_item_to_active_pane(
+                                Box::new(stack_trace_view),
+                                None,
+                                true,
+                                window,
+                                cx,
+                            );
+                        }
+                    },
+                )
                 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
                     NewSessionModal::show(workspace, window, cx);
                 });

crates/debugger_ui/src/session.rs 🔗

@@ -1,6 +1,6 @@
 pub mod running;
 
-use std::sync::OnceLock;
+use std::{cell::OnceCell, sync::OnceLock};
 
 use dap::client::SessionId;
 use gpui::{
@@ -17,15 +17,16 @@ use workspace::{
     item::{self, Item},
 };
 
-use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
+use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
 
 pub struct DebugSession {
     remote_id: Option<workspace::ViewId>,
     running_state: Entity<RunningState>,
     label: OnceLock<SharedString>,
+    stack_trace_view: OnceCell<Entity<StackTraceView>>,
     _debug_panel: WeakEntity<DebugPanel>,
     _worktree_store: WeakEntity<WorktreeStore>,
-    _workspace: WeakEntity<Workspace>,
+    workspace: WeakEntity<Workspace>,
     _subscriptions: [Subscription; 1],
 }
 
@@ -66,8 +67,9 @@ impl DebugSession {
             running_state,
             label: OnceLock::new(),
             _debug_panel,
+            stack_trace_view: OnceCell::new(),
             _worktree_store: project.read(cx).worktree_store().downgrade(),
-            _workspace: workspace,
+            workspace,
         })
     }
 
@@ -75,6 +77,32 @@ impl DebugSession {
         self.running_state.read(cx).session_id()
     }
 
+    pub(crate) fn stack_trace_view(
+        &mut self,
+        project: &Entity<Project>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> &Entity<StackTraceView> {
+        let workspace = self.workspace.clone();
+        let running_state = self.running_state.clone();
+
+        self.stack_trace_view.get_or_init(|| {
+            let stackframe_list = running_state.read(cx).stack_frame_list().clone();
+
+            let stack_frame_view = cx.new(|cx| {
+                StackTraceView::new(
+                    workspace.clone(),
+                    project.clone(),
+                    stackframe_list,
+                    window,
+                    cx,
+                )
+            });
+
+            stack_frame_view
+        })
+    }
+
     pub fn session(&self, cx: &App) -> Entity<Session> {
         self.running_state.read(cx).session().clone()
     }

crates/debugger_ui/src/session/running.rs 🔗

@@ -1235,8 +1235,7 @@ impl RunningState {
         self.stack_frame_list.read(cx).selected_stack_frame_id()
     }
 
-    #[cfg(test)]
-    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
+    pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
         &self.stack_frame_list
     }
 

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -62,7 +62,6 @@ impl Console {
             editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
             editor
         });
-        let focus_handle = cx.focus_handle();
 
         let this = cx.weak_entity();
         let query_bar = cx.new(|cx| {
@@ -77,6 +76,8 @@ impl Console {
             editor
         });
 
+        let focus_handle = query_bar.focus_handle(cx);
+
         let _subscriptions =
             vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
 
@@ -110,6 +111,7 @@ impl Console {
     ) {
         match event {
             StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
+            StackFrameListEvent::BuiltEntries => {}
         }
     }
 

crates/debugger_ui/src/session/running/stack_frame_list.rs 🔗

@@ -15,13 +15,16 @@ use project::debugger::session::{Session, SessionEvent, StackFrame};
 use project::{ProjectItem, ProjectPath};
 use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ItemHandle, Workspace};
+
+use crate::StackTraceView;
 
 use super::RunningState;
 
 #[derive(Debug)]
 pub enum StackFrameListEvent {
     SelectedStackFrameChanged(StackFrameId),
+    BuiltEntries,
 }
 
 pub struct StackFrameList {
@@ -101,13 +104,18 @@ impl StackFrameList {
         &self.entries
     }
 
-    #[cfg(test)]
-    pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
+    pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {
         self.entries
             .iter()
             .flat_map(|frame| match frame {
                 StackFrameEntry::Normal(frame) => vec![frame.clone()],
-                StackFrameEntry::Collapsed(frames) => frames.clone(),
+                StackFrameEntry::Collapsed(frames) => {
+                    if show_collapsed {
+                        frames.clone()
+                    } else {
+                        vec![]
+                    }
+                }
             })
             .collect::<Vec<_>>()
     }
@@ -136,6 +144,25 @@ impl StackFrameList {
         self.selected_stack_frame_id
     }
 
+    pub(crate) fn select_stack_frame_id(
+        &mut self,
+        id: StackFrameId,
+        window: &Window,
+        cx: &mut Context<Self>,
+    ) {
+        if !self.entries.iter().any(|entry| match entry {
+            StackFrameEntry::Normal(entry) => entry.id == id,
+            StackFrameEntry::Collapsed(stack_frames) => {
+                stack_frames.iter().any(|frame| frame.id == id)
+            }
+        }) {
+            return;
+        }
+
+        self.selected_stack_frame_id = Some(id);
+        self.go_to_selected_stack_frame(window, cx);
+    }
+
     pub(super) fn schedule_refresh(
         &mut self,
         select_first: bool,
@@ -206,6 +233,7 @@ impl StackFrameList {
                 .detach_and_log_err(cx);
         }
 
+        cx.emit(StackFrameListEvent::BuiltEntries);
         cx.notify();
     }
 
@@ -255,7 +283,7 @@ impl StackFrameList {
 
         let row = (stack_frame.line.saturating_sub(1)) as u32;
 
-        let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
+        let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
             return Task::ready(Err(anyhow!("Project path not found")));
         };
 
@@ -294,12 +322,22 @@ impl StackFrameList {
                     let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
                         anyhow!("Could not select a stack frame for unnamed buffer")
                     })?;
+
+                    let open_preview = !workspace
+                        .item_of_type::<StackTraceView>(cx)
+                        .map(|viewer| {
+                            workspace
+                                .active_item(cx)
+                                .is_some_and(|item| item.item_id() == viewer.item_id())
+                        })
+                        .unwrap_or_default();
+
                     anyhow::Ok(workspace.open_path_preview(
                         project_path,
                         None,
-                        false,
                         true,
                         true,
+                        open_preview,
                         window,
                         cx,
                     ))
@@ -332,7 +370,7 @@ impl StackFrameList {
         })
     }
 
-    fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
+    pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
         stack_frame.source.as_ref().and_then(|s| {
             s.path
                 .as_deref()

crates/debugger_ui/src/stack_trace_view.rs 🔗

@@ -0,0 +1,453 @@
+use std::any::{Any, TypeId};
+
+use collections::HashMap;
+use dap::StackFrameId;
+use editor::{
+    Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
+    RowHighlightOptions, ToPoint, scroll::Autoscroll,
+};
+use gpui::{
+    AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
+    Subscription, Task, WeakEntity, Window,
+};
+use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
+use project::{Project, ProjectPath};
+use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div};
+use util::ResultExt as _;
+use workspace::{
+    Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
+    item::{BreadcrumbText, ItemEvent},
+    searchable::SearchableItemHandle,
+};
+
+use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent};
+use anyhow::Result;
+
+pub(crate) struct StackTraceView {
+    editor: Entity<Editor>,
+    multibuffer: Entity<MultiBuffer>,
+    workspace: WeakEntity<Workspace>,
+    project: Entity<Project>,
+    stack_frame_list: Entity<StackFrameList>,
+    selected_stack_frame_id: Option<StackFrameId>,
+    highlights: Vec<(StackFrameId, Anchor)>,
+    excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
+    refresh_task: Option<Task<Result<()>>>,
+    _subscription: Option<Subscription>,
+}
+
+impl StackTraceView {
+    pub(crate) fn new(
+        workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
+        stack_frame_list: Entity<StackFrameList>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
+        let editor = cx.new(|cx| {
+            let mut editor =
+                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
+            editor.set_vertical_scroll_margin(5, cx);
+            editor
+        });
+
+        cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
+            if let EditorEvent::SelectionsChanged { local: true } = event {
+                let excerpt_id = editor.update(cx, |editor, cx| {
+                    let position: Point = editor.selections.newest(cx).head();
+
+                    editor
+                        .snapshot(window, cx)
+                        .buffer_snapshot
+                        .excerpt_containing(position..position)
+                        .map(|excerpt| excerpt.id())
+                });
+
+                if let Some(stack_frame_id) = excerpt_id
+                    .and_then(|id| this.excerpt_for_frames.get(&id))
+                    .filter(|id| Some(**id) != this.selected_stack_frame_id)
+                {
+                    this.stack_frame_list.update(cx, |list, cx| {
+                        list.select_stack_frame_id(*stack_frame_id, window, cx);
+                    });
+                }
+            }
+        })
+        .detach();
+
+        cx.subscribe_in(
+            &stack_frame_list,
+            window,
+            |this, stack_frame_list, event, window, cx| match event {
+                StackFrameListEvent::BuiltEntries => {
+                    this.selected_stack_frame_id =
+                        stack_frame_list.read(cx).selected_stack_frame_id();
+                    this.update_excerpts(window, cx);
+                }
+                StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
+                    this.selected_stack_frame_id = Some(*selected_frame_id);
+                    this.update_highlights(window, cx);
+
+                    if let Some(frame_anchor) = this
+                        .highlights
+                        .iter()
+                        .find(|(frame_id, _)| frame_id == selected_frame_id)
+                        .map(|highlight| highlight.1)
+                    {
+                        this.editor.update(cx, |editor, cx| {
+                            if frame_anchor.excerpt_id
+                                != editor.selections.newest_anchor().head().excerpt_id
+                            {
+                                let auto_scroll =
+                                    Some(Autoscroll::center().for_anchor(frame_anchor));
+
+                                editor.change_selections(auto_scroll, window, cx, |selections| {
+                                    let selection_id = selections.new_selection_id();
+
+                                    let selection = Selection {
+                                        id: selection_id,
+                                        start: frame_anchor,
+                                        end: frame_anchor,
+                                        goal: SelectionGoal::None,
+                                        reversed: false,
+                                    };
+
+                                    selections.select_anchors(vec![selection]);
+                                })
+                            }
+                        });
+                    }
+                }
+            },
+        )
+        .detach();
+
+        let mut this = Self {
+            editor,
+            multibuffer,
+            workspace,
+            project,
+            excerpt_for_frames: HashMap::default(),
+            highlights: Vec::default(),
+            stack_frame_list,
+            selected_stack_frame_id: None,
+            refresh_task: None,
+            _subscription: None,
+        };
+
+        this.update_excerpts(window, cx);
+        this
+    }
+
+    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.refresh_task.take();
+        self.editor.update(cx, |editor, cx| {
+            editor.clear_highlights::<DebugStackFrameLine>(cx)
+        });
+
+        let stack_frames = self
+            .stack_frame_list
+            .update(cx, |list, _| list.flatten_entries(false));
+
+        let frames_to_open: Vec<_> = stack_frames
+            .into_iter()
+            .filter_map(|frame| {
+                Some((
+                    frame.id,
+                    frame.line as u32 - 1,
+                    StackFrameList::abs_path_from_stack_frame(&frame)?,
+                ))
+            })
+            .collect();
+
+        self.multibuffer
+            .update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
+
+        let task = cx.spawn_in(window, async move |this, cx| {
+            let mut to_highlights = Vec::default();
+
+            for (stack_frame_id, line, abs_path) in frames_to_open {
+                let (worktree, relative_path) = this
+                    .update(cx, |this, cx| {
+                        this.workspace.update(cx, |workspace, cx| {
+                            workspace.project().update(cx, |this, cx| {
+                                this.find_or_create_worktree(&abs_path, false, cx)
+                            })
+                        })
+                    })??
+                    .await?;
+
+                let project_path = ProjectPath {
+                    worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
+                    path: relative_path.into(),
+                };
+
+                if let Some(buffer) = this
+                    .read_with(cx, |this, _| this.project.clone())?
+                    .update(cx, |project, cx| project.open_buffer(project_path, cx))?
+                    .await
+                    .log_err()
+                {
+                    this.update(cx, |this, cx| {
+                        this.multibuffer.update(cx, |multi_buffer, cx| {
+                            let line_point = Point::new(line, 0);
+                            let start_context = Self::heuristic_syntactic_expand(
+                                &buffer.read(cx).snapshot(),
+                                line_point,
+                            );
+
+                            // Users will want to see what happened before an active debug line in most cases
+                            let range = ExcerptRange {
+                                context: start_context..Point::new(line.saturating_add(1), 0),
+                                primary: line_point..line_point,
+                            };
+                            multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
+
+                            let line_anchor =
+                                multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
+
+                            if let Some(line_anchor) = line_anchor {
+                                this.excerpt_for_frames
+                                    .insert(line_anchor.excerpt_id, stack_frame_id);
+                                to_highlights.push((stack_frame_id, line_anchor));
+                            }
+                        });
+                    })
+                    .ok();
+                }
+            }
+
+            this.update_in(cx, |this, window, cx| {
+                this.highlights = to_highlights;
+                this.update_highlights(window, cx);
+            })
+            .ok();
+
+            anyhow::Ok(())
+        });
+
+        self.refresh_task = Some(task);
+    }
+
+    fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, _| {
+            editor.clear_row_highlights::<DebugStackFrameLine>()
+        });
+
+        let stack_frames = self
+            .stack_frame_list
+            .update(cx, |session, _| session.flatten_entries(false));
+
+        let active_idx = self
+            .selected_stack_frame_id
+            .and_then(|id| {
+                stack_frames
+                    .iter()
+                    .enumerate()
+                    .find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
+            })
+            .unwrap_or(0);
+
+        self.editor.update(cx, |editor, cx| {
+            let snapshot = editor.snapshot(window, cx).display_snapshot;
+            let first_color = cx.theme().colors().editor_debugger_active_line_background;
+
+            let color = first_color.opacity(0.5);
+
+            let mut is_first = true;
+
+            for (_, highlight) in self.highlights.iter().skip(active_idx) {
+                let position = highlight.to_point(&snapshot.buffer_snapshot);
+                let color = if is_first {
+                    is_first = false;
+                    first_color
+                } else {
+                    color
+                };
+
+                let start = snapshot
+                    .buffer_snapshot
+                    .clip_point(Point::new(position.row, 0), Bias::Left);
+                let end = start + Point::new(1, 0);
+                let start = snapshot.buffer_snapshot.anchor_before(start);
+                let end = snapshot.buffer_snapshot.anchor_before(end);
+                editor.highlight_rows::<DebugStackFrameLine>(
+                    start..end,
+                    color,
+                    RowHighlightOptions::default(),
+                    cx,
+                );
+            }
+        })
+    }
+
+    fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
+        let mut text_objects = snapshot.text_object_ranges(
+            selected_point..selected_point,
+            TreeSitterOptions::max_start_depth(4),
+        );
+
+        let mut start_position = text_objects
+            .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
+            .map(|(range, _)| snapshot.offset_to_point(range.start))
+            .map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
+            .unwrap_or(selected_point);
+
+        if start_position.row == selected_point.row {
+            start_position.row = start_position.row.saturating_sub(1);
+        }
+
+        start_position
+    }
+}
+
+impl Render for StackTraceView {
+    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+        div().size_full().child(self.editor.clone())
+    }
+}
+
+impl EventEmitter<EditorEvent> for StackTraceView {}
+impl Focusable for StackTraceView {
+    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
+impl Item for StackTraceView {
+    type Event = EditorEvent;
+
+    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+        Editor::to_item_events(event, f)
+    }
+
+    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor
+            .update(cx, |editor, cx| editor.deactivated(window, cx));
+    }
+
+    fn navigate(
+        &mut self,
+        data: Box<dyn Any>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> bool {
+        self.editor
+            .update(cx, |editor, cx| editor.navigate(data, window, cx))
+    }
+
+    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
+        Some("Stack Frame Viewer".into())
+    }
+
+    fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
+        "Stack Frames".into()
+    }
+
+    fn for_each_project_item(
+        &self,
+        cx: &App,
+        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
+    ) {
+        self.editor.for_each_project_item(cx, f)
+    }
+
+    fn is_singleton(&self, _: &App) -> bool {
+        false
+    }
+
+    fn set_nav_history(
+        &mut self,
+        nav_history: ItemNavHistory,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.editor.update(cx, |editor, _| {
+            editor.set_nav_history(Some(nav_history));
+        });
+    }
+
+    fn is_dirty(&self, cx: &App) -> bool {
+        self.multibuffer.read(cx).is_dirty(cx)
+    }
+
+    fn has_deleted_file(&self, cx: &App) -> bool {
+        self.multibuffer.read(cx).has_deleted_file(cx)
+    }
+
+    fn has_conflict(&self, cx: &App) -> bool {
+        self.multibuffer.read(cx).has_conflict(cx)
+    }
+
+    fn can_save(&self, _: &App) -> bool {
+        true
+    }
+
+    fn save(
+        &mut self,
+        format: bool,
+        project: Entity<Project>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        self.editor.save(format, project, window, cx)
+    }
+
+    fn save_as(
+        &mut self,
+        _: Entity<Project>,
+        _: ProjectPath,
+        _window: &mut Window,
+        _: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        unreachable!()
+    }
+
+    fn reload(
+        &mut self,
+        project: Entity<Project>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        self.editor.reload(project, window, cx)
+    }
+
+    fn act_as_type<'a>(
+        &'a self,
+        type_id: TypeId,
+        self_handle: &'a Entity<Self>,
+        _: &'a App,
+    ) -> Option<AnyView> {
+        if type_id == TypeId::of::<Self>() {
+            Some(self_handle.to_any())
+        } else if type_id == TypeId::of::<Editor>() {
+            Some(self.editor.to_any())
+        } else {
+            None
+        }
+    }
+
+    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(self.editor.clone()))
+    }
+
+    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
+        ToolbarItemLocation::PrimaryLeft
+    }
+
+    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
+        self.editor.breadcrumbs(theme, cx)
+    }
+
+    fn added_to_workspace(
+        &mut self,
+        workspace: &mut Workspace,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            editor.added_to_workspace(workspace, window, cx)
+        });
+    }
+}

crates/debugger_ui/src/tests/variable_list.rs 🔗

@@ -190,7 +190,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.selected_stack_frame_id())
+                (list.flatten_entries(true), list.selected_stack_frame_id())
             });
 
         assert_eq!(stack_frames, stack_frame_list);
@@ -431,7 +431,7 @@ async fn test_fetch_variables_for_multiple_scopes(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.selected_stack_frame_id())
+                (list.flatten_entries(true), list.selected_stack_frame_id())
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1452,7 +1452,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.selected_stack_frame_id())
+                (list.flatten_entries(true), list.selected_stack_frame_id())
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1734,7 +1734,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.selected_stack_frame_id())
+                (list.flatten_entries(true), list.selected_stack_frame_id())
             });
 
         let variable_list = running_state.variable_list().read(cx);
@@ -1789,7 +1789,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.selected_stack_frame_id())
+                (list.flatten_entries(true), list.selected_stack_frame_id())
             });
 
         let variable_list = running_state.variable_list().read(cx);

crates/editor/src/editor.rs 🔗

@@ -289,6 +289,7 @@ impl InlayId {
 }
 
 pub enum ActiveDebugLine {}
+pub enum DebugStackFrameLine {}
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
 enum InputComposition {}
@@ -13880,7 +13881,10 @@ impl Editor {
             Default::default(),
             cx,
         );
-        self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
+
+        if self.buffer.read(cx).is_singleton() {
+            self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
+        }
     }
 
     pub fn go_to_definition(
@@ -16886,6 +16890,7 @@ impl Editor {
 
                 handled = true;
                 self.clear_row_highlights::<ActiveDebugLine>();
+
                 self.go_to_line::<ActiveDebugLine>(
                     multibuffer_anchor,
                     Some(cx.theme().colors().editor_debugger_active_line_background),
@@ -17900,9 +17905,7 @@ impl Editor {
         let Some(project) = self.project.clone() else {
             return;
         };
-        let Some(buffer) = self.buffer.read(cx).as_singleton() else {
-            return;
-        };
+
         if !self.inline_value_cache.enabled {
             let inlays = std::mem::take(&mut self.inline_value_cache.inlays);
             self.splice_inlays(&inlays, Vec::new(), cx);
@@ -17920,15 +17923,24 @@ impl Editor {
                 .ok()?;
 
             let inline_values = editor
-                .update(cx, |_, cx| {
+                .update(cx, |editor, cx| {
                     let Some(current_execution_position) = current_execution_position else {
                         return Some(Task::ready(Ok(Vec::new())));
                     };
 
-                    // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text
-                    // anchor is in the same buffer
+                    let buffer = editor.buffer.read_with(cx, |buffer, cx| {
+                        let snapshot = buffer.snapshot(cx);
+
+                        let excerpt = snapshot.excerpt_containing(
+                            current_execution_position..current_execution_position,
+                        )?;
+
+                        editor.buffer.read(cx).buffer(excerpt.buffer_id())
+                    })?;
+
                     let range =
                         buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor;
+
                     project.inline_values(buffer, range, cx)
                 })
                 .ok()