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