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:
parent
a2f5c47e2d
commit
6673c7cd4c
18 changed files with 1732 additions and 71 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -4393,12 +4393,15 @@ dependencies = [
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"hex",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
|
"notifications",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"parse_int",
|
||||||
"paths",
|
"paths",
|
||||||
"picker",
|
"picker",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
@ -11276,6 +11279,15 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "partial-json-fixer"
|
name = "partial-json-fixer"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
@ -12319,6 +12331,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askpass",
|
"askpass",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"circular-buffer",
|
"circular-buffer",
|
||||||
"client",
|
"client",
|
||||||
|
@ -12364,6 +12377,7 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"shellexpand 2.1.2",
|
"shellexpand 2.1.2",
|
||||||
"shlex",
|
"shlex",
|
||||||
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"snippet",
|
"snippet",
|
||||||
"snippet_provider",
|
"snippet_provider",
|
||||||
|
|
|
@ -507,6 +507,7 @@ ordered-float = "2.1.1"
|
||||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||||
parking_lot = "0.12.1"
|
parking_lot = "0.12.1"
|
||||||
partial-json-fixer = "0.5.3"
|
partial-json-fixer = "0.5.3"
|
||||||
|
parse_int = "0.9"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
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" }
|
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||||
|
|
1
assets/icons/location_edit.svg
Normal file
1
assets/icons/location_edit.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg>
|
After Width: | Height: | Size: 491 B |
|
@ -40,12 +40,15 @@ file_icons.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
notifications.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
parse_int.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem;
|
||||||
use crate::session::DebugSession;
|
use crate::session::DebugSession;
|
||||||
use crate::session::running::RunningState;
|
use crate::session::running::RunningState;
|
||||||
use crate::session::running::breakpoint_list::BreakpointList;
|
use crate::session::running::breakpoint_list::BreakpointList;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
|
ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
|
||||||
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
|
FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
|
||||||
|
@ -1804,6 +1805,7 @@ impl Render for DebugPanel {
|
||||||
.child(breakpoint_list)
|
.child(breakpoint_list)
|
||||||
.child(Divider::vertical())
|
.child(Divider::vertical())
|
||||||
.child(welcome_experience)
|
.child(welcome_experience)
|
||||||
|
.child(Divider::vertical())
|
||||||
} else {
|
} else {
|
||||||
this.items_end()
|
this.items_end()
|
||||||
.child(welcome_experience)
|
.child(welcome_experience)
|
||||||
|
|
|
@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace};
|
||||||
|
|
||||||
use crate::session::running::{
|
use crate::session::running::{
|
||||||
self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console,
|
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,
|
stack_frame_list::StackFrameList, variable_list::VariableList,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem {
|
||||||
Modules,
|
Modules,
|
||||||
LoadedSources,
|
LoadedSources,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
MemoryView,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DebuggerPaneItem {
|
impl DebuggerPaneItem {
|
||||||
|
@ -36,6 +37,7 @@ impl DebuggerPaneItem {
|
||||||
DebuggerPaneItem::Modules,
|
DebuggerPaneItem::Modules,
|
||||||
DebuggerPaneItem::LoadedSources,
|
DebuggerPaneItem::LoadedSources,
|
||||||
DebuggerPaneItem::Terminal,
|
DebuggerPaneItem::Terminal,
|
||||||
|
DebuggerPaneItem::MemoryView,
|
||||||
];
|
];
|
||||||
VARIANTS
|
VARIANTS
|
||||||
}
|
}
|
||||||
|
@ -43,6 +45,9 @@ impl DebuggerPaneItem {
|
||||||
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
|
pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool {
|
||||||
match self {
|
match self {
|
||||||
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
|
DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(),
|
||||||
|
DebuggerPaneItem::MemoryView => capabilities
|
||||||
|
.supports_read_memory_request
|
||||||
|
.unwrap_or_default(),
|
||||||
DebuggerPaneItem::LoadedSources => capabilities
|
DebuggerPaneItem::LoadedSources => capabilities
|
||||||
.supports_loaded_sources_request
|
.supports_loaded_sources_request
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
@ -59,6 +64,7 @@ impl DebuggerPaneItem {
|
||||||
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
|
DebuggerPaneItem::Modules => SharedString::new_static("Modules"),
|
||||||
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
|
DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"),
|
||||||
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
|
DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"),
|
||||||
|
DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) fn tab_tooltip(self) -> SharedString {
|
pub(crate) fn tab_tooltip(self) -> SharedString {
|
||||||
|
@ -80,6 +86,7 @@ impl DebuggerPaneItem {
|
||||||
DebuggerPaneItem::Terminal => {
|
DebuggerPaneItem::Terminal => {
|
||||||
"Provides an interactive terminal session within the debugging environment."
|
"Provides an interactive terminal session within the debugging environment."
|
||||||
}
|
}
|
||||||
|
DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.",
|
||||||
};
|
};
|
||||||
SharedString::new_static(tooltip)
|
SharedString::new_static(tooltip)
|
||||||
}
|
}
|
||||||
|
@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||||
breakpoint_list: &Entity<BreakpointList>,
|
breakpoint_list: &Entity<BreakpointList>,
|
||||||
loaded_sources: &Entity<LoadedSourceList>,
|
loaded_sources: &Entity<LoadedSourceList>,
|
||||||
terminal: &Entity<DebugTerminal>,
|
terminal: &Entity<DebugTerminal>,
|
||||||
|
memory_view: &Entity<MemoryView>,
|
||||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<RunningState>,
|
cx: &mut Context<RunningState>,
|
||||||
|
@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||||
breakpoint_list,
|
breakpoint_list,
|
||||||
loaded_sources,
|
loaded_sources,
|
||||||
terminal,
|
terminal,
|
||||||
|
memory_view,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout(
|
||||||
DebuggerPaneItem::Terminal,
|
DebuggerPaneItem::Terminal,
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
|
DebuggerPaneItem::MemoryView => Box::new(SubView::new(
|
||||||
|
memory_view.focus_handle(cx),
|
||||||
|
memory_view.clone().into(),
|
||||||
|
DebuggerPaneItem::MemoryView,
|
||||||
|
cx,
|
||||||
|
)),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
pub(crate) mod breakpoint_list;
|
pub(crate) mod breakpoint_list;
|
||||||
pub(crate) mod console;
|
pub(crate) mod console;
|
||||||
pub(crate) mod loaded_source_list;
|
pub(crate) mod loaded_source_list;
|
||||||
|
pub(crate) mod memory_view;
|
||||||
pub(crate) mod module_list;
|
pub(crate) mod module_list;
|
||||||
pub mod stack_frame_list;
|
pub mod stack_frame_list;
|
||||||
pub mod variable_list;
|
pub mod variable_list;
|
||||||
|
|
||||||
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ToggleExpandItem,
|
ToggleExpandItem,
|
||||||
new_process_modal::resolve_path,
|
new_process_modal::resolve_path,
|
||||||
persistence::{self, DebuggerPaneItem, SerializedLayout},
|
persistence::{self, DebuggerPaneItem, SerializedLayout},
|
||||||
|
session::running::memory_view::MemoryView,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::DebugPanelItemEvent;
|
use super::DebugPanelItemEvent;
|
||||||
|
@ -81,6 +82,7 @@ pub struct RunningState {
|
||||||
_schedule_serialize: Option<Task<()>>,
|
_schedule_serialize: Option<Task<()>>,
|
||||||
pub(crate) scenario: Option<DebugScenario>,
|
pub(crate) scenario: Option<DebugScenario>,
|
||||||
pub(crate) scenario_context: Option<DebugScenarioContext>,
|
pub(crate) scenario_context: Option<DebugScenarioContext>,
|
||||||
|
memory_view: Entity<MemoryView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunningState {
|
impl RunningState {
|
||||||
|
@ -676,14 +678,36 @@ impl RunningState {
|
||||||
let session_id = session.read(cx).session_id();
|
let session_id = session.read(cx).session_id();
|
||||||
let weak_state = cx.weak_entity();
|
let weak_state = cx.weak_entity();
|
||||||
let stack_frame_list = cx.new(|cx| {
|
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 =
|
let debug_terminal =
|
||||||
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
|
parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
|
||||||
|
let memory_view = cx.new(|cx| {
|
||||||
let variable_list =
|
MemoryView::new(
|
||||||
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
|
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));
|
let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
|
||||||
|
|
||||||
|
@ -795,6 +819,7 @@ impl RunningState {
|
||||||
&breakpoint_list,
|
&breakpoint_list,
|
||||||
&loaded_source_list,
|
&loaded_source_list,
|
||||||
&debug_terminal,
|
&debug_terminal,
|
||||||
|
&memory_view,
|
||||||
&mut pane_close_subscriptions,
|
&mut pane_close_subscriptions,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -823,6 +848,7 @@ impl RunningState {
|
||||||
let active_pane = panes.first_pane();
|
let active_pane = panes.first_pane();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
memory_view,
|
||||||
session,
|
session,
|
||||||
workspace,
|
workspace,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
|
@ -1234,6 +1260,12 @@ impl RunningState {
|
||||||
item_kind,
|
item_kind,
|
||||||
cx,
|
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
|
&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
|
let (variable_list_position, pane) = self
|
||||||
.panes
|
.panes
|
||||||
.panes()
|
.panes()
|
||||||
|
@ -1430,9 +1469,10 @@ impl RunningState {
|
||||||
.map(|view| (view, pane))
|
.map(|view| (view, pane))
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
pane.update(cx, |this, cx| {
|
pane.update(cx, |this, cx| {
|
||||||
this.activate_item(variable_list_position, true, true, window, cx);
|
this.activate_item(variable_list_position, true, true, window, cx);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
902
crates/debugger_ui/src/session/running/memory_view.rs
Normal file
902
crates/debugger_ui/src/session/running/memory_view.rs
Normal 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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::session::running::{RunningState, memory_view::MemoryView};
|
||||||
|
|
||||||
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||||
use dap::{
|
use dap::{
|
||||||
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
|
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
|
||||||
|
@ -7,13 +9,14 @@ use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
|
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
|
||||||
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
|
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 menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||||
use project::debugger::session::{Session, SessionEvent, Watcher};
|
use project::debugger::session::{Session, SessionEvent, Watcher};
|
||||||
use std::{collections::HashMap, ops::Range, sync::Arc};
|
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||||
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
||||||
use util::debug_panic;
|
use util::{debug_panic, maybe};
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
variable_list,
|
variable_list,
|
||||||
|
@ -32,6 +35,8 @@ actions!(
|
||||||
AddWatch,
|
AddWatch,
|
||||||
/// Removes the selected variable from the watch list.
|
/// Removes the selected variable from the watch list.
|
||||||
RemoveWatch,
|
RemoveWatch,
|
||||||
|
/// Jump to variable's memory location.
|
||||||
|
GoToMemory,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -86,30 +91,30 @@ impl EntryPath {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum EntryKind {
|
enum DapEntry {
|
||||||
Watcher(Watcher),
|
Watcher(Watcher),
|
||||||
Variable(dap::Variable),
|
Variable(dap::Variable),
|
||||||
Scope(dap::Scope),
|
Scope(dap::Scope),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntryKind {
|
impl DapEntry {
|
||||||
fn as_watcher(&self) -> Option<&Watcher> {
|
fn as_watcher(&self) -> Option<&Watcher> {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Watcher(watcher) => Some(watcher),
|
DapEntry::Watcher(watcher) => Some(watcher),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Variable(dap) => Some(dap),
|
DapEntry::Variable(dap) => Some(dap),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Scope(dap) => Some(dap),
|
DapEntry::Scope(dap) => Some(dap),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,38 +122,38 @@ impl EntryKind {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Watcher(watcher) => &watcher.expression,
|
DapEntry::Watcher(watcher) => &watcher.expression,
|
||||||
EntryKind::Variable(dap) => &dap.name,
|
DapEntry::Variable(dap) => &dap.name,
|
||||||
EntryKind::Scope(dap) => &dap.name,
|
DapEntry::Scope(dap) => &dap.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
struct ListEntry {
|
struct ListEntry {
|
||||||
dap_kind: EntryKind,
|
entry: DapEntry,
|
||||||
path: EntryPath,
|
path: EntryPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListEntry {
|
impl ListEntry {
|
||||||
fn as_watcher(&self) -> Option<&Watcher> {
|
fn as_watcher(&self) -> Option<&Watcher> {
|
||||||
self.dap_kind.as_watcher()
|
self.entry.as_watcher()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||||
self.dap_kind.as_variable()
|
self.entry.as_variable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_scope(&self) -> Option<&dap::Scope> {
|
fn as_scope(&self) -> Option<&dap::Scope> {
|
||||||
self.dap_kind.as_scope()
|
self.entry.as_scope()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_id(&self) -> ElementId {
|
fn item_id(&self) -> ElementId {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let mut id = match &self.dap_kind {
|
let mut id = match &self.entry {
|
||||||
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
|
||||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
|
||||||
};
|
};
|
||||||
for name in self.path.indices.iter() {
|
for name in self.path.indices.iter() {
|
||||||
_ = write!(id, "-{}", name);
|
_ = write!(id, "-{}", name);
|
||||||
|
@ -158,10 +163,10 @@ impl ListEntry {
|
||||||
|
|
||||||
fn item_value_id(&self) -> ElementId {
|
fn item_value_id(&self) -> ElementId {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let mut id = match &self.dap_kind {
|
let mut id = match &self.entry {
|
||||||
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
DapEntry::Variable(dap) => format!("variable-{}", dap.name),
|
||||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
DapEntry::Scope(dap) => format!("scope-{}", dap.name),
|
||||||
};
|
};
|
||||||
for name in self.path.indices.iter() {
|
for name in self.path.indices.iter() {
|
||||||
_ = write!(id, "-{}", name);
|
_ = write!(id, "-{}", name);
|
||||||
|
@ -188,13 +193,17 @@ pub struct VariableList {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
edited_path: Option<(EntryPath, Entity<Editor>)>,
|
edited_path: Option<(EntryPath, Entity<Editor>)>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
memory_view: Entity<MemoryView>,
|
||||||
|
weak_running: WeakEntity<RunningState>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VariableList {
|
impl VariableList {
|
||||||
pub fn new(
|
pub(crate) fn new(
|
||||||
session: Entity<Session>,
|
session: Entity<Session>,
|
||||||
stack_frame_list: Entity<StackFrameList>,
|
stack_frame_list: Entity<StackFrameList>,
|
||||||
|
memory_view: Entity<MemoryView>,
|
||||||
|
weak_running: WeakEntity<RunningState>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -234,6 +243,8 @@ impl VariableList {
|
||||||
edited_path: None,
|
edited_path: None,
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
entry_states: Default::default(),
|
entry_states: Default::default(),
|
||||||
|
weak_running,
|
||||||
|
memory_view,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,7 +295,7 @@ impl VariableList {
|
||||||
scope.variables_reference,
|
scope.variables_reference,
|
||||||
scope.variables_reference,
|
scope.variables_reference,
|
||||||
EntryPath::for_scope(&scope.name),
|
EntryPath::for_scope(&scope.name),
|
||||||
EntryKind::Scope(scope),
|
DapEntry::Scope(scope),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -298,7 +309,7 @@ impl VariableList {
|
||||||
watcher.variables_reference,
|
watcher.variables_reference,
|
||||||
watcher.variables_reference,
|
watcher.variables_reference,
|
||||||
EntryPath::for_watcher(watcher.expression.clone()),
|
EntryPath::for_watcher(watcher.expression.clone()),
|
||||||
EntryKind::Watcher(watcher.clone()),
|
DapEntry::Watcher(watcher.clone()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
@ -309,9 +320,9 @@ impl VariableList {
|
||||||
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
||||||
{
|
{
|
||||||
match &dap_kind {
|
match &dap_kind {
|
||||||
EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
|
DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
|
||||||
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
||||||
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
||||||
}
|
}
|
||||||
|
|
||||||
let var_state = self
|
let var_state = self
|
||||||
|
@ -336,7 +347,7 @@ impl VariableList {
|
||||||
});
|
});
|
||||||
|
|
||||||
entries.push(ListEntry {
|
entries.push(ListEntry {
|
||||||
dap_kind,
|
entry: dap_kind,
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -349,7 +360,7 @@ impl VariableList {
|
||||||
variables_reference,
|
variables_reference,
|
||||||
child.variables_reference,
|
child.variables_reference,
|
||||||
path.with_child(child.name.clone().into()),
|
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> {
|
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
|
||||||
self.entries
|
self.entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| match &entry.dap_kind {
|
.filter_map(|entry| match &entry.entry {
|
||||||
EntryKind::Variable(dap) => Some(dap.clone()),
|
DapEntry::Variable(dap) => Some(dap.clone()),
|
||||||
EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
|
DapEntry::Scope(_) | DapEntry::Watcher { .. } => None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -400,12 +411,12 @@ impl VariableList {
|
||||||
.get(ix)
|
.get(ix)
|
||||||
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
||||||
|
|
||||||
match &entry.dap_kind {
|
match &entry.entry {
|
||||||
EntryKind::Watcher { .. } => {
|
DapEntry::Watcher { .. } => {
|
||||||
Some(self.render_watcher(entry, *state, window, cx))
|
Some(self.render_watcher(entry, *state, window, cx))
|
||||||
}
|
}
|
||||||
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
||||||
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
|
DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.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(
|
fn deploy_list_entry_context_menu(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: ListEntry,
|
entry: ListEntry,
|
||||||
|
@ -584,6 +640,7 @@ impl VariableList {
|
||||||
menu.action("Edit Value", EditVariable.boxed_clone())
|
menu.action("Edit Value", EditVariable.boxed_clone())
|
||||||
})
|
})
|
||||||
.action("Watch Variable", AddWatch.boxed_clone())
|
.action("Watch Variable", AddWatch.boxed_clone())
|
||||||
|
.action("Go To Memory", GoToMemory.boxed_clone())
|
||||||
})
|
})
|
||||||
.when(entry.as_watcher().is_some(), |menu| {
|
.when(entry.as_watcher().is_some(), |menu| {
|
||||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||||
|
@ -628,10 +685,10 @@ impl VariableList {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let variable_name = match &entry.dap_kind {
|
let variable_name = match &entry.entry {
|
||||||
EntryKind::Variable(dap) => dap.name.clone(),
|
DapEntry::Variable(dap) => dap.name.clone(),
|
||||||
EntryKind::Watcher(watcher) => watcher.expression.to_string(),
|
DapEntry::Watcher(watcher) => watcher.expression.to_string(),
|
||||||
EntryKind::Scope(_) => return,
|
DapEntry::Scope(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
|
cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
|
||||||
|
@ -651,10 +708,10 @@ impl VariableList {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let variable_value = match &entry.dap_kind {
|
let variable_value = match &entry.entry {
|
||||||
EntryKind::Variable(dap) => dap.value.clone(),
|
DapEntry::Variable(dap) => dap.value.clone(),
|
||||||
EntryKind::Watcher(watcher) => watcher.value.to_string(),
|
DapEntry::Watcher(watcher) => watcher.value.to_string(),
|
||||||
EntryKind::Scope(_) => return,
|
DapEntry::Scope(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
|
cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
|
||||||
|
@ -669,10 +726,10 @@ impl VariableList {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let variable_value = match &entry.dap_kind {
|
let variable_value = match &entry.entry {
|
||||||
EntryKind::Watcher(watcher) => watcher.value.to_string(),
|
DapEntry::Watcher(watcher) => watcher.value.to_string(),
|
||||||
EntryKind::Variable(variable) => variable.value.clone(),
|
DapEntry::Variable(variable) => variable.value.clone(),
|
||||||
EntryKind::Scope(_) => return,
|
DapEntry::Scope(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor = Self::create_variable_editor(&variable_value, window, cx);
|
let editor = Self::create_variable_editor(&variable_value, window, cx);
|
||||||
|
@ -753,7 +810,7 @@ impl VariableList {
|
||||||
"{}{} {}{}",
|
"{}{} {}{}",
|
||||||
INDENT.repeat(state.depth - 1),
|
INDENT.repeat(state.depth - 1),
|
||||||
if state.is_expanded { "v" } else { ">" },
|
if state.is_expanded { "v" } else { ">" },
|
||||||
entry.dap_kind.name(),
|
entry.entry.name(),
|
||||||
if self.selection.as_ref() == Some(&entry.path) {
|
if self.selection.as_ref() == Some(&entry.path) {
|
||||||
" <=== selected"
|
" <=== selected"
|
||||||
} else {
|
} else {
|
||||||
|
@ -770,8 +827,8 @@ impl VariableList {
|
||||||
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
|
pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
|
||||||
self.entries
|
self.entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| match &entry.dap_kind {
|
.filter_map(|entry| match &entry.entry {
|
||||||
EntryKind::Scope(scope) => Some(scope),
|
DapEntry::Scope(scope) => Some(scope),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -785,10 +842,10 @@ impl VariableList {
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
|
|
||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
match &entry.dap_kind {
|
match &entry.entry {
|
||||||
EntryKind::Watcher { .. } => continue,
|
DapEntry::Watcher { .. } => continue,
|
||||||
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
||||||
EntryKind::Scope(scope) => {
|
DapEntry::Scope(scope) => {
|
||||||
if scopes.len() > 0 {
|
if scopes.len() > 0 {
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
@ -806,8 +863,8 @@ impl VariableList {
|
||||||
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
|
pub(crate) fn variables(&self) -> Vec<dap::Variable> {
|
||||||
self.entries
|
self.entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| match &entry.dap_kind {
|
.filter_map(|entry| match &entry.entry {
|
||||||
EntryKind::Variable(variable) => Some(variable),
|
DapEntry::Variable(variable) => Some(variable),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -1358,6 +1415,7 @@ impl Render for VariableList {
|
||||||
.on_action(cx.listener(Self::edit_variable))
|
.on_action(cx.listener(Self::edit_variable))
|
||||||
.on_action(cx.listener(Self::add_watcher))
|
.on_action(cx.listener(Self::add_watcher))
|
||||||
.on_action(cx.listener(Self::remove_watcher))
|
.on_action(cx.listener(Self::remove_watcher))
|
||||||
|
.on_action(cx.listener(Self::jump_to_variable_memory))
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
"variable-list",
|
"variable-list",
|
||||||
|
|
|
@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||||
});
|
});
|
||||||
|
|
||||||
running_state.update_in(cx, |this, window, cx| {
|
running_state.update_in(cx, |this, window, cx| {
|
||||||
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
|
|
||||||
this.activate_item(DebuggerPaneItem::Modules, window, cx);
|
this.activate_item(DebuggerPaneItem::Modules, window, cx);
|
||||||
cx.refresh_windows();
|
cx.refresh_windows();
|
||||||
});
|
});
|
||||||
|
|
|
@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized {
|
||||||
/// Apply the given style when the given data type is dragged over this element
|
/// Apply the given style when the given data type is dragged over this element
|
||||||
fn drag_over<S: 'static>(
|
fn drag_over<S: 'static>(
|
||||||
mut self,
|
mut self,
|
||||||
f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement,
|
f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.interactivity().drag_over_styles.push((
|
self.interactivity().drag_over_styles.push((
|
||||||
TypeId::of::<S>(),
|
TypeId::of::<S>(),
|
||||||
|
|
|
@ -163,6 +163,7 @@ pub enum IconName {
|
||||||
ListTree,
|
ListTree,
|
||||||
ListX,
|
ListX,
|
||||||
LoadCircle,
|
LoadCircle,
|
||||||
|
LocationEdit,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
LspDebug,
|
LspDebug,
|
||||||
LspRestart,
|
LspRestart,
|
||||||
|
|
|
@ -31,6 +31,7 @@ aho-corasick.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
askpass.workspace = true
|
askpass.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
circular-buffer.workspace = true
|
circular-buffer.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
@ -72,6 +73,7 @@ settings.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
shellexpand.workspace = true
|
shellexpand.workspace = true
|
||||||
shlex.workspace = true
|
shlex.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
snippet.workspace = true
|
snippet.workspace = true
|
||||||
snippet_provider.workspace = true
|
snippet_provider.workspace = true
|
||||||
|
|
|
@ -15,7 +15,9 @@ pub mod breakpoint_store;
|
||||||
pub mod dap_command;
|
pub mod dap_command;
|
||||||
pub mod dap_store;
|
pub mod dap_store;
|
||||||
pub mod locators;
|
pub mod locators;
|
||||||
|
mod memory;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
#[cfg(any(feature = "test-support", test))]
|
#[cfg(any(feature = "test-support", test))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
pub use memory::MemoryCell;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context as _, Ok, Result};
|
use anyhow::{Context as _, Ok, Result};
|
||||||
|
use base64::Engine;
|
||||||
use dap::{
|
use dap::{
|
||||||
Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments,
|
Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments,
|
||||||
InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint,
|
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<u64>,
|
||||||
|
pub(crate) count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) struct ReadMemoryResponse {
|
||||||
|
pub(super) address: Arc<str>,
|
||||||
|
pub(super) unreadable_bytes: Option<u64>,
|
||||||
|
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) -> <Self::DapRequest as dap::requests::Request>::Arguments {
|
||||||
|
dap::ReadMemoryArguments {
|
||||||
|
memory_reference: self.memory_reference.clone(),
|
||||||
|
offset: self.offset,
|
||||||
|
count: self.count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_dap(
|
||||||
|
&self,
|
||||||
|
message: <Self::DapRequest as dap::requests::Request>::Response,
|
||||||
|
) -> Result<Self::Response> {
|
||||||
|
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) -> <Self::DapRequest as dap::requests::Request>::Arguments {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_dap(
|
||||||
|
&self,
|
||||||
|
message: <Self::DapRequest as dap::requests::Request>::Response,
|
||||||
|
) -> Result<Self::Response> {
|
||||||
|
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) -> <Self::DapRequest as dap::requests::Request>::Arguments {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from_dap(
|
||||||
|
&self,
|
||||||
|
message: <Self::DapRequest as dap::requests::Request>::Response,
|
||||||
|
) -> Result<Self::Response> {
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
384
crates/project/src/debugger/memory.rs
Normal file
384
crates/project/src/debugger/memory.rs
Normal file
|
@ -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<MappedPageContents>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageContents {
|
||||||
|
#[cfg(test)]
|
||||||
|
fn mapped(contents: Vec<u8>) -> 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<PageAddress>,
|
||||||
|
) -> impl Iterator<Item = PageAddress> {
|
||||||
|
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<PageAddress, PageContents>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<u8>);
|
||||||
|
|
||||||
|
impl Memory {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pages: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn memory_range_to_page_range(
|
||||||
|
range: RangeInclusive<MemoryAddress>,
|
||||||
|
) -> RangeInclusive<PageAddress> {
|
||||||
|
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<MemoryPageBuilder> {
|
||||||
|
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<MemoryAddress>) -> MemoryIterator {
|
||||||
|
let pages = Self::memory_range_to_page_range(range.clone());
|
||||||
|
let pages = self
|
||||||
|
.pages
|
||||||
|
.range(pages)
|
||||||
|
.map(|(address, page)| (*address, page.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
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<UnknownMemory> {
|
||||||
|
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<MappedPageContents>) -> Box<dyn Iterator<Item = MemoryCell>> {
|
||||||
|
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<dyn Iterator<Item = MemoryCell>>
|
||||||
|
}
|
||||||
|
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<dyn Iterator<Item = MemoryCell>>)>,
|
||||||
|
pages: std::vec::IntoIter<(PageAddress, PageContents)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemoryIterator {
|
||||||
|
fn new(
|
||||||
|
range: RangeInclusive<MemoryAddress>,
|
||||||
|
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<Self::Item> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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::<Vec<_>>();
|
||||||
|
let expected = (20..=30)
|
||||||
|
.map(|val| MemoryCell(Some(val)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(actual.len(), expected.len());
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
use crate::debugger::breakpoint_store::BreakpointSessionState;
|
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::{
|
use super::breakpoint_store::{
|
||||||
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
|
BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
|
||||||
|
@ -13,6 +15,7 @@ use super::dap_command::{
|
||||||
};
|
};
|
||||||
use super::dap_store::DapStore;
|
use super::dap_store::DapStore;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
|
use base64::Engine;
|
||||||
use collections::{HashMap, HashSet, IndexMap};
|
use collections::{HashMap, HashSet, IndexMap};
|
||||||
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
|
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
|
||||||
use dap::messages::Response;
|
use dap::messages::Response;
|
||||||
|
@ -26,7 +29,7 @@ use dap::{
|
||||||
use dap::{
|
use dap::{
|
||||||
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
|
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
|
||||||
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
|
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
|
||||||
StartDebuggingRequestArgumentsRequest, VariablePresentationHint,
|
StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
|
||||||
};
|
};
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
|
@ -42,6 +45,7 @@ use serde_json::Value;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
use std::u64;
|
use std::u64;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
@ -52,7 +56,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use task::TaskContext;
|
use task::TaskContext;
|
||||||
use text::{PointUtf16, ToPointUtf16};
|
use text::{PointUtf16, ToPointUtf16};
|
||||||
use util::ResultExt;
|
use util::{ResultExt, maybe};
|
||||||
use worktree::Worktree;
|
use worktree::Worktree;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
|
||||||
|
@ -685,6 +689,7 @@ pub struct Session {
|
||||||
background_tasks: Vec<Task<()>>,
|
background_tasks: Vec<Task<()>>,
|
||||||
restart_task: Option<Task<()>>,
|
restart_task: Option<Task<()>>,
|
||||||
task_context: TaskContext,
|
task_context: TaskContext,
|
||||||
|
memory: memory::Memory,
|
||||||
quirks: SessionQuirks,
|
quirks: SessionQuirks,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -855,6 +860,7 @@ impl Session {
|
||||||
label,
|
label,
|
||||||
adapter,
|
adapter,
|
||||||
task_context,
|
task_context,
|
||||||
|
memory: memory::Memory::new(),
|
||||||
quirks,
|
quirks,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1664,6 +1670,11 @@ impl Session {
|
||||||
self.invalidate_command_type::<ModulesCommand>();
|
self.invalidate_command_type::<ModulesCommand>();
|
||||||
self.invalidate_command_type::<LoadedSourcesCommand>();
|
self.invalidate_command_type::<LoadedSourcesCommand>();
|
||||||
self.invalidate_command_type::<ThreadsCommand>();
|
self.invalidate_command_type::<ThreadsCommand>();
|
||||||
|
self.invalidate_command_type::<ReadMemory>();
|
||||||
|
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) {
|
fn invalidate_state(&mut self, key: &RequestSlot) {
|
||||||
|
@ -1736,6 +1747,135 @@ impl Session {
|
||||||
&self.modules
|
&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<u64>,
|
||||||
|
evaluate_name: &str,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Option<u64>> {
|
||||||
|
let request = self.request(
|
||||||
|
EvaluateCommand {
|
||||||
|
expression: format!("?${{sizeof({evaluate_name})}}"),
|
||||||
|
frame_id,
|
||||||
|
|
||||||
|
context: Some(EvaluateArgumentsContext::Repl),
|
||||||
|
source: None,
|
||||||
|
},
|
||||||
|
|_, response, _| response.ok(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let result = request.await?;
|
||||||
|
result.result.parse().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn memory_reference_of_expr(
|
||||||
|
&mut self,
|
||||||
|
frame_id: Option<u64>,
|
||||||
|
expression: String,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Option<String>> {
|
||||||
|
let request = self.request(
|
||||||
|
EvaluateCommand {
|
||||||
|
expression,
|
||||||
|
frame_id,
|
||||||
|
|
||||||
|
context: Some(EvaluateArgumentsContext::Repl),
|
||||||
|
source: None,
|
||||||
|
},
|
||||||
|
|_, response, _| response.ok(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let result = request.await?;
|
||||||
|
result.memory_reference
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_memory(&mut self, address: u64, data: &[u8], cx: &mut Context<Self>) {
|
||||||
|
let data = base64::engine::general_purpose::STANDARD.encode(data);
|
||||||
|
self.request(
|
||||||
|
WriteMemoryArguments {
|
||||||
|
memory_reference: address.to_string(),
|
||||||
|
data,
|
||||||
|
allow_partial: None,
|
||||||
|
offset: None,
|
||||||
|
},
|
||||||
|
|this, response, cx| {
|
||||||
|
this.memory.clear(cx.background_executor());
|
||||||
|
this.invalidate_command_type::<ReadMemory>();
|
||||||
|
this.invalidate_command_type::<VariablesCommand>();
|
||||||
|
cx.emit(SessionEvent::Variables);
|
||||||
|
response.ok()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
pub fn read_memory(
|
||||||
|
&mut self,
|
||||||
|
range: RangeInclusive<u64>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> MemoryIterator {
|
||||||
|
// This function is a bit more involved when it comes to fetching data.
|
||||||
|
// Since we attempt to read memory in pages, we need to account for some parts
|
||||||
|
// of memory being unreadable. Therefore, we start off by fetching a page per request.
|
||||||
|
// In case that fails, we try to re-fetch smaller regions until we have the full range.
|
||||||
|
let page_range = Memory::memory_range_to_page_range(range.clone());
|
||||||
|
for page_address in PageAddress::iter_range(page_range) {
|
||||||
|
self.read_single_page_memory(page_address, cx);
|
||||||
|
}
|
||||||
|
self.memory.memory_range(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_single_page_memory(&mut self, page_start: PageAddress, cx: &mut Context<Self>) {
|
||||||
|
_ = maybe!({
|
||||||
|
let builder = self.memory.build_page(page_start)?;
|
||||||
|
|
||||||
|
self.memory_read_fetch_page_recursive(builder, cx);
|
||||||
|
Some(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fn memory_read_fetch_page_recursive(
|
||||||
|
&mut self,
|
||||||
|
mut builder: MemoryPageBuilder,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(next_request) = builder.next_request() else {
|
||||||
|
// We're done fetching. Let's grab the page and insert it into our memory store.
|
||||||
|
let (address, contents) = builder.build();
|
||||||
|
self.memory.insert_page(address, contents);
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let size = next_request.size;
|
||||||
|
self.fetch(
|
||||||
|
ReadMemory {
|
||||||
|
memory_reference: format!("0x{:X}", next_request.address),
|
||||||
|
offset: Some(0),
|
||||||
|
count: next_request.size,
|
||||||
|
},
|
||||||
|
move |this, memory, cx| {
|
||||||
|
if let Ok(memory) = memory {
|
||||||
|
builder.known(memory.content);
|
||||||
|
if let Some(unknown) = memory.unreadable_bytes {
|
||||||
|
builder.unknown(unknown);
|
||||||
|
}
|
||||||
|
// This is the recursive bit: if we're not yet done with
|
||||||
|
// the whole page, we'll kick off a new request with smaller range.
|
||||||
|
// Note that this function is recursive only conceptually;
|
||||||
|
// since it kicks off a new request with callback, we don't need to worry about stack overflow.
|
||||||
|
this.memory_read_fetch_page_recursive(builder, cx);
|
||||||
|
} else {
|
||||||
|
builder.unknown(size);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ignore_breakpoints(&self) -> bool {
|
pub fn ignore_breakpoints(&self) -> bool {
|
||||||
self.ignore_breakpoints
|
self.ignore_breakpoints
|
||||||
}
|
}
|
||||||
|
@ -2378,6 +2518,8 @@ impl Session {
|
||||||
move |this, response, cx| {
|
move |this, response, cx| {
|
||||||
let response = response.log_err()?;
|
let response = response.log_err()?;
|
||||||
this.invalidate_command_type::<VariablesCommand>();
|
this.invalidate_command_type::<VariablesCommand>();
|
||||||
|
this.invalidate_command_type::<ReadMemory>();
|
||||||
|
this.memory.clear(cx.background_executor());
|
||||||
this.refresh_watchers(stack_frame_id, cx);
|
this.refresh_watchers(stack_frame_id, cx);
|
||||||
cx.emit(SessionEvent::Variables);
|
cx.emit(SessionEvent::Variables);
|
||||||
Some(response)
|
Some(response)
|
||||||
|
@ -2417,6 +2559,8 @@ impl Session {
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let response = request.await;
|
let response = request.await;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
this.memory.clear(cx.background_executor());
|
||||||
|
this.invalidate_command_type::<ReadMemory>();
|
||||||
match response {
|
match response {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let event = dap::OutputEvent {
|
let event = dap::OutputEvent {
|
||||||
|
|
|
@ -668,7 +668,7 @@ impl ContextMenu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if let Some(ix) = self.selected_index {
|
if let Some(ix) = self.selected_index {
|
||||||
let next_index = ix + 1;
|
let next_index = ix + 1;
|
||||||
if self.items.len() <= next_index {
|
if self.items.len() <= next_index {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue