Skip to repository content935 lines · 35.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:38:30.722Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
memory_view.rs
1use std::{
2 cell::{LazyCell, RefCell, RefMut},
3 fmt::Write,
4 ops::RangeInclusive,
5 rc::Rc,
6 sync::{Arc, LazyLock},
7 time::Duration,
8};
9
10use editor::{Editor, EditorElement, EditorStyle};
11use gpui::{
12 Action, Along, AppContext, Axis, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle,
13 Focusable, ListHorizontalSizingBehavior, MouseButton, Point, ScrollStrategy, ScrollWheelEvent,
14 Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions,
15 anchored, deferred, uniform_list,
16};
17use notifications::status_toast::StatusToast;
18use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session};
19use settings::Settings;
20use theme_settings::ThemeSettings;
21use ui::{
22 ContextMenu, Divider, DropdownMenu, PopoverMenuHandle, ScrollableHandle,
23 StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*,
24};
25use workspace::Workspace;
26
27use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList};
28
29actions!(debugger, [GoToSelectedAddress]);
30
31pub(crate) struct MemoryView {
32 workspace: WeakEntity<Workspace>,
33 stack_frame_list: WeakEntity<StackFrameList>,
34 focus_handle: FocusHandle,
35 view_state_handle: ViewStateHandle,
36 query_editor: Entity<Editor>,
37 session: Entity<Session>,
38 width_picker_handle: PopoverMenuHandle<ContextMenu>,
39 is_writing_memory: bool,
40 open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
41}
42
43impl Focusable for MemoryView {
44 fn focus_handle(&self, _: &ui::App) -> FocusHandle {
45 self.focus_handle.clone()
46 }
47}
48#[derive(Clone, Debug)]
49struct Drag {
50 start_address: u64,
51 end_address: u64,
52}
53
54impl Drag {
55 fn contains(&self, address: u64) -> bool {
56 let range = self.memory_range();
57 range.contains(&address)
58 }
59
60 fn memory_range(&self) -> RangeInclusive<u64> {
61 if self.start_address < self.end_address {
62 self.start_address..=self.end_address
63 } else {
64 self.end_address..=self.start_address
65 }
66 }
67}
68#[derive(Clone, Debug)]
69enum SelectedMemoryRange {
70 DragUnderway(Drag),
71 DragComplete(Drag),
72}
73
74impl SelectedMemoryRange {
75 fn contains(&self, address: u64) -> bool {
76 match self {
77 SelectedMemoryRange::DragUnderway(drag) => drag.contains(address),
78 SelectedMemoryRange::DragComplete(drag) => drag.contains(address),
79 }
80 }
81 fn is_dragging(&self) -> bool {
82 matches!(self, SelectedMemoryRange::DragUnderway(_))
83 }
84 fn drag(&self) -> &Drag {
85 match self {
86 SelectedMemoryRange::DragUnderway(drag) => drag,
87 SelectedMemoryRange::DragComplete(drag) => drag,
88 }
89 }
90}
91
92#[derive(Clone)]
93struct ViewStateHandle(Rc<RefCell<ViewState>>);
94
95impl ViewStateHandle {
96 fn new(base_row: u64, line_width: ViewWidth) -> Self {
97 Self(Rc::new(RefCell::new(ViewState::new(base_row, line_width))))
98 }
99}
100
101#[derive(Clone)]
102struct ViewState {
103 /// Uppermost row index
104 base_row: u64,
105 /// How many cells per row do we have?
106 line_width: ViewWidth,
107 scroll_handle: UniformListScrollHandle,
108 selection: Option<SelectedMemoryRange>,
109}
110
111impl ViewState {
112 fn new(base_row: u64, line_width: ViewWidth) -> Self {
113 Self {
114 scroll_handle: UniformListScrollHandle::new(),
115 base_row,
116 line_width,
117 selection: None,
118 }
119 }
120 fn row_count(&self) -> u64 {
121 // 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
122 // middle ground for data size.
123 const PAGE_SIZE: u64 = 4096;
124 PAGE_SIZE / self.line_width.width as u64
125 }
126 fn schedule_scroll_down(&mut self) {
127 self.base_row = self.base_row.saturating_add(1)
128 }
129 fn schedule_scroll_up(&mut self) {
130 self.base_row = self.base_row.saturating_sub(1);
131 }
132
133 fn set_offset(&mut self, point: Point<Pixels>) {
134 if point.y >= -Pixels::ZERO {
135 self.schedule_scroll_up();
136 } else if point.y <= -self.scroll_handle.max_offset().y {
137 self.schedule_scroll_down();
138 }
139 self.scroll_handle.set_offset(point);
140 }
141}
142
143impl ScrollableHandle for ViewStateHandle {
144 fn max_offset(&self) -> gpui::Point<Pixels> {
145 self.0.borrow().scroll_handle.max_offset()
146 }
147
148 fn set_offset(&self, point: Point<Pixels>) {
149 self.0.borrow_mut().set_offset(point);
150 }
151
152 fn offset(&self) -> Point<Pixels> {
153 self.0.borrow().scroll_handle.offset()
154 }
155
156 fn viewport(&self) -> gpui::Bounds<Pixels> {
157 self.0.borrow().scroll_handle.viewport()
158 }
159}
160
161static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> =
162 LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}"))));
163static UNKNOWN_BYTE: SharedString = SharedString::new_static("??");
164
165impl MemoryView {
166 pub(crate) fn new(
167 session: Entity<Session>,
168 workspace: WeakEntity<Workspace>,
169 stack_frame_list: WeakEntity<StackFrameList>,
170 window: &mut Window,
171 cx: &mut Context<Self>,
172 ) -> Self {
173 let view_state_handle = ViewStateHandle::new(0, WIDTHS[4].clone());
174
175 let query_editor = cx.new(|cx| Editor::single_line(window, cx));
176
177 let mut this = Self {
178 workspace,
179 stack_frame_list,
180 focus_handle: cx.focus_handle(),
181 view_state_handle,
182 query_editor,
183 session,
184 width_picker_handle: Default::default(),
185 is_writing_memory: true,
186 open_context_menu: None,
187 };
188 this.change_query_bar_mode(false, window, cx);
189 cx.on_focus_out(&this.focus_handle, window, |this, _, window, cx| {
190 this.change_query_bar_mode(false, window, cx);
191 cx.notify();
192 })
193 .detach();
194 this
195 }
196
197 fn view_state(&self) -> RefMut<'_, ViewState> {
198 self.view_state_handle.0.borrow_mut()
199 }
200
201 fn render_memory(&self, cx: &mut Context<Self>) -> UniformList {
202 let weak = cx.weak_entity();
203 let session = self.session.clone();
204 let view_state = self.view_state_handle.0.borrow().clone();
205 uniform_list(
206 "debugger-memory-view",
207 view_state.row_count() as usize,
208 move |range, _, cx| {
209 let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize);
210 let memory_start =
211 (view_state.base_row + range.start as u64) * view_state.line_width.width as u64;
212 let memory_end = (view_state.base_row + range.end as u64)
213 * view_state.line_width.width as u64
214 - 1;
215 let mut memory = session.update(cx, |this, cx| {
216 this.read_memory(memory_start..=memory_end, cx)
217 });
218 let mut rows = Vec::with_capacity(range.end - range.start);
219 for ix in range {
220 line_buffer.extend((&mut memory).take(view_state.line_width.width as usize));
221 rows.push(render_single_memory_view_line(
222 &line_buffer,
223 ix as u64,
224 weak.clone(),
225 cx,
226 ));
227 line_buffer.clear();
228 }
229 rows
230 },
231 )
232 .track_scroll(&view_state.scroll_handle)
233 .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
234 .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| {
235 let mut view_state = this.view_state();
236 let delta = evt.delta.pixel_delta(window.line_height());
237 let current_offset = view_state.scroll_handle.offset();
238 view_state
239 .set_offset(current_offset.apply_along(Axis::Vertical, |offset| offset + delta.y));
240 }))
241 }
242 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
243 EditorElement::new(
244 &self.query_editor,
245 Self::editor_style(&self.query_editor, cx),
246 )
247 }
248 pub(super) fn go_to_memory_reference(
249 &mut self,
250 memory_reference: &str,
251 evaluate_name: Option<&str>,
252 stack_frame_id: Option<u64>,
253 cx: &mut Context<Self>,
254 ) {
255 use parse_int::parse;
256 let Ok(as_address) = parse::<u64>(memory_reference) else {
257 return;
258 };
259 let access_size = evaluate_name
260 .map(|typ| {
261 self.session.update(cx, |this, cx| {
262 this.data_access_size(stack_frame_id, typ, cx)
263 })
264 })
265 .unwrap_or_else(|| Task::ready(None));
266 cx.spawn(async move |this, cx| {
267 let access_size = access_size.await.unwrap_or(1);
268 this.update(cx, |this, cx| {
269 this.view_state().selection = Some(SelectedMemoryRange::DragComplete(Drag {
270 start_address: as_address,
271 end_address: as_address + access_size - 1,
272 }));
273 this.jump_to_address(as_address, cx);
274 })
275 .ok();
276 })
277 .detach();
278 }
279
280 fn handle_memory_drag(&mut self, evt: &DragMoveEvent<Drag>) {
281 let mut view_state = self.view_state();
282 if !view_state
283 .selection
284 .as_ref()
285 .is_some_and(|selection| selection.is_dragging())
286 {
287 return;
288 }
289 let row_count = view_state.row_count();
290 debug_assert!(row_count > 1);
291 let scroll_handle = &view_state.scroll_handle;
292 let viewport = scroll_handle.viewport();
293
294 if viewport.bottom() < evt.event.position.y {
295 view_state.schedule_scroll_down();
296 } else if viewport.top() > evt.event.position.y {
297 view_state.schedule_scroll_up();
298 }
299 }
300
301 fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
302 let is_read_only = editor.read(cx).read_only(cx);
303 let settings = ThemeSettings::get_global(cx);
304 let theme = cx.theme();
305 let text_style = TextStyle {
306 color: if is_read_only {
307 theme.colors().text_muted
308 } else {
309 theme.colors().text
310 },
311 font_family: settings.buffer_font.family.clone(),
312 font_features: settings.buffer_font.features.clone(),
313 font_size: TextSize::Small.rems(cx).into(),
314 font_weight: settings.buffer_font.weight,
315
316 ..Default::default()
317 };
318 EditorStyle {
319 background: theme.colors().editor_background,
320 local_player: theme.players().local(),
321 text: text_style,
322 ..Default::default()
323 }
324 }
325
326 fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu {
327 let weak = cx.weak_entity();
328 let selected_width = self.view_state().line_width.clone();
329 DropdownMenu::new(
330 "memory-view-width-picker",
331 selected_width.label.clone(),
332 ContextMenu::build(window, cx, |mut this, window, cx| {
333 for width in &WIDTHS {
334 let weak = weak.clone();
335 let width = width.clone();
336 this = this.entry(width.label.clone(), None, move |_, cx| {
337 _ = weak.update(cx, |this, _| {
338 let mut view_state = this.view_state();
339 // Convert base ix between 2 line widths to keep the shown memory address roughly the same.
340 // All widths are powers of 2, so the conversion should be lossless.
341 match view_state.line_width.width.cmp(&width.width) {
342 std::cmp::Ordering::Less => {
343 // We're converting up.
344 let shift = width.width.trailing_zeros()
345 - view_state.line_width.width.trailing_zeros();
346 view_state.base_row >>= shift;
347 }
348 std::cmp::Ordering::Greater => {
349 // We're converting down.
350 let shift = view_state.line_width.width.trailing_zeros()
351 - width.width.trailing_zeros();
352 view_state.base_row <<= shift;
353 }
354 _ => {}
355 }
356 view_state.line_width = width.clone();
357 });
358 });
359 }
360 if let Some(ix) = WIDTHS
361 .iter()
362 .position(|width| width.width == selected_width.width)
363 {
364 for _ in 0..=ix {
365 this.select_next(&Default::default(), window, cx);
366 }
367 }
368 this
369 }),
370 )
371 .style(ui::DropdownStyle::Outlined)
372 .handle(self.width_picker_handle.clone())
373 .attach(gpui::Anchor::BottomLeft)
374 }
375
376 fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) {
377 let mut view_state = self.view_state();
378 view_state.base_row = view_state
379 .base_row
380 .overflowing_add(view_state.row_count())
381 .0;
382 cx.notify();
383 }
384 fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) {
385 let mut view_state = self.view_state();
386 view_state.base_row = view_state
387 .base_row
388 .overflowing_sub(view_state.row_count())
389 .0;
390 cx.notify();
391 }
392
393 fn change_query_bar_mode(
394 &mut self,
395 is_writing_memory: bool,
396 window: &mut Window,
397 cx: &mut Context<Self>,
398 ) {
399 if is_writing_memory == self.is_writing_memory {
400 return;
401 }
402 if !self.is_writing_memory {
403 self.query_editor.update(cx, |this, cx| {
404 this.clear(window, cx);
405 this.set_placeholder_text("Write to Selected Memory Range", window, cx);
406 });
407 self.is_writing_memory = true;
408 self.query_editor.focus_handle(cx).focus(window, cx);
409 } else {
410 self.query_editor.update(cx, |this, cx| {
411 this.clear(window, cx);
412 this.set_placeholder_text("Go to Memory Address / Expression", window, cx);
413 });
414 self.is_writing_memory = false;
415 }
416 }
417
418 fn toggle_data_breakpoint(
419 &mut self,
420 _: &crate::ToggleDataBreakpoint,
421 _: &mut Window,
422 cx: &mut Context<Self>,
423 ) {
424 let Some(SelectedMemoryRange::DragComplete(selection)) =
425 self.view_state().selection.clone()
426 else {
427 return;
428 };
429 let range = selection.memory_range();
430 let context = Arc::new(DataBreakpointContext::Address {
431 address: range.start().to_string(),
432 bytes: Some(*range.end() - *range.start()),
433 });
434
435 self.session.update(cx, |this, cx| {
436 let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
437 cx.spawn(async move |this, cx| {
438 if let Some(info) = data_breakpoint_info.await {
439 let Some(data_id) = info.data_id else {
440 return;
441 };
442 _ = this.update(cx, |this, cx| {
443 this.create_data_breakpoint(
444 context,
445 data_id.clone(),
446 dap::DataBreakpoint {
447 data_id,
448 access_type: None,
449 condition: None,
450 hit_condition: None,
451 },
452 cx,
453 );
454 });
455 }
456 })
457 .detach();
458 })
459 }
460
461 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
462 let selection = self.view_state().selection.clone();
463 if let Some(SelectedMemoryRange::DragComplete(drag)) = selection {
464 // Go into memory writing mode.
465 if !self.is_writing_memory {
466 let should_return = self.session.update(cx, |session, cx| {
467 if !session
468 .capabilities()
469 .supports_write_memory_request
470 .unwrap_or_default()
471 {
472 let adapter_name = session.adapter();
473 // We cannot write memory with this adapter.
474 _ = self.workspace.update(cx, |this, cx| {
475 this.toggle_status_toast(
476 StatusToast::new(format!(
477 "Debug Adapter `{adapter_name}` does not support writing to memory"
478 ), cx, |this, cx| {
479 cx.spawn(async move |this, cx| {
480 cx.background_executor().timer(Duration::from_secs(2)).await;
481 _ = this.update(cx, |_, cx| {
482 cx.emit(DismissEvent)
483 });
484 }).detach();
485 this.icon(Icon::new(IconName::XCircle).size(IconSize::Small).color(Color::Error))
486 }),
487 cx,
488 );
489 });
490 true
491 } else {
492 false
493 }
494 });
495 if should_return {
496 return;
497 }
498
499 self.change_query_bar_mode(true, window, cx);
500 } else if self.query_editor.focus_handle(cx).is_focused(window) {
501 let mut text = self.query_editor.read(cx).text(cx);
502 if text.chars().any(|c| !c.is_ascii_hexdigit()) {
503 // Interpret this text as a string and oh-so-conveniently convert it.
504 text = text.bytes().map(|byte| format!("{:02x}", byte)).collect();
505 }
506 self.session.update(cx, |this, cx| {
507 let range = drag.memory_range();
508
509 if let Ok(as_hex) = hex::decode(text) {
510 this.write_memory(*range.start(), &as_hex, cx);
511 }
512 });
513 self.change_query_bar_mode(false, window, cx);
514 }
515
516 cx.notify();
517 return;
518 }
519 // Just change the currently viewed address.
520 if !self.query_editor.focus_handle(cx).is_focused(window) {
521 return;
522 }
523 self.jump_to_query_bar_address(cx);
524 }
525
526 fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) {
527 use parse_int::parse;
528 let text = self.query_editor.read(cx).text(cx);
529
530 let Ok(as_address) = parse::<u64>(&text) else {
531 return self.jump_to_expression(text, cx);
532 };
533 self.jump_to_address(as_address, cx);
534 }
535
536 fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) {
537 let mut view_state = self.view_state();
538 view_state.base_row = (address & !0xfff) / view_state.line_width.width as u64;
539 let line_ix = (address & 0xfff) / view_state.line_width.width as u64;
540 view_state
541 .scroll_handle
542 .scroll_to_item(line_ix as usize, ScrollStrategy::Center);
543 cx.notify();
544 }
545
546 fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) {
547 let Ok(selected_frame) = self
548 .stack_frame_list
549 .update(cx, |this, _| this.opened_stack_frame_id())
550 else {
551 return;
552 };
553 let expr = format!("?${{{expr}}}");
554 let reference = self.session.update(cx, |this, cx| {
555 this.memory_reference_of_expr(selected_frame, expr, cx)
556 });
557 cx.spawn(async move |this, cx| {
558 if let Some((reference, typ)) = reference.await {
559 _ = this.update(cx, |this, cx| {
560 let sizeof_expr = if typ.as_ref().is_some_and(|t| {
561 t.chars()
562 .all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*')
563 }) {
564 typ.as_deref()
565 } else {
566 None
567 };
568 this.go_to_memory_reference(&reference, sizeof_expr, selected_frame, cx);
569 });
570 }
571 })
572 .detach();
573 }
574
575 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
576 self.view_state().selection = None;
577 cx.notify();
578 }
579
580 /// Jump to memory pointed to by selected memory range.
581 fn go_to_address(
582 &mut self,
583 _: &GoToSelectedAddress,
584 window: &mut Window,
585 cx: &mut Context<Self>,
586 ) {
587 let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state().selection.clone()
588 else {
589 return;
590 };
591 let range = drag.memory_range();
592 let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| {
593 this.read_memory(range, cx).map(|cell| cell.0).collect()
594 }) else {
595 return;
596 };
597 if memory.len() > 8 {
598 return;
599 }
600 let zeros_to_write = 8 - memory.len();
601 let mut acc = String::from("0x");
602 acc.extend(std::iter::repeat("00").take(zeros_to_write));
603 let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| {
604 _ = write!(&mut acc, "{:02x}", byte);
605 acc
606 });
607 self.query_editor.update(cx, |this, cx| {
608 this.set_text(as_query, window, cx);
609 });
610 self.jump_to_query_bar_address(cx);
611 }
612
613 fn deploy_memory_context_menu(
614 &mut self,
615 range: RangeInclusive<u64>,
616 position: Point<Pixels>,
617 window: &mut Window,
618 cx: &mut Context<Self>,
619 ) {
620 let session = self.session.clone();
621 let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
622 let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64;
623 let caps = session.read(cx).capabilities();
624 let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default()
625 && caps.supports_data_breakpoint_bytes.unwrap_or_default();
626 let memory_unreadable = LazyCell::new(|| {
627 session.update(cx, |this, cx| {
628 this.read_memory(range.clone(), cx)
629 .any(|cell| cell.0.is_none())
630 })
631 });
632
633 let mut menu = menu.action_disabled_when(
634 range_too_large || *memory_unreadable,
635 "Go To Selected Address",
636 GoToSelectedAddress.boxed_clone(),
637 );
638
639 if supports_data_breakpoints {
640 menu = menu.action_disabled_when(
641 *memory_unreadable,
642 "Set Data Breakpoint",
643 ToggleDataBreakpoint { access_type: None }.boxed_clone(),
644 );
645 }
646 menu.context(self.focus_handle.clone())
647 });
648
649 cx.focus_view(&context_menu, window);
650 let subscription = cx.subscribe_in(
651 &context_menu,
652 window,
653 |this, _, _: &DismissEvent, window, cx| {
654 if this.open_context_menu.as_ref().is_some_and(|context_menu| {
655 context_menu.0.focus_handle(cx).contains_focused(window, cx)
656 }) {
657 cx.focus_self(window);
658 }
659 this.open_context_menu.take();
660 cx.notify();
661 },
662 );
663
664 self.open_context_menu = Some((context_menu, position, subscription));
665 }
666}
667
668#[derive(Clone)]
669struct ViewWidth {
670 width: u8,
671 label: SharedString,
672}
673
674impl ViewWidth {
675 const fn new(width: u8, label: &'static str) -> Self {
676 Self {
677 width,
678 label: SharedString::new_static(label),
679 }
680 }
681}
682
683static WIDTHS: [ViewWidth; 7] = [
684 ViewWidth::new(1, "1 byte"),
685 ViewWidth::new(2, "2 bytes"),
686 ViewWidth::new(4, "4 bytes"),
687 ViewWidth::new(8, "8 bytes"),
688 ViewWidth::new(16, "16 bytes"),
689 ViewWidth::new(32, "32 bytes"),
690 ViewWidth::new(64, "64 bytes"),
691];
692
693fn render_single_memory_view_line(
694 memory: &[MemoryCell],
695 ix: u64,
696 weak: gpui::WeakEntity<MemoryView>,
697 cx: &mut App,
698) -> AnyElement {
699 let Ok(view_state) = weak.update(cx, |this, _| this.view_state().clone()) else {
700 return div().into_any();
701 };
702 let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64;
703
704 h_flex()
705 .id((
706 "memory-view-row-full",
707 ix * view_state.line_width.width as u64,
708 ))
709 .size_full()
710 .gap_x_2()
711 .child(
712 div()
713 .child(
714 Label::new(format!("{:016X}", base_address))
715 .buffer_font(cx)
716 .size(ui::LabelSize::Small)
717 .color(Color::Muted),
718 )
719 .px_1()
720 .border_r_1()
721 .border_color(Color::Muted.color(cx)),
722 )
723 .child(
724 h_flex()
725 .id((
726 "memory-view-row-raw-memory",
727 ix * view_state.line_width.width as u64,
728 ))
729 .px_1()
730 .children(memory.iter().enumerate().map(|(cell_ix, cell)| {
731 let weak = weak.clone();
732 div()
733 .id(("memory-view-row-raw-memory-cell", cell_ix as u64))
734 .px_0p5()
735 .when_some(view_state.selection.as_ref(), |this, selection| {
736 this.when(selection.contains(base_address + cell_ix as u64), |this| {
737 let weak = weak.clone();
738
739 this.bg(Color::Selected.color(cx).opacity(0.2)).when(
740 !selection.is_dragging(),
741 |this| {
742 let selection = selection.drag().memory_range();
743 this.on_mouse_down(
744 MouseButton::Right,
745 move |click, window, cx| {
746 _ = weak.update(cx, |this, cx| {
747 this.deploy_memory_context_menu(
748 selection.clone(),
749 click.position,
750 window,
751 cx,
752 )
753 });
754 cx.stop_propagation();
755 },
756 )
757 },
758 )
759 })
760 })
761 .child(
762 Label::new(
763 cell.0
764 .map(|val| HEX_BYTES_MEMOIZED[val as usize].clone())
765 .unwrap_or_else(|| UNKNOWN_BYTE.clone()),
766 )
767 .buffer_font(cx)
768 .when(cell.0.is_none(), |this| this.color(Color::Muted))
769 .size(ui::LabelSize::Small),
770 )
771 .on_drag(
772 Drag {
773 start_address: base_address + cell_ix as u64,
774 end_address: base_address + cell_ix as u64,
775 },
776 {
777 let weak = weak.clone();
778 move |drag, _, _, cx| {
779 _ = weak.update(cx, |this, _| {
780 this.view_state().selection =
781 Some(SelectedMemoryRange::DragUnderway(drag.clone()));
782 });
783
784 cx.new(|_| Empty)
785 }
786 },
787 )
788 .on_drop({
789 let weak = weak.clone();
790 move |drag: &Drag, _, cx| {
791 _ = weak.update(cx, |this, _| {
792 this.view_state().selection =
793 Some(SelectedMemoryRange::DragComplete(Drag {
794 start_address: drag.start_address,
795 end_address: base_address + cell_ix as u64,
796 }));
797 });
798 }
799 })
800 .drag_over(move |style, drag: &Drag, _, cx| {
801 _ = weak.update(cx, |this, _| {
802 this.view_state().selection =
803 Some(SelectedMemoryRange::DragUnderway(Drag {
804 start_address: drag.start_address,
805 end_address: base_address + cell_ix as u64,
806 }));
807 });
808
809 style
810 })
811 })),
812 )
813 .child(
814 h_flex()
815 .id((
816 "memory-view-row-ascii-memory",
817 ix * view_state.line_width.width as u64,
818 ))
819 .h_full()
820 .px_1()
821 .mr_4()
822 // .gap_x_1p5()
823 .border_x_1()
824 .border_color(Color::Muted.color(cx))
825 .children(memory.iter().enumerate().map(|(ix, cell)| {
826 let as_character = char::from(cell.0.unwrap_or(0));
827 let as_visible = if as_character.is_ascii_graphic() {
828 as_character
829 } else {
830 '·'
831 };
832 div()
833 .px_0p5()
834 .when_some(view_state.selection.as_ref(), |this, selection| {
835 this.when(selection.contains(base_address + ix as u64), |this| {
836 this.bg(Color::Selected.color(cx).opacity(0.2))
837 })
838 })
839 .child(
840 Label::new(format!("{as_visible}"))
841 .buffer_font(cx)
842 .when(cell.0.is_none(), |this| this.color(Color::Muted))
843 .size(ui::LabelSize::Small),
844 )
845 })),
846 )
847 .into_any()
848}
849
850impl Render for MemoryView {
851 fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
852 let (icon, tooltip_text) = if self.is_writing_memory {
853 (IconName::Pencil, "Edit Memory at a Selected Address")
854 } else {
855 (
856 IconName::LocationEdit,
857 "Change Address of Currently Viewed Memory",
858 )
859 };
860
861 v_flex()
862 .id("Memory-view")
863 .on_action(cx.listener(Self::cancel))
864 .on_action(cx.listener(Self::go_to_address))
865 .p_1()
866 .on_action(cx.listener(Self::confirm))
867 .on_action(cx.listener(Self::toggle_data_breakpoint))
868 .on_action(cx.listener(Self::page_down))
869 .on_action(cx.listener(Self::page_up))
870 .size_full()
871 .track_focus(&self.focus_handle)
872 .child(
873 h_flex()
874 .w_full()
875 .mb_1()
876 .gap_1()
877 .child(
878 h_flex()
879 .px_1()
880 .h_6()
881 .w_full()
882 .rounded_sm()
883 .gap_1()
884 .border_1()
885 .when_else(
886 self.query_editor
887 .focus_handle(cx)
888 .contains_focused(window, cx),
889 |this| this.border_color(cx.theme().colors().border_focused),
890 |this| this.border_color(cx.theme().colors().border_variant),
891 )
892 .bg(cx.theme().colors().editor_background)
893 .child(
894 div()
895 .id("memory-view-editor-icon")
896 .child(Icon::new(icon).size(ui::IconSize::XSmall))
897 .tooltip(Tooltip::text(tooltip_text)),
898 )
899 .child(Divider::vertical())
900 .child(self.render_query_bar(cx)),
901 )
902 .child(self.render_width_picker(window, cx)),
903 )
904 .child(Divider::horizontal())
905 .child(
906 v_flex()
907 .size_full()
908 .on_drag_move(cx.listener(|this, evt, _, _| {
909 this.handle_memory_drag(evt);
910 }))
911 .child(self.render_memory(cx).size_full())
912 .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
913 deferred(
914 anchored()
915 .position(*position)
916 .anchor(gpui::Anchor::TopLeft)
917 .child(menu.clone()),
918 )
919 .with_priority(1)
920 }))
921 .custom_scrollbars(
922 ui::Scrollbars::new(ui::ScrollAxes::Both)
923 .tracked_scroll_handle(&self.view_state_handle)
924 .with_track_along(
925 ui::ScrollAxes::Both,
926 cx.theme().colors().panel_background,
927 )
928 .tracked_entity(cx.entity_id()),
929 window,
930 cx,
931 ),
932 )
933 }
934}
935