diff --git a/Cargo.lock b/Cargo.lock
index 97b1e33211..da46d191ef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4393,12 +4393,15 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
+ "hex",
"indoc",
"itertools 0.14.0",
"language",
"log",
"menu",
+ "notifications",
"parking_lot",
+ "parse_int",
"paths",
"picker",
"pretty_assertions",
@@ -11276,6 +11279,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "parse_int"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "partial-json-fixer"
version = "0.5.3"
@@ -12319,6 +12331,7 @@ dependencies = [
"anyhow",
"askpass",
"async-trait",
+ "base64 0.22.1",
"buffer_diff",
"circular-buffer",
"client",
@@ -12364,6 +12377,7 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"shlex",
+ "smallvec",
"smol",
"snippet",
"snippet_provider",
diff --git a/Cargo.toml b/Cargo.toml
index dedf570052..e270dd1891 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -507,6 +507,7 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
+parse_int = "0.9"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
diff --git a/assets/icons/location_edit.svg b/assets/icons/location_edit.svg
new file mode 100644
index 0000000000..de82e8db4e
--- /dev/null
+++ b/assets/icons/location_edit.svg
@@ -0,0 +1 @@
+
diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml
index fe9640b7b9..ebb135c1d9 100644
--- a/crates/debugger_ui/Cargo.toml
+++ b/crates/debugger_ui/Cargo.toml
@@ -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
diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs
index 37064d5d5d..bf5f313918 100644
--- a/crates/debugger_ui/src/debugger_panel.rs
+++ b/crates/debugger_ui/src/debugger_panel.rs
@@ -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)
diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs
index d15244c349..3a0ad7a40e 100644
--- a/crates/debugger_ui/src/persistence.rs
+++ b/crates/debugger_ui/src/persistence.rs
@@ -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,
loaded_sources: &Entity,
terminal: &Entity,
+ memory_view: &Entity,
subscriptions: &mut HashMap,
window: &mut Window,
cx: &mut Context,
@@ -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();
diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs
index 264d46370f..2651a94520 100644
--- a/crates/debugger_ui/src/session/running.rs
+++ b/crates/debugger_ui/src/session/running.rs
@@ -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>,
pub(crate) scenario: Option,
pub(crate) scenario_context: Option,
+ memory_view: Entity,
}
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.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)]
diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs
new file mode 100644
index 0000000000..e9dcb0839d
--- /dev/null
+++ b/crates/debugger_ui/src/session/running/memory_view.rs
@@ -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,
+ scroll_handle: UniformListScrollHandle,
+ scroll_state: ScrollbarState,
+ show_scrollbar: bool,
+ stack_frame_list: WeakEntity,
+ hide_scrollbar_task: Option>,
+ focus_handle: FocusHandle,
+ view_state: ViewState,
+ query_editor: Entity,
+ session: Entity,
+ width_picker_handle: PopoverMenuHandle,
+ is_writing_memory: bool,
+ open_context_menu: Option<(Entity, Point, 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 {
+ 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,
+}
+
+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,
+ workspace: WeakEntity,
+ stack_frame_list: WeakEntity,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> 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) {
+ 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) -> Option> {
+ 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) -> 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) -> 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,
+ cx: &mut Context,
+ ) {
+ use parse_int::parse;
+ let Ok(as_address) = parse::(&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, cx: &Context) -> 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) -> 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.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.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,
+ ) {
+ 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) {
+ 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) {
+ use parse_int::parse;
+ let text = self.query_editor.read(cx).text(cx);
+
+ let Ok(as_address) = parse::(&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.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) {
+ 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::(&reference) else {
+ return;
+ };
+ this.jump_to_address(address, cx);
+ });
+ }
+ })
+ .detach();
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) {
+ 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,
+ ) {
+ let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone()
+ else {
+ return;
+ };
+ let range = drag.memory_range();
+ let Some(memory): Option> = 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,
+ position: Point,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ 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::() 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,
+ 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,
+ ) -> 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)),
+ )
+ }
+}
diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs
index bdb095bde3..c7df449ee6 100644
--- a/crates/debugger_ui/src/session/running/variable_list.rs
+++ b/crates/debugger_ui/src/session/running/variable_list.rs
@@ -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)>,
disabled: bool,
+ memory_view: Entity,
+ weak_running: WeakEntity,
_subscriptions: Vec,
}
impl VariableList {
- pub fn new(
+ pub(crate) fn new(
session: Entity,
stack_frame_list: Entity,
+ memory_view: Entity,
+ weak_running: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> 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::>();
@@ -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::>(),
@@ -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) -> Vec {
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,
+ ) {
+ _ = 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 {
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 {
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",
diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs
index 49cfd6fcf8..09c90cbc4a 100644
--- a/crates/debugger_ui/src/tests/module_list.rs
+++ b/crates/debugger_ui/src/tests/module_list.rs
@@ -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();
});
diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs
index 6e05b384e1..cb53276bc2 100644
--- a/crates/gpui/src/elements/div.rs
+++ b/crates/gpui/src/elements/div.rs
@@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized {
/// Apply the given style when the given data type is dragged over this element
fn drag_over(
mut self,
- f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
+ f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement,
) -> Self {
self.interactivity().drag_over_styles.push((
TypeId::of::(),
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 4151e6b2ea..29f7a8f50d 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -163,6 +163,7 @@ pub enum IconName {
ListTree,
ListX,
LoadCircle,
+ LocationEdit,
LockOutlined,
LspDebug,
LspRestart,
diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml
index 729d61aab5..57d6d6ca28 100644
--- a/crates/project/Cargo.toml
+++ b/crates/project/Cargo.toml
@@ -31,6 +31,7 @@ aho-corasick.workspace = true
anyhow.workspace = true
askpass.workspace = true
async-trait.workspace = true
+base64.workspace = true
buffer_diff.workspace = true
circular-buffer.workspace = true
client.workspace = true
@@ -72,6 +73,7 @@ settings.workspace = true
sha2.workspace = true
shellexpand.workspace = true
shlex.workspace = true
+smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
snippet_provider.workspace = true
diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs
index d078988a51..6c22468040 100644
--- a/crates/project/src/debugger.rs
+++ b/crates/project/src/debugger.rs
@@ -15,7 +15,9 @@ pub mod breakpoint_store;
pub mod dap_command;
pub mod dap_store;
pub mod locators;
+mod memory;
pub mod session;
#[cfg(any(feature = "test-support", test))]
pub mod test;
+pub use memory::MemoryCell;
diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs
index 411bacd3ba..1a3587024e 100644
--- a/crates/project/src/debugger/dap_command.rs
+++ b/crates/project/src/debugger/dap_command.rs
@@ -1,6 +1,7 @@
use std::sync::Arc;
use anyhow::{Context as _, Ok, Result};
+use base64::Engine;
use dap::{
Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments,
InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint,
@@ -1774,3 +1775,95 @@ impl DapCommand for LocationsCommand {
})
}
}
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub(crate) struct ReadMemory {
+ pub(crate) memory_reference: String,
+ pub(crate) offset: Option,
+ pub(crate) count: u64,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct ReadMemoryResponse {
+ pub(super) address: Arc,
+ pub(super) unreadable_bytes: Option,
+ pub(super) content: Arc<[u8]>,
+}
+
+impl LocalDapCommand for ReadMemory {
+ type Response = ReadMemoryResponse;
+ type DapRequest = dap::requests::ReadMemory;
+ const CACHEABLE: bool = true;
+
+ fn is_supported(capabilities: &Capabilities) -> bool {
+ capabilities
+ .supports_read_memory_request
+ .unwrap_or_default()
+ }
+ fn to_dap(&self) -> ::Arguments {
+ dap::ReadMemoryArguments {
+ memory_reference: self.memory_reference.clone(),
+ offset: self.offset,
+ count: self.count,
+ }
+ }
+
+ fn response_from_dap(
+ &self,
+ message: ::Response,
+ ) -> Result {
+ let data = if let Some(data) = message.data {
+ base64::engine::general_purpose::STANDARD
+ .decode(data)
+ .log_err()
+ .context("parsing base64 data from DAP's ReadMemory response")?
+ } else {
+ vec![]
+ };
+
+ Ok(ReadMemoryResponse {
+ address: message.address.into(),
+ content: data.into(),
+ unreadable_bytes: message.unreadable_bytes,
+ })
+ }
+}
+
+impl LocalDapCommand for dap::DataBreakpointInfoArguments {
+ type Response = dap::DataBreakpointInfoResponse;
+ type DapRequest = dap::requests::DataBreakpointInfo;
+ const CACHEABLE: bool = true;
+ fn is_supported(capabilities: &Capabilities) -> bool {
+ capabilities.supports_data_breakpoints.unwrap_or_default()
+ }
+ fn to_dap(&self) -> ::Arguments {
+ self.clone()
+ }
+
+ fn response_from_dap(
+ &self,
+ message: ::Response,
+ ) -> Result {
+ Ok(message)
+ }
+}
+
+impl LocalDapCommand for dap::WriteMemoryArguments {
+ type Response = dap::WriteMemoryResponse;
+ type DapRequest = dap::requests::WriteMemory;
+ fn is_supported(capabilities: &Capabilities) -> bool {
+ capabilities
+ .supports_write_memory_request
+ .unwrap_or_default()
+ }
+ fn to_dap(&self) -> ::Arguments {
+ self.clone()
+ }
+
+ fn response_from_dap(
+ &self,
+ message: ::Response,
+ ) -> Result {
+ Ok(message)
+ }
+}
diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs
new file mode 100644
index 0000000000..fec3c344c5
--- /dev/null
+++ b/crates/project/src/debugger/memory.rs
@@ -0,0 +1,384 @@
+//! This module defines the format in which memory of debuggee is represented.
+//!
+//! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold:
+//! - We assume that the memory is divided into pages of a fixed size.
+//! - We assume that each page can be either mapped or unmapped.
+//! These two assumptions drive the shape of the memory representation.
+//! In particular, we want the unmapped pages to be represented without allocating any memory, as *most*
+//! of the memory in a program space is usually unmapped.
+//! Note that per DAP we don't know what the address space layout is, so we can't optimize off of it.
+//! Note that while we optimize for a paged layout, we also want to be able to represent memory that is not paged.
+//! This use case is relevant to embedded folks. Furthermore, we cater to default 4k page size.
+//! It is picked arbitrarily as a ubiquous default - other than that, the underlying format of Zed's memory storage should not be relevant
+//! to the users of this module.
+
+use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
+
+use gpui::BackgroundExecutor;
+use smallvec::SmallVec;
+
+const PAGE_SIZE: u64 = 4096;
+
+/// Represents the contents of a single page. We special-case unmapped pages to be allocation-free,
+/// since they're going to make up the majority of the memory in a program space (even though the user might not even get to see them - ever).
+#[derive(Clone, Debug)]
+pub(super) enum PageContents {
+ /// Whole page is unreadable.
+ Unmapped,
+ Mapped(Arc),
+}
+
+impl PageContents {
+ #[cfg(test)]
+ fn mapped(contents: Vec) -> Self {
+ PageContents::Mapped(Arc::new(MappedPageContents(
+ vec![PageChunk::Mapped(contents.into())].into(),
+ )))
+ }
+}
+
+#[derive(Clone, Debug)]
+enum PageChunk {
+ Mapped(Arc<[u8]>),
+ Unmapped(u64),
+}
+
+impl PageChunk {
+ fn len(&self) -> u64 {
+ match self {
+ PageChunk::Mapped(contents) => contents.len() as u64,
+ PageChunk::Unmapped(size) => *size,
+ }
+ }
+}
+
+impl MappedPageContents {
+ fn len(&self) -> u64 {
+ self.0.iter().map(|chunk| chunk.len()).sum()
+ }
+}
+/// We hope for the whole page to be mapped in a single chunk, but we do leave the possibility open
+/// of having interleaved read permissions in a single page; debuggee's execution environment might either
+/// have a different page size OR it might not have paged memory layout altogether
+/// (which might be relevant to embedded systems).
+///
+/// As stated previously, the concept of a page in this module has to do more
+/// with optimizing fetching of the memory and not with the underlying bits and pieces
+/// of the memory of a debuggee.
+
+#[derive(Default, Debug)]
+pub(super) struct MappedPageContents(
+ /// Most of the time there should be only one chunk (either mapped or unmapped),
+ /// but we do leave the possibility open of having multiple regions of memory in a single page.
+ SmallVec<[PageChunk; 1]>,
+);
+
+type MemoryAddress = u64;
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
+#[repr(transparent)]
+pub(super) struct PageAddress(u64);
+
+impl PageAddress {
+ pub(super) fn iter_range(
+ range: RangeInclusive,
+ ) -> impl Iterator- {
+ let mut current = range.start().0;
+ let end = range.end().0;
+
+ std::iter::from_fn(move || {
+ if current > end {
+ None
+ } else {
+ let addr = PageAddress(current);
+ current += PAGE_SIZE;
+ Some(addr)
+ }
+ })
+ }
+}
+
+pub(super) struct Memory {
+ pages: BTreeMap,
+}
+
+/// Represents a single memory cell (or None if a given cell is unmapped/unknown).
+#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq)]
+#[repr(transparent)]
+pub struct MemoryCell(pub Option);
+
+impl Memory {
+ pub(super) fn new() -> Self {
+ Self {
+ pages: Default::default(),
+ }
+ }
+
+ pub(super) fn memory_range_to_page_range(
+ range: RangeInclusive,
+ ) -> RangeInclusive {
+ let start_page = (range.start() / PAGE_SIZE) * PAGE_SIZE;
+ let end_page = (range.end() / PAGE_SIZE) * PAGE_SIZE;
+ PageAddress(start_page)..=PageAddress(end_page)
+ }
+
+ pub(super) fn build_page(&self, page_address: PageAddress) -> Option {
+ if self.pages.contains_key(&page_address) {
+ // We already know the state of this page.
+ None
+ } else {
+ Some(MemoryPageBuilder::new(page_address))
+ }
+ }
+
+ pub(super) fn insert_page(&mut self, address: PageAddress, page: PageContents) {
+ self.pages.insert(address, page);
+ }
+
+ pub(super) fn memory_range(&self, range: RangeInclusive) -> MemoryIterator {
+ let pages = Self::memory_range_to_page_range(range.clone());
+ let pages = self
+ .pages
+ .range(pages)
+ .map(|(address, page)| (*address, page.clone()))
+ .collect::>();
+ MemoryIterator::new(range, pages.into_iter())
+ }
+
+ pub(crate) fn clear(&mut self, background_executor: &BackgroundExecutor) {
+ let memory = std::mem::take(&mut self.pages);
+ background_executor
+ .spawn(async move {
+ drop(memory);
+ })
+ .detach();
+ }
+}
+
+/// Builder for memory pages.
+///
+/// Memory reads in DAP are sequential (or at least we make them so).
+/// ReadMemory response includes `unreadableBytes` property indicating the number of bytes
+/// that could not be read after the last successfully read byte.
+///
+/// We use it as follows:
+/// - We start off with a "large" 1-page ReadMemory request.
+/// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page.
+/// - If it succeeds partially, we know # of mapped bytes.
+/// We might also know the # of unmapped bytes.
+/// However, we're still unsure about what's *after* the unreadable region.
+///
+/// This is where this builder comes in. It lets us track the state of figuring out contents of a single page.
+pub(super) struct MemoryPageBuilder {
+ chunks: MappedPageContents,
+ base_address: PageAddress,
+ left_to_read: u64,
+}
+
+/// Represents a chunk of memory of which we don't know if it's mapped or unmapped; thus we need
+/// to issue a request to figure out it's state.
+pub(super) struct UnknownMemory {
+ pub(super) address: MemoryAddress,
+ pub(super) size: u64,
+}
+
+impl MemoryPageBuilder {
+ fn new(base_address: PageAddress) -> Self {
+ Self {
+ chunks: Default::default(),
+ base_address,
+ left_to_read: PAGE_SIZE,
+ }
+ }
+
+ pub(super) fn build(self) -> (PageAddress, PageContents) {
+ debug_assert_eq!(self.left_to_read, 0);
+ debug_assert_eq!(
+ self.chunks.len(),
+ PAGE_SIZE,
+ "Expected `build` to be called on a fully-fetched page"
+ );
+ let contents = if let Some(first) = self.chunks.0.first()
+ && self.chunks.len() == 1
+ && matches!(first, PageChunk::Unmapped(PAGE_SIZE))
+ {
+ PageContents::Unmapped
+ } else {
+ PageContents::Mapped(Arc::new(MappedPageContents(self.chunks.0)))
+ };
+ (self.base_address, contents)
+ }
+ /// Drives the fetching of memory, in an iterator-esque style.
+ pub(super) fn next_request(&self) -> Option {
+ if self.left_to_read == 0 {
+ None
+ } else {
+ let offset_in_current_page = PAGE_SIZE - self.left_to_read;
+ Some(UnknownMemory {
+ address: self.base_address.0 + offset_in_current_page,
+ size: self.left_to_read,
+ })
+ }
+ }
+ pub(super) fn unknown(&mut self, bytes: u64) {
+ if bytes == 0 {
+ return;
+ }
+ self.left_to_read -= bytes;
+ self.chunks.0.push(PageChunk::Unmapped(bytes));
+ }
+ pub(super) fn known(&mut self, data: Arc<[u8]>) {
+ if data.is_empty() {
+ return;
+ }
+ self.left_to_read -= data.len() as u64;
+ self.chunks.0.push(PageChunk::Mapped(data));
+ }
+}
+
+fn page_contents_into_iter(data: Arc) -> Box> {
+ let mut data_range = 0..data.0.len();
+ let iter = std::iter::from_fn(move || {
+ let data = &data;
+ let data_ref = data.clone();
+ data_range.next().map(move |index| {
+ let contents = &data_ref.0[index];
+ match contents {
+ PageChunk::Mapped(items) => {
+ let chunk_range = 0..items.len();
+ let items = items.clone();
+ Box::new(
+ chunk_range
+ .into_iter()
+ .map(move |ix| MemoryCell(Some(items[ix]))),
+ ) as Box>
+ }
+ PageChunk::Unmapped(len) => {
+ Box::new(std::iter::repeat_n(MemoryCell(None), *len as usize))
+ }
+ }
+ })
+ })
+ .flatten();
+
+ Box::new(iter)
+}
+/// Defines an iteration over a range of memory. Some of this memory might be unmapped or straight up missing.
+/// Thus, this iterator alternates between synthesizing values and yielding known memory.
+pub struct MemoryIterator {
+ start: MemoryAddress,
+ end: MemoryAddress,
+ current_known_page: Option<(PageAddress, Box>)>,
+ pages: std::vec::IntoIter<(PageAddress, PageContents)>,
+}
+
+impl MemoryIterator {
+ fn new(
+ range: RangeInclusive,
+ pages: std::vec::IntoIter<(PageAddress, PageContents)>,
+ ) -> Self {
+ Self {
+ start: *range.start(),
+ end: *range.end(),
+ current_known_page: None,
+ pages,
+ }
+ }
+ fn fetch_next_page(&mut self) -> bool {
+ if let Some((mut address, chunk)) = self.pages.next() {
+ let mut contents = match chunk {
+ PageContents::Unmapped => None,
+ PageContents::Mapped(mapped_page_contents) => {
+ Some(page_contents_into_iter(mapped_page_contents))
+ }
+ };
+
+ if address.0 < self.start {
+ // Skip ahead till our iterator is at the start of the range
+
+ //address: 20, start: 25
+ //
+ let to_skip = self.start - address.0;
+ address.0 += to_skip;
+ if let Some(contents) = &mut contents {
+ contents.nth(to_skip as usize - 1);
+ }
+ }
+ self.current_known_page = contents.map(|contents| (address, contents));
+ true
+ } else {
+ false
+ }
+ }
+}
+impl Iterator for MemoryIterator {
+ type Item = MemoryCell;
+
+ fn next(&mut self) -> Option {
+ if self.start > self.end {
+ return None;
+ }
+ if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut()
+ {
+ if current_page_address.0 <= self.start {
+ if let Some(next_cell) = current_memory_chunk.next() {
+ self.start += 1;
+ return Some(next_cell);
+ } else {
+ self.current_known_page.take();
+ }
+ }
+ }
+ if !self.fetch_next_page() {
+ self.start += 1;
+ return Some(MemoryCell(None));
+ } else {
+ self.next()
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::debugger::{
+ MemoryCell,
+ memory::{MemoryIterator, PageAddress, PageContents},
+ };
+
+ #[test]
+ fn iterate_over_unmapped_memory() {
+ let empty_iterator = MemoryIterator::new(0..=127, Default::default());
+ let actual = empty_iterator.collect::>();
+ let expected = vec![MemoryCell(None); 128];
+ assert_eq!(actual.len(), expected.len());
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn iterate_over_partially_mapped_memory() {
+ let it = MemoryIterator::new(
+ 0..=127,
+ vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(),
+ );
+ let actual = it.collect::>();
+ let expected = std::iter::repeat_n(MemoryCell(None), 5)
+ .chain(std::iter::once(MemoryCell(Some(1))))
+ .chain(std::iter::repeat_n(MemoryCell(None), 122))
+ .collect::>();
+ assert_eq!(actual.len(), expected.len());
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn reads_from_the_middle_of_a_page() {
+ let partial_iter = MemoryIterator::new(
+ 20..=30,
+ vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(),
+ );
+ let actual = partial_iter.collect::>();
+ let expected = (20..=30)
+ .map(|val| MemoryCell(Some(val)))
+ .collect::>();
+ assert_eq!(actual.len(), expected.len());
+ assert_eq!(actual, expected);
+ }
+}
diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs
index f526d5a3fe..53c13e13c3 100644
--- a/crates/project/src/debugger/session.rs
+++ b/crates/project/src/debugger/session.rs
@@ -1,4 +1,6 @@
use crate::debugger::breakpoint_store::BreakpointSessionState;
+use crate::debugger::dap_command::ReadMemory;
+use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress};
use super::breakpoint_store::{
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
@@ -13,6 +15,7 @@ use super::dap_command::{
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
+use base64::Engine;
use collections::{HashMap, HashSet, IndexMap};
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
use dap::messages::Response;
@@ -26,7 +29,7 @@ use dap::{
use dap::{
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
- StartDebuggingRequestArgumentsRequest, VariablePresentationHint,
+ StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
};
use futures::SinkExt;
use futures::channel::mpsc::UnboundedSender;
@@ -42,6 +45,7 @@ use serde_json::Value;
use smol::stream::StreamExt;
use std::any::TypeId;
use std::collections::BTreeMap;
+use std::ops::RangeInclusive;
use std::u64;
use std::{
any::Any,
@@ -52,7 +56,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
-use util::ResultExt;
+use util::{ResultExt, maybe};
use worktree::Worktree;
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -685,6 +689,7 @@ pub struct Session {
background_tasks: Vec>,
restart_task: Option>,
task_context: TaskContext,
+ memory: memory::Memory,
quirks: SessionQuirks,
}
@@ -855,6 +860,7 @@ impl Session {
label,
adapter,
task_context,
+ memory: memory::Memory::new(),
quirks,
};
@@ -1664,6 +1670,11 @@ impl Session {
self.invalidate_command_type::();
self.invalidate_command_type::();
self.invalidate_command_type::();
+ self.invalidate_command_type::();
+ let executor = self.as_running().map(|running| running.executor.clone());
+ if let Some(executor) = executor {
+ self.memory.clear(&executor);
+ }
}
fn invalidate_state(&mut self, key: &RequestSlot) {
@@ -1736,6 +1747,135 @@ impl Session {
&self.modules
}
+ // CodeLLDB returns the size of a pointed-to-memory, which we can use to make the experience of go-to-memory better.
+ pub fn data_access_size(
+ &mut self,
+ frame_id: Option,
+ evaluate_name: &str,
+ cx: &mut Context,
+ ) -> Task