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