memory_view.rs

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