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