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