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