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}