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}