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        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}