debugger: Add memory view (#33955)

This is mostly setting up the UI for now; I expect it to be the biggest
chunk of work.

Release Notes:

- debugger: Added memory view

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-07-14 16:32:06 +02:00 committed by GitHub
parent a2f5c47e2d
commit 6673c7cd4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1732 additions and 71 deletions

View file

@ -40,12 +40,15 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
hex.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
notifications.workspace = true
parking_lot.workspace = true
parse_int.workspace = true
paths.workspace = true
picker.workspace = true
pretty_assertions.workspace = true

View file

@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::session::running::breakpoint_list::BreakpointList;
use crate::{
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
@ -1804,6 +1805,7 @@ impl Render for DebugPanel {
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
.child(Divider::vertical())
} else {
this.items_end()
.child(welcome_experience)

View file

@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
use crate::session::running::{
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
loaded_source_list::LoadedSourceList, module_list::ModuleList,
loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList,
stack_frame_list::StackFrameList, variable_list::VariableList,
};
@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem {
Modules,
LoadedSources,
Terminal,
MemoryView,
}
impl DebuggerPaneItem {
@ -36,6 +37,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules,
DebuggerPaneItem::LoadedSources,
DebuggerPaneItem::Terminal,
DebuggerPaneItem::MemoryView,
];
VARIANTS
}
@ -43,6 +45,9 @@ impl DebuggerPaneItem {
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
match self {
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
DebuggerPaneItem::MemoryView => capabilities
.supports_read_memory_request
.unwrap_or_default(),
DebuggerPaneItem::LoadedSources => capabilities
.supports_loaded_sources_request
.unwrap_or_default(),
@ -59,6 +64,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
}
}
pub(crate) fn tab_tooltip(self) -> SharedString {
@ -80,6 +86,7 @@ impl DebuggerPaneItem {
DebuggerPaneItem::Terminal => {
"Provides an interactive terminal session within the debugging environment."
}
DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
};
SharedString::new_static(tooltip)
}
@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list: &Entity<BreakpointList>,
loaded_sources: &Entity<LoadedSourceList>,
terminal: &Entity<DebugTerminal>,
memory_view: &Entity<MemoryView>,
subscriptions: &mut HashMap<EntityId, Subscription>,
window: &mut Window,
cx: &mut Context<RunningState>,
@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout(
breakpoint_list,
loaded_sources,
terminal,
memory_view,
subscriptions,
window,
cx,
@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout(
DebuggerPaneItem::Terminal,
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
memory_view.focus_handle(cx),
memory_view.clone().into(),
DebuggerPaneItem::MemoryView,
cx,
)),
})
.collect();

View file

@ -1,16 +1,17 @@
pub(crate) mod breakpoint_list;
pub(crate) mod console;
pub(crate) mod loaded_source_list;
pub(crate) mod memory_view;
pub(crate) mod module_list;
pub mod stack_frame_list;
pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
ToggleExpandItem,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
session::running::memory_view::MemoryView,
};
use super::DebugPanelItemEvent;
@ -81,6 +82,7 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
pub(crate) scenario: Option<DebugScenario>,
pub(crate) scenario_context: Option<DebugScenarioContext>,
memory_view: Entity<MemoryView>,
}
impl RunningState {
@ -676,14 +678,36 @@ impl RunningState {
let session_id = session.read(cx).session_id();
let weak_state = cx.weak_entity();
let stack_frame_list = cx.new(|cx| {
StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
StackFrameList::new(
workspace.clone(),
session.clone(),
weak_state.clone(),
window,
cx,
)
});
let debug_terminal =
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
let variable_list =
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
let memory_view = cx.new(|cx| {
MemoryView::new(
session.clone(),
workspace.clone(),
stack_frame_list.downgrade(),
window,
cx,
)
});
let variable_list = cx.new(|cx| {
VariableList::new(
session.clone(),
stack_frame_list.clone(),
memory_view.clone(),
weak_state.clone(),
window,
cx,
)
});
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
@ -795,6 +819,7 @@ impl RunningState {
&breakpoint_list,
&loaded_source_list,
&debug_terminal,
&memory_view,
&mut pane_close_subscriptions,
window,
cx,
@ -823,6 +848,7 @@ impl RunningState {
let active_pane = panes.first_pane();
Self {
memory_view,
session,
workspace,
focus_handle,
@ -1234,6 +1260,12 @@ impl RunningState {
item_kind,
cx,
)),
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
self.memory_view.focus_handle(cx),
self.memory_view.clone().into(),
item_kind,
cx,
)),
}
}
@ -1418,7 +1450,14 @@ impl RunningState {
&self.module_list
}
pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
pub(crate) fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.ensure_pane_item(item, window, cx);
let (variable_list_position, pane) = self
.panes
.panes()
@ -1430,9 +1469,10 @@ impl RunningState {
.map(|view| (view, pane))
})
.unwrap();
pane.update(cx, |this, cx| {
this.activate_item(variable_list_position, true, true, window, cx);
})
});
}
#[cfg(test)]

View file

@ -0,0 +1,902 @@
use std::{fmt::Write, ops::RangeInclusive, sync::LazyLock, time::Duration};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton,
MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task,
TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds,
deferred, point, size, uniform_list,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::debugger::{MemoryCell, session::Session};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use crate::session::running::stack_frame_list::StackFrameList;
actions!(debugger, [GoToSelectedAddress]);
pub(crate) struct MemoryView {
workspace: WeakEntity<Workspace>,
scroll_handle: UniformListScrollHandle,
scroll_state: ScrollbarState,
show_scrollbar: bool,
stack_frame_list: WeakEntity<StackFrameList>,
hide_scrollbar_task: Option<Task<()>>,
focus_handle: FocusHandle,
view_state: ViewState,
query_editor: Entity<Editor>,
session: Entity<Session>,
width_picker_handle: PopoverMenuHandle<ContextMenu>,
is_writing_memory: bool,
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
}
impl Focusable for MemoryView {
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
#[derive(Clone, Debug)]
struct Drag {
start_address: u64,
end_address: u64,
}
impl Drag {
fn contains(&self, address: u64) -> bool {
let range = self.memory_range();
range.contains(&address)
}
fn memory_range(&self) -> RangeInclusive<u64> {
if self.start_address < self.end_address {
self.start_address..=self.end_address
} else {
self.end_address..=self.start_address
}
}
}
#[derive(Clone, Debug)]
enum SelectedMemoryRange {
DragUnderway(Drag),
DragComplete(Drag),
}
impl SelectedMemoryRange {
fn contains(&self, address: u64) -> bool {
match self {
SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
}
}
fn is_dragging(&self) -> bool {
matches!(self, SelectedMemoryRange::DragUnderway(_))
}
fn drag(&self) -> &Drag {
match self {
SelectedMemoryRange::DragUnderway(drag) => drag,
SelectedMemoryRange::DragComplete(drag) => drag,
}
}
}
#[derive(Clone)]
struct ViewState {
/// Uppermost row index
base_row: u64,
/// How many cells per row do we have?
line_width: ViewWidth,
selection: Option<SelectedMemoryRange>,
}
impl ViewState {
fn new(base_row: u64, line_width: ViewWidth) -> Self {
Self {
base_row,
line_width,
selection: None,
}
}
fn row_count(&self) -> u64 {
// This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good
// middle ground for data size.
const PAGE_SIZE: u64 = 4096;
PAGE_SIZE / self.line_width.width as u64
}
fn schedule_scroll_down(&mut self) {
self.base_row = self.base_row.saturating_add(1)
}
fn schedule_scroll_up(&mut self) {
self.base_row = self.base_row.saturating_sub(1);
}
}
static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
impl MemoryView {
pub(crate) fn new(
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
stack_frame_list: WeakEntity<StackFrameList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let view_state = ViewState::new(0, WIDTHS[4].clone());
let scroll_handle = UniformListScrollHandle::default();
let query_editor = cx.new(|cx| Editor::single_line(window, cx));
let scroll_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
workspace,
scroll_state,
scroll_handle,
stack_frame_list,
show_scrollbar: false,
hide_scrollbar_task: None,
focus_handle: cx.focus_handle(),
view_state,
query_editor,
session,
width_picker_handle: Default::default(),
is_writing_memory: true,
open_context_menu: None,
};
this.change_query_bar_mode(false, window, cx);
this
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
if !(self.show_scrollbar || self.scroll_state.is_dragging()) {
return None;
}
Some(
div()
.occlude()
.id("memory-view-vertical-scrollbar")
.on_mouse_move(cx.listener(|this, evt, _, cx| {
this.handle_drag(evt);
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scroll_state.clone())),
)
}
fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
let weak = cx.weak_entity();
let session = self.session.clone();
let view_state = self.view_state.clone();
uniform_list(
"debugger-memory-view",
self.view_state.row_count() as usize,
move |range, _, cx| {
let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
let memory_start =
(view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
let memory_end = (view_state.base_row + range.end as u64)
* view_state.line_width.width as u64
- 1;
let mut memory = session.update(cx, |this, cx| {
this.read_memory(memory_start..=memory_end, cx)
});
let mut rows = Vec::with_capacity(range.end - range.start);
for ix in range {
line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
rows.push(render_single_memory_view_line(
&line_buffer,
ix as u64,
weak.clone(),
cx,
));
line_buffer.clear();
}
rows
},
)
.track_scroll(self.scroll_handle.clone())
.on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
let delta = evt.delta.pixel_delta(window.line_height());
let scroll_handle = this.scroll_state.scroll_handle();
let size = scroll_handle.content_size();
let viewport = scroll_handle.viewport();
let current_offset = scroll_handle.offset();
let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32;
let last_entry_offset_boundary = size.height - first_entry_offset_boundary;
if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() {
// The topmost entry is visible, hence if we're scrolling up, we need to load extra lines.
this.view_state.schedule_scroll_up();
} else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height {
this.view_state.schedule_scroll_down();
}
scroll_handle.set_offset(current_offset + point(px(0.), delta.y));
}))
}
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(
&self.query_editor,
Self::editor_style(&self.query_editor, cx),
)
}
pub(super) fn go_to_memory_reference(
&mut self,
memory_reference: &str,
evaluate_name: Option<&str>,
stack_frame_id: Option<u64>,
cx: &mut Context<Self>,
) {
use parse_int::parse;
let Ok(as_address) = parse::<u64>(&memory_reference) else {
return;
};
let access_size = evaluate_name
.map(|typ| {
self.session.update(cx, |this, cx| {
this.data_access_size(stack_frame_id, typ, cx)
})
})
.unwrap_or_else(|| Task::ready(None));
cx.spawn(async move |this, cx| {
let access_size = access_size.await.unwrap_or(1);
this.update(cx, |this, cx| {
this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag {
start_address: as_address,
end_address: as_address + access_size - 1,
}));
this.jump_to_address(as_address, cx);
})
.ok();
})
.detach();
}
fn handle_drag(&mut self, evt: &MouseMoveEvent) {
if !evt.dragging() {
return;
}
if !self.scroll_state.is_dragging()
&& !self
.view_state
.selection
.as_ref()
.is_some_and(|selection| selection.is_dragging())
{
return;
}
let row_count = self.view_state.row_count();
debug_assert!(row_count > 1);
let scroll_handle = self.scroll_state.scroll_handle();
let viewport = scroll_handle.viewport();
let (top_area, bottom_area) = {
let size = size(viewport.size.width, viewport.size.height / 10.);
(
bounds(viewport.origin, size),
bounds(
point(viewport.origin.x, viewport.origin.y + size.height * 2.),
size,
),
)
};
if bottom_area.contains(&evt.position) {
//ix == row_count - 1 {
self.view_state.schedule_scroll_down();
} else if top_area.contains(&evt.position) {
self.view_state.schedule_scroll_up();
}
}
fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
let is_read_only = editor.read(cx).read_only(cx);
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
let text_style = TextStyle {
color: if is_read_only {
theme.colors().text_muted
} else {
theme.colors().text
},
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: settings.buffer_font.weight,
..Default::default()
};
EditorStyle {
background: theme.colors().editor_background,
local_player: theme.players().local(),
text: text_style,
..Default::default()
}
}
fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
let weak = cx.weak_entity();
let selected_width = self.view_state.line_width.clone();
DropdownMenu::new(
"memory-view-width-picker",
selected_width.label.clone(),
ContextMenu::build(window, cx, |mut this, window, cx| {
for width in &WIDTHS {
let weak = weak.clone();
let width = width.clone();
this = this.entry(width.label.clone(), None, move |_, cx| {
_ = weak.update(cx, |this, _| {
// Convert base ix between 2 line widths to keep the shown memory address roughly the same.
// All widths are powers of 2, so the conversion should be lossless.
match this.view_state.line_width.width.cmp(&width.width) {
std::cmp::Ordering::Less => {
// We're converting up.
let shift = width.width.trailing_zeros()
- this.view_state.line_width.width.trailing_zeros();
this.view_state.base_row >>= shift;
}
std::cmp::Ordering::Greater => {
// We're converting down.
let shift = this.view_state.line_width.width.trailing_zeros()
- width.width.trailing_zeros();
this.view_state.base_row <<= shift;
}
_ => {}
}
this.view_state.line_width = width.clone();
});
});
}
if let Some(ix) = WIDTHS
.iter()
.position(|width| width.width == selected_width.width)
{
for _ in 0..=ix {
this.select_next(&Default::default(), window, cx);
}
}
this
}),
)
.handle(self.width_picker_handle.clone())
}
fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.base_row = self
.view_state
.base_row
.overflowing_add(self.view_state.row_count())
.0;
cx.notify();
}
fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.base_row = self
.view_state
.base_row
.overflowing_sub(self.view_state.row_count())
.0;
cx.notify();
}
fn change_query_bar_mode(
&mut self,
is_writing_memory: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
if is_writing_memory == self.is_writing_memory {
return;
}
if !self.is_writing_memory {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
this.set_placeholder_text("Write to Selected Memory Range", cx);
});
self.is_writing_memory = true;
self.query_editor.focus_handle(cx).focus(window);
} else {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
this.set_placeholder_text("Go to Memory Address / Expression", cx);
});
self.is_writing_memory = false;
}
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection {
// Go into memory writing mode.
if !self.is_writing_memory {
let should_return = self.session.update(cx, |session, cx| {
if !session
.capabilities()
.supports_write_memory_request
.unwrap_or_default()
{
let adapter_name = session.adapter();
// We cannot write memory with this adapter.
_ = self.workspace.update(cx, |this, cx| {
this.toggle_status_toast(
StatusToast::new(format!(
"Debug Adapter `{adapter_name}` does not support writing to memory"
), cx, |this, cx| {
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
_ = this.update(cx, |_, cx| {
cx.emit(DismissEvent)
});
}).detach();
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
}),
cx,
);
});
true
} else {
false
}
});
if should_return {
return;
}
self.change_query_bar_mode(true, window, cx);
} else if self.query_editor.focus_handle(cx).is_focused(window) {
let mut text = self.query_editor.read(cx).text(cx);
if text.chars().any(|c| !c.is_ascii_hexdigit()) {
// Interpret this text as a string and oh-so-conveniently convert it.
text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
}
self.session.update(cx, |this, cx| {
let range = drag.memory_range();
if let Ok(as_hex) = hex::decode(text) {
this.write_memory(*range.start(), &as_hex, cx);
}
});
self.change_query_bar_mode(false, window, cx);
}
cx.notify();
return;
}
// Just change the currently viewed address.
if !self.query_editor.focus_handle(cx).is_focused(window) {
return;
}
self.jump_to_query_bar_address(cx);
}
fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
use parse_int::parse;
let text = self.query_editor.read(cx).text(cx);
let Ok(as_address) = parse::<u64>(&text) else {
return self.jump_to_expression(text, cx);
};
self.jump_to_address(as_address, cx);
}
fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64;
let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64;
self.scroll_handle
.scroll_to_item(line_ix as usize, ScrollStrategy::Center);
cx.notify();
}
fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
let Ok(selected_frame) = self
.stack_frame_list
.update(cx, |this, _| this.opened_stack_frame_id())
else {
return;
};
let reference = self.session.update(cx, |this, cx| {
this.memory_reference_of_expr(selected_frame, expr, cx)
});
cx.spawn(async move |this, cx| {
if let Some(reference) = reference.await {
_ = this.update(cx, |this, cx| {
let Ok(address) = parse_int::parse::<u64>(&reference) else {
return;
};
this.jump_to_address(address, cx);
});
}
})
.detach();
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.view_state.selection = None;
cx.notify();
}
/// Jump to memory pointed to by selected memory range.
fn go_to_address(
&mut self,
_: &GoToSelectedAddress,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
else {
return;
};
let range = drag.memory_range();
let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
this.read_memory(range, cx).map(|cell| cell.0).collect()
}) else {
return;
};
if memory.len() > 8 {
return;
}
let zeros_to_write = 8 - memory.len();
let mut acc = String::from("0x");
acc.extend(std::iter::repeat("00").take(zeros_to_write));
let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
_ = write!(&mut acc, "{:02x}", byte);
acc
});
self.query_editor.update(cx, |this, cx| {
this.set_text(as_query, window, cx);
});
self.jump_to_query_bar_address(cx);
}
fn deploy_memory_context_menu(
&mut self,
range: RangeInclusive<u64>,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let session = self.session.clone();
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
let memory_unreadable = |cx| {
session.update(cx, |this, cx| {
this.read_memory(range.clone(), cx)
.any(|cell| cell.0.is_none())
})
};
menu.action_disabled_when(
range_too_large || memory_unreadable(cx),
"Go To Selected Address",
GoToSelectedAddress.boxed_clone(),
)
.context(self.focus_handle.clone())
});
cx.focus_view(&context_menu, window);
let subscription = cx.subscribe_in(
&context_menu,
window,
|this, _, _: &DismissEvent, window, cx| {
if this.open_context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(window, cx)
}) {
cx.focus_self(window);
}
this.open_context_menu.take();
cx.notify();
},
);
self.open_context_menu = Some((context_menu, position, subscription));
}
}
#[derive(Clone)]
struct ViewWidth {
width: u8,
label: SharedString,
}
impl ViewWidth {
const fn new(width: u8, label: &'static str) -> Self {
Self {
width,
label: SharedString::new_static(label),
}
}
}
static WIDTHS: [ViewWidth; 7] = [
ViewWidth::new(1, "1 byte"),
ViewWidth::new(2, "2 bytes"),
ViewWidth::new(4, "4 bytes"),
ViewWidth::new(8, "8 bytes"),
ViewWidth::new(16, "16 bytes"),
ViewWidth::new(32, "32 bytes"),
ViewWidth::new(64, "64 bytes"),
];
fn render_single_memory_view_line(
memory: &[MemoryCell],
ix: u64,
weak: gpui::WeakEntity<MemoryView>,
cx: &mut App,
) -> AnyElement {
let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else {
return div().into_any();
};
let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
h_flex()
.id((
"memory-view-row-full",
ix * view_state.line_width.width as u64,
))
.size_full()
.gap_x_2()
.child(
div()
.child(
Label::new(format!("{:016X}", base_address))
.buffer_font(cx)
.size(ui::LabelSize::Small)
.color(Color::Muted),
)
.px_1()
.border_r_1()
.border_color(Color::Muted.color(cx)),
)
.child(
h_flex()
.id((
"memory-view-row-raw-memory",
ix * view_state.line_width.width as u64,
))
.px_1()
.children(memory.iter().enumerate().map(|(cell_ix, cell)| {
let weak = weak.clone();
div()
.id(("memory-view-row-raw-memory-cell", cell_ix as u64))
.px_0p5()
.when_some(view_state.selection.as_ref(), |this, selection| {
this.when(selection.contains(base_address + cell_ix as u64), |this| {
let weak = weak.clone();
this.bg(Color::Accent.color(cx)).when(
!selection.is_dragging(),
|this| {
let selection = selection.drag().memory_range();
this.on_mouse_down(
MouseButton::Right,
move |click, window, cx| {
_ = weak.update(cx, |this, cx| {
this.deploy_memory_context_menu(
selection.clone(),
click.position,
window,
cx,
)
});
cx.stop_propagation();
},
)
},
)
})
})
.child(
Label::new(
cell.0
.map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
.unwrap_or_else(|| UNKNOWN_BYTE.clone()),
)
.buffer_font(cx)
.when(cell.0.is_none(), |this| this.color(Color::Muted))
.size(ui::LabelSize::Small),
)
.on_drag(
Drag {
start_address: base_address + cell_ix as u64,
end_address: base_address + cell_ix as u64,
},
{
let weak = weak.clone();
move |drag, _, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragUnderway(drag.clone()));
});
cx.new(|_| Empty)
}
},
)
.on_drop({
let weak = weak.clone();
move |drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragComplete(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
}));
});
}
})
.drag_over(move |style, drag: &Drag, _, cx| {
_ = weak.update(cx, |this, _| {
this.view_state.selection =
Some(SelectedMemoryRange::DragUnderway(Drag {
start_address: drag.start_address,
end_address: base_address + cell_ix as u64,
}));
});
style
})
})),
)
.child(
h_flex()
.id((
"memory-view-row-ascii-memory",
ix * view_state.line_width.width as u64,
))
.h_full()
.px_1()
.mr_4()
// .gap_x_1p5()
.border_x_1()
.border_color(Color::Muted.color(cx))
.children(memory.iter().enumerate().map(|(ix, cell)| {
let as_character = char::from(cell.0.unwrap_or(0));
let as_visible = if as_character.is_ascii_graphic() {
as_character
} else {
'·'
};
div()
.px_0p5()
.when_some(view_state.selection.as_ref(), |this, selection| {
this.when(selection.contains(base_address + ix as u64), |this| {
this.bg(Color::Accent.color(cx))
})
})
.child(
Label::new(format!("{as_visible}"))
.buffer_font(cx)
.when(cell.0.is_none(), |this| this.color(Color::Muted))
.size(ui::LabelSize::Small),
)
})),
)
.into_any()
}
impl Render for MemoryView {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let (icon, tooltip_text) = if self.is_writing_memory {
(IconName::Pencil, "Edit memory at a selected address")
} else {
(
IconName::LocationEdit,
"Change address of currently viewed memory",
)
};
v_flex()
.id("Memory-view")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::go_to_address))
.p_1()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::page_down))
.on_action(cx.listener(Self::page_up))
.size_full()
.track_focus(&self.focus_handle)
.on_hover(cx.listener(|this, hovered, window, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(window, cx) {
this.hide_scrollbar(window, cx);
}
}))
.child(
h_flex()
.w_full()
.mb_0p5()
.gap_1()
.child(
h_flex()
.w_full()
.rounded_md()
.border_1()
.gap_x_2()
.px_2()
.py_0p5()
.mb_0p5()
.bg(cx.theme().colors().editor_background)
.when_else(
self.query_editor
.focus_handle(cx)
.contains_focused(window, cx),
|this| this.border_color(cx.theme().colors().border_focused),
|this| this.border_color(cx.theme().colors().border_transparent),
)
.child(
div()
.id("memory-view-editor-icon")
.child(Icon::new(icon).size(ui::IconSize::XSmall))
.tooltip(Tooltip::text(tooltip_text)),
)
.child(self.render_query_bar(cx)),
)
.child(self.render_width_picker(window, cx)),
)
.child(Divider::horizontal())
.child(
v_flex()
.size_full()
.on_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| {
this.handle_drag(evt);
}))
.child(self.render_memory(cx).size_full())
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
.position(*position)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)
}))
.children(self.render_vertical_scrollbar(cx)),
)
}
}

View file

@ -1,3 +1,5 @@
use crate::session::running::{RunningState, memory_view::MemoryView};
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
use dap::{
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
@ -7,13 +9,14 @@ use editor::Editor;
use gpui::{
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::debugger::session::{Session, SessionEvent, Watcher};
use std::{collections::HashMap, ops::Range, sync::Arc};
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::debug_panic;
use util::{debug_panic, maybe};
actions!(
variable_list,
@ -32,6 +35,8 @@ actions!(
AddWatch,
/// Removes the selected variable from the watch list.
RemoveWatch,
/// Jump to variable's memory location.
GoToMemory,
]
);
@ -86,30 +91,30 @@ impl EntryPath {
}
#[derive(Debug, Clone, PartialEq)]
enum EntryKind {
enum DapEntry {
Watcher(Watcher),
Variable(dap::Variable),
Scope(dap::Scope),
}
impl EntryKind {
impl DapEntry {
fn as_watcher(&self) -> Option<&Watcher> {
match self {
EntryKind::Watcher(watcher) => Some(watcher),
DapEntry::Watcher(watcher) => Some(watcher),
_ => None,
}
}
fn as_variable(&self) -> Option<&dap::Variable> {
match self {
EntryKind::Variable(dap) => Some(dap),
DapEntry::Variable(dap) => Some(dap),
_ => None,
}
}
fn as_scope(&self) -> Option<&dap::Scope> {
match self {
EntryKind::Scope(dap) => Some(dap),
DapEntry::Scope(dap) => Some(dap),
_ => None,
}
}
@ -117,38 +122,38 @@ impl EntryKind {
#[cfg(test)]
fn name(&self) -> &str {
match self {
EntryKind::Watcher(watcher) => &watcher.expression,
EntryKind::Variable(dap) => &dap.name,
EntryKind::Scope(dap) => &dap.name,
DapEntry::Watcher(watcher) => &watcher.expression,
DapEntry::Variable(dap) => &dap.name,
DapEntry::Scope(dap) => &dap.name,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct ListEntry {
dap_kind: EntryKind,
entry: DapEntry,
path: EntryPath,
}
impl ListEntry {
fn as_watcher(&self) -> Option<&Watcher> {
self.dap_kind.as_watcher()
self.entry.as_watcher()
}
fn as_variable(&self) -> Option<&dap::Variable> {
self.dap_kind.as_variable()
self.entry.as_variable()
}
fn as_scope(&self) -> Option<&dap::Scope> {
self.dap_kind.as_scope()
self.entry.as_scope()
}
fn item_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
let mut id = match &self.entry {
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@ -158,10 +163,10 @@ impl ListEntry {
fn item_value_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
let mut id = match &self.entry {
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
};
for name in self.path.indices.iter() {
_ = write!(id, "-{}", name);
@ -188,13 +193,17 @@ pub struct VariableList {
focus_handle: FocusHandle,
edited_path: Option<(EntryPath, Entity<Editor>)>,
disabled: bool,
memory_view: Entity<MemoryView>,
weak_running: WeakEntity<RunningState>,
_subscriptions: Vec<Subscription>,
}
impl VariableList {
pub fn new(
pub(crate) fn new(
session: Entity<Session>,
stack_frame_list: Entity<StackFrameList>,
memory_view: Entity<MemoryView>,
weak_running: WeakEntity<RunningState>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@ -234,6 +243,8 @@ impl VariableList {
edited_path: None,
entries: Default::default(),
entry_states: Default::default(),
weak_running,
memory_view,
}
}
@ -284,7 +295,7 @@ impl VariableList {
scope.variables_reference,
scope.variables_reference,
EntryPath::for_scope(&scope.name),
EntryKind::Scope(scope),
DapEntry::Scope(scope),
)
})
.collect::<Vec<_>>();
@ -298,7 +309,7 @@ impl VariableList {
watcher.variables_reference,
watcher.variables_reference,
EntryPath::for_watcher(watcher.expression.clone()),
EntryKind::Watcher(watcher.clone()),
DapEntry::Watcher(watcher.clone()),
)
})
.collect::<Vec<_>>(),
@ -309,9 +320,9 @@ impl VariableList {
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
{
match &dap_kind {
EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()),
DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()),
}
let var_state = self
@ -336,7 +347,7 @@ impl VariableList {
});
entries.push(ListEntry {
dap_kind,
entry: dap_kind,
path: path.clone(),
});
@ -349,7 +360,7 @@ impl VariableList {
variables_reference,
child.variables_reference,
path.with_child(child.name.clone().into()),
EntryKind::Variable(child),
DapEntry::Variable(child),
)
}));
}
@ -380,9 +391,9 @@ impl VariableList {
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(dap) => Some(dap.clone()),
EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
.filter_map(|entry| match &entry.entry {
DapEntry::Variable(dap) => Some(dap.clone()),
DapEntry::Scope(_) | DapEntry::Watcher { .. } => None,
})
.collect()
}
@ -400,12 +411,12 @@ impl VariableList {
.get(ix)
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
match &entry.dap_kind {
EntryKind::Watcher { .. } => {
match &entry.entry {
DapEntry::Watcher { .. } => {
Some(self.render_watcher(entry, *state, window, cx))
}
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)),
}
})
.collect()
@ -562,6 +573,51 @@ impl VariableList {
}
}
fn jump_to_variable_memory(
&mut self,
_: &GoToMemory,
window: &mut Window,
cx: &mut Context<Self>,
) {
_ = maybe!({
let selection = self.selection.as_ref()?;
let entry = self.entries.iter().find(|entry| &entry.path == selection)?;
let var = entry.entry.as_variable()?;
let memory_reference = var.memory_reference.as_deref()?;
let sizeof_expr = if var.type_.as_ref().is_some_and(|t| {
t.chars()
.all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
}) {
var.type_.as_deref()
} else {
var.evaluate_name
.as_deref()
.map(|name| name.strip_prefix("/nat ").unwrap_or_else(|| name))
};
self.memory_view.update(cx, |this, cx| {
this.go_to_memory_reference(
memory_reference,
sizeof_expr,
self.selected_stack_frame_id,
cx,
);
});
let weak_panel = self.weak_running.clone();
window.defer(cx, move |window, cx| {
_ = weak_panel.update(cx, |this, cx| {
this.activate_item(
crate::persistence::DebuggerPaneItem::MemoryView,
window,
cx,
);
});
});
Some(())
});
}
fn deploy_list_entry_context_menu(
&mut self,
entry: ListEntry,
@ -584,6 +640,7 @@ impl VariableList {
menu.action("Edit Value", EditVariable.boxed_clone())
})
.action("Watch Variable", AddWatch.boxed_clone())
.action("Go To Memory", GoToMemory.boxed_clone())
})
.when(entry.as_watcher().is_some(), |menu| {
menu.action("Copy Name", CopyVariableName.boxed_clone())
@ -628,10 +685,10 @@ impl VariableList {
return;
};
let variable_name = match &entry.dap_kind {
EntryKind::Variable(dap) => dap.name.clone(),
EntryKind::Watcher(watcher) => watcher.expression.to_string(),
EntryKind::Scope(_) => return,
let variable_name = match &entry.entry {
DapEntry::Variable(dap) => dap.name.clone(),
DapEntry::Watcher(watcher) => watcher.expression.to_string(),
DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
@ -651,10 +708,10 @@ impl VariableList {
return;
};
let variable_value = match &entry.dap_kind {
EntryKind::Variable(dap) => dap.value.clone(),
EntryKind::Watcher(watcher) => watcher.value.to_string(),
EntryKind::Scope(_) => return,
let variable_value = match &entry.entry {
DapEntry::Variable(dap) => dap.value.clone(),
DapEntry::Watcher(watcher) => watcher.value.to_string(),
DapEntry::Scope(_) => return,
};
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
@ -669,10 +726,10 @@ impl VariableList {
return;
};
let variable_value = match &entry.dap_kind {
EntryKind::Watcher(watcher) => watcher.value.to_string(),
EntryKind::Variable(variable) => variable.value.clone(),
EntryKind::Scope(_) => return,
let variable_value = match &entry.entry {
DapEntry::Watcher(watcher) => watcher.value.to_string(),
DapEntry::Variable(variable) => variable.value.clone(),
DapEntry::Scope(_) => return,
};
let editor = Self::create_variable_editor(&variable_value, window, cx);
@ -753,7 +810,7 @@ impl VariableList {
"{}{} {}{}",
INDENT.repeat(state.depth - 1),
if state.is_expanded { "v" } else { ">" },
entry.dap_kind.name(),
entry.entry.name(),
if self.selection.as_ref() == Some(&entry.path) {
" <=== selected"
} else {
@ -770,8 +827,8 @@ impl VariableList {
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Scope(scope) => Some(scope),
.filter_map(|entry| match &entry.entry {
DapEntry::Scope(scope) => Some(scope),
_ => None,
})
.cloned()
@ -785,10 +842,10 @@ impl VariableList {
let mut idx = 0;
for entry in self.entries.iter() {
match &entry.dap_kind {
EntryKind::Watcher { .. } => continue,
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
EntryKind::Scope(scope) => {
match &entry.entry {
DapEntry::Watcher { .. } => continue,
DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
DapEntry::Scope(scope) => {
if scopes.len() > 0 {
idx += 1;
}
@ -806,8 +863,8 @@ impl VariableList {
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(variable) => Some(variable),
.filter_map(|entry| match &entry.entry {
DapEntry::Variable(variable) => Some(variable),
_ => None,
})
.cloned()
@ -1358,6 +1415,7 @@ impl Render for VariableList {
.on_action(cx.listener(Self::edit_variable))
.on_action(cx.listener(Self::add_watcher))
.on_action(cx.listener(Self::remove_watcher))
.on_action(cx.listener(Self::jump_to_variable_memory))
.child(
uniform_list(
"variable-list",

View file

@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
});
running_state.update_in(cx, |this, window, cx| {
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
this.activate_item(DebuggerPaneItem::Modules, window, cx);
cx.refresh_windows();
});