memory_view.rs

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