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