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