memory_view.rs

  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}