1use super::storage::{
4 StorageAccess, StorageSpace, hex_u256, next_storage_write_values, storage_access_at,
5 storage_accesses_until,
6};
7use crate::{DebugNode, DebuggerLayout, ExitReason, debugger::DebuggerContext};
8use alloy_primitives::{
9 Address, B256, U256, hex,
10 map::{B256Map, IndexMap},
11};
12use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
13use foundry_compilers::artifacts::sourcemap::SourceElement;
14use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
15use foundry_evm_traces::debug::SourceData;
16use foundry_tui::TuiApp;
17use ratatui::Frame;
18use revm::bytecode::opcode::OpCode;
19use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
20use std::{fmt::Write, ops::ControlFlow};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub(crate) enum StatusKind {
24 Info,
25 Error,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub(crate) struct StatusMessage {
30 pub(crate) kind: StatusKind,
31 pub(crate) text: String,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub(crate) struct ActiveInternalCallLocation {
36 pub(crate) trace_node_idx: usize,
37 pub(crate) marker_node_idx: usize,
38 pub(crate) marker_step_idx: usize,
39 pub(crate) entry_step: usize,
40 pub(crate) end_step: usize,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub(crate) struct ActiveInternalCallCache {
45 pub(crate) current_node_idx: usize,
46 pub(crate) trace_node_idx: usize,
47 pub(crate) absolute_step: usize,
48 pub(crate) location: Option<ActiveInternalCallLocation>,
49}
50
51impl ActiveInternalCallCache {
52 pub(crate) const fn matches(
53 self,
54 current_node_idx: usize,
55 trace_node_idx: usize,
56 absolute_step: usize,
57 ) -> bool {
58 self.current_node_idx == current_node_idx
59 && self.trace_node_idx == trace_node_idx
60 && self.absolute_step == absolute_step
61 }
62}
63
64#[derive(Default)]
66pub(crate) struct DrawMemory {
67 pub(crate) inner_call_index: usize,
68 pub(crate) current_buf_startline: usize,
69 pub(crate) current_storage_startline: usize,
70 pub(crate) current_stack_startline: usize,
71 pub(crate) active_internal_call: Option<ActiveInternalCallCache>,
72}
73
74#[derive(Default)]
75struct OpcodeListState {
76 inner_call_index: Option<usize>,
77 max_pc: usize,
78}
79
80#[derive(Debug, PartialEq, Eq)]
82pub(crate) enum Prompt {
83 Pc(String),
84 BufferOffset(String),
85 Command(String),
86 OpcodeSearch(String),
87}
88
89impl Prompt {
90 const fn input_mut(&mut self) -> &mut String {
91 match self {
92 Self::Pc(input)
93 | Self::BufferOffset(input)
94 | Self::Command(input)
95 | Self::OpcodeSearch(input) => input,
96 }
97 }
98
99 fn accepts(&self, c: char) -> bool {
100 match self {
101 Self::Pc(_) => c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':'),
102 Self::BufferOffset(input) => is_buffer_offset_input_char(input, c),
103 Self::Command(_) => !c.is_control(),
104 Self::OpcodeSearch(_) => true,
105 }
106 }
107}
108
109pub(crate) struct TUIContext<'a> {
110 pub(crate) debugger_context: &'a mut DebuggerContext,
111
112 pub(crate) key_buffer: String,
114 pub(crate) prompt: Option<Prompt>,
116 pub(crate) last_opcode_search: Option<String>,
118 pub(crate) status: Option<StatusMessage>,
120 pub(crate) current_step: usize,
122 pub(crate) draw_memory: DrawMemory,
123 opcode_list: OpcodeListState,
124
125 pub(crate) stack_labels: bool,
126 pub(crate) buf_utf: bool,
128 pub(crate) show_shortcuts: bool,
129 pub(crate) show_opcodes: bool,
130 pub(crate) show_source: bool,
131 pub(crate) show_variables: bool,
132 pub(crate) show_stack: bool,
133 pub(crate) show_data: bool,
134 pub(crate) active_buffer: BufferKind,
136 active_storage: Option<StorageSpace>,
137}
138
139impl<'a> TUIContext<'a> {
140 pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
141 TUIContext {
142 debugger_context,
143
144 key_buffer: String::with_capacity(64),
145 prompt: None,
146 last_opcode_search: None,
147 status: None,
148 current_step: 0,
149 draw_memory: DrawMemory::default(),
150 opcode_list: OpcodeListState::default(),
151
152 stack_labels: false,
153 buf_utf: false,
154 show_shortcuts: true,
155 show_opcodes: true,
156 show_source: true,
157 show_variables: true,
158 show_stack: true,
159 show_data: true,
160 active_buffer: BufferKind::Memory,
161 active_storage: None,
162 }
163 }
164
165 pub(crate) fn init(&mut self) {
166 self.refresh_opcode_list_state();
167 }
168
169 pub(crate) fn debug_arena(&self) -> &[DebugNode] {
170 &self.debugger_context.debug_arena
171 }
172
173 pub(crate) const fn layout(&self) -> DebuggerLayout {
174 self.debugger_context.layout
175 }
176
177 pub(crate) fn debug_call(&self) -> &DebugNode {
178 &self.debug_arena()[self.draw_memory.inner_call_index]
179 }
180
181 pub(crate) fn address(&self) -> &Address {
183 &self.debug_call().address
184 }
185
186 pub(crate) fn call_kind(&self) -> CallKind {
188 self.debug_call().kind
189 }
190
191 pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
193 &self.debug_call().steps
194 }
195
196 pub(crate) fn current_step(&self) -> &CallTraceStep {
198 &self.debug_steps()[self.current_step]
199 }
200
201 pub(super) const fn opcode_max_pc(&self) -> usize {
202 self.opcode_list.max_pc
203 }
204
205 fn refresh_opcode_list_state(&mut self) {
206 let inner_call_index = self.draw_memory.inner_call_index;
207 if self.opcode_list.inner_call_index == Some(inner_call_index) {
208 return;
209 }
210
211 let debug_steps = &self.debugger_context.debug_arena[inner_call_index].steps;
212 self.opcode_list.max_pc = debug_steps.iter().map(|step| step.pc).max().unwrap_or(0);
213 self.opcode_list.inner_call_index = Some(inner_call_index);
214 }
215
216 fn active_buffer(&self) -> &[u8] {
217 self.buffer(&self.active_buffer)
218 }
219
220 pub(super) const fn active_storage(&self) -> Option<StorageSpace> {
221 self.active_storage
222 }
223
224 pub(super) fn storage_accesses(&self, space: StorageSpace) -> IndexMap<U256, StorageAccess> {
225 storage_accesses_until(
226 self.debug_arena(),
227 self.draw_memory.inner_call_index,
228 self.current_step,
229 space,
230 )
231 }
232
233 pub(super) fn storage_label(
234 &self,
235 space: StorageSpace,
236 slot: U256,
237 storage_values: Option<&B256Map<B256>>,
238 next_values: Option<&B256Map<B256>>,
239 ) -> Option<String> {
240 if space != StorageSpace::Persistent {
241 return None;
242 }
243 let identifier = self.debugger_context.slot_identifiers.as_ref()?.get(self.address())?;
244 let slot = B256::from(slot);
245 identifier
246 .identify(&slot, None)
247 .or_else(|| {
248 storage_values.and_then(|values| identifier.identify_bytes_or_string(&slot, values))
249 })
250 .or_else(|| {
251 next_values.and_then(|values| identifier.identify_bytes_or_string(&slot, values))
254 })
255 .map(|info| info.label)
256 }
257
258 pub(super) fn next_storage_write_values(&self) -> B256Map<B256> {
259 next_storage_write_values(
260 self.debug_arena(),
261 self.draw_memory.inner_call_index,
262 self.current_step,
263 )
264 }
265
266 fn active_data_len(&self) -> usize {
267 self.active_storage.map_or_else(
268 || self.active_buffer().len().div_ceil(32),
269 |space| self.storage_accesses(space).len(),
270 )
271 }
272
273 fn buffer(&self, buffer: &BufferKind) -> &[u8] {
274 match buffer {
275 BufferKind::Memory => self.current_step().memory.as_ref().map_or(&[], |m| m.as_bytes()),
276 BufferKind::Calldata => &self.debug_call().calldata,
277 BufferKind::Returndata => &self.current_step().returndata,
278 }
279 }
280
281 pub(crate) const fn active_buffer_name(&self) -> &'static str {
282 buffer_name(&self.active_buffer)
283 }
284
285 pub(crate) fn src_map(&self) -> Result<(SourceElement, &SourceData), String> {
287 let address = self.address();
288 let Some(contract_name) = self.debugger_context.identified_contracts.get(address) else {
289 return Err(format!("Unknown contract at address {address}"));
290 };
291
292 self.debugger_context
293 .contracts_sources
294 .find_source_mapping(
295 contract_name,
296 self.current_step().pc as u32,
297 self.debug_call().kind.is_any_create(),
298 )
299 .ok_or_else(|| format!("No source map for contract {contract_name}"))
300 }
301}
302
303impl TUIContext<'_> {
304 pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
305 let ret = match event {
306 Event::Key(event) => self.handle_key_event(event),
307 Event::Mouse(event) => self.handle_mouse_event(event),
308 _ => ControlFlow::Continue(()),
309 };
310 self.refresh_opcode_list_state();
311 ret
312 }
313
314 fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
315 if let Some(prompt) = self.prompt.take() {
316 self.handle_prompt_key_event(prompt, event);
317 return ControlFlow::Continue(());
318 }
319
320 if let KeyCode::Char(c) = event.code
322 && c.is_alphabetic()
323 && self.key_buffer.starts_with('\'')
324 {
325 self.handle_breakpoint(c);
326 return ControlFlow::Continue(());
327 }
328
329 let control = event.modifiers.contains(KeyModifiers::CONTROL);
330
331 match event.code {
332 KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
334
335 KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
337 if this.active_storage.is_some() {
338 this.draw_memory.current_storage_startline =
339 this.draw_memory.current_storage_startline.saturating_sub(1);
340 } else {
341 this.draw_memory.current_buf_startline =
342 this.draw_memory.current_buf_startline.saturating_sub(1);
343 }
344 }),
345 KeyCode::Char('j') | KeyCode::Down if control => {
347 let max_line = self.active_data_len().saturating_sub(1);
348 self.repeat(|this| {
349 if this.active_storage.is_some() {
350 if this.draw_memory.current_storage_startline < max_line {
351 this.draw_memory.current_storage_startline += 1;
352 }
353 } else if this.draw_memory.current_buf_startline < max_line {
354 this.draw_memory.current_buf_startline += 1;
355 }
356 });
357 }
358
359 KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
361 KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
363
364 KeyCode::Char('K') => self.repeat(|this| {
366 this.draw_memory.current_stack_startline =
367 this.draw_memory.current_stack_startline.saturating_sub(1);
368 }),
369 KeyCode::Char('J') => self.repeat(|this| {
371 let max_stack =
372 this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
373 if this.draw_memory.current_stack_startline < max_stack {
374 this.draw_memory.current_stack_startline += 1;
375 }
376 }),
377
378 KeyCode::Char('b') => {
380 if self.active_storage.take().is_none() {
381 self.active_buffer = self.active_buffer.next();
382 }
383 self.draw_memory.current_buf_startline = 0;
384 self.set_info(format!("Active buffer: {}", self.active_buffer_name()));
385 }
386
387 KeyCode::Char('l') => self.cycle_layout(),
389
390 KeyCode::Char('g') => {
392 self.draw_memory.inner_call_index = 0;
393 self.current_step = 0;
394 self.update_scroll_positions();
395 }
396
397 KeyCode::Char('G') => {
399 self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
400 self.current_step = self.n_steps() - 1;
401 self.update_scroll_positions();
402 }
403
404 KeyCode::Char('c') if self.draw_memory.inner_call_index > 0 => {
406 self.draw_memory.inner_call_index -= 1;
407 self.current_step = self.n_steps() - 1;
408 self.update_scroll_positions();
409 }
410
411 KeyCode::Char('C')
413 if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 =>
414 {
415 self.draw_memory.inner_call_index += 1;
416 self.current_step = 0;
417 self.update_scroll_positions();
418 }
419
420 KeyCode::Char('s') => self.repeat(|this| {
422 let remaining_steps = &this.debug_steps()[this.current_step..];
423 if let Some((i, _)) =
424 remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
425 let prev = &remaining_steps[*i - 1];
426 is_jump(step, prev)
427 })
428 {
429 this.current_step += i;
430 this.update_scroll_positions();
431 }
432 }),
433
434 KeyCode::Char('a') => self.repeat(|this| {
436 let ops = &this.debug_steps()[..this.current_step];
437 this.current_step = ops
438 .iter()
439 .enumerate()
440 .skip(1)
441 .rev()
442 .find(|&(i, op)| {
443 let prev = &ops[i - 1];
444 is_jump(op, prev)
445 })
446 .map(|(i, _)| i)
447 .unwrap_or_default();
448 this.update_scroll_positions();
449 }),
450
451 KeyCode::Char('t') => {
453 self.stack_labels = !self.stack_labels;
454 self.set_info(format!("Stack labels: {}", toggle_state(self.stack_labels)));
455 }
456
457 KeyCode::Char('m') => {
459 self.buf_utf = !self.buf_utf;
460 self.set_info(format!("UTF-8 decoding: {}", toggle_state(self.buf_utf)));
461 }
462
463 KeyCode::Char('p') => {
465 self.key_buffer.clear();
466 self.status = None;
467 self.prompt = Some(Prompt::Pc(String::new()));
468 }
469
470 KeyCode::Char('o') => {
472 self.key_buffer.clear();
473 self.status = None;
474 if let Some(space) = self.active_storage {
475 self.prompt = Some(Prompt::Command(format!("{} ", space.command())));
476 } else {
477 self.prompt = Some(Prompt::BufferOffset(String::new()));
478 }
479 }
480
481 KeyCode::Char(':') => {
483 self.key_buffer.clear();
484 self.status = None;
485 self.prompt = Some(Prompt::Command(String::new()));
486 }
487
488 KeyCode::Char('/') => {
490 self.key_buffer.clear();
491 self.status = None;
492 self.prompt = Some(Prompt::OpcodeSearch(String::new()));
493 }
494
495 KeyCode::Char('n') => self.repeat(|this| {
497 this.repeat_opcode_search(SearchDirection::Forward);
498 }),
499
500 KeyCode::Char('N') => self.repeat(|this| {
502 this.repeat_opcode_search(SearchDirection::Backward);
503 }),
504
505 KeyCode::Char('h') => {
507 self.show_shortcuts = !self.show_shortcuts;
508 let state = if self.show_shortcuts { "shown" } else { "hidden" };
509 self.set_info(format!("Shortcut help: {state}"));
510 }
511
512 KeyCode::Char(
514 other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
515 ) => {
516 self.key_buffer.push(other);
518 return ControlFlow::Continue(());
519 }
520
521 _ => {}
523 };
524
525 self.key_buffer.clear();
526 ControlFlow::Continue(())
527 }
528
529 fn handle_prompt_key_event(&mut self, mut prompt: Prompt, event: KeyEvent) {
530 match event.code {
531 KeyCode::Esc => return,
532 KeyCode::Enter => {
533 match prompt {
534 Prompt::Pc(input) => self.goto_pc_from_input(&input),
535 Prompt::BufferOffset(input) => self.goto_buffer_offset_from_input(&input),
536 Prompt::Command(input) => self.run_command_from_input(&input),
537 Prompt::OpcodeSearch(input) => self.search_opcode_from_input(&input),
538 }
539 return;
540 }
541 KeyCode::Backspace => {
542 prompt.input_mut().pop();
543 }
544 KeyCode::Char(c)
545 if !event.modifiers.contains(KeyModifiers::CONTROL) && prompt.accepts(c) =>
546 {
547 prompt.input_mut().push(c);
548 }
549 _ => {}
550 }
551 self.prompt = Some(prompt);
552 }
553
554 fn search_opcode_from_input(&mut self, input: &str) {
555 let query = input.trim();
556 if query.is_empty() {
557 self.set_error("Enter an opcode search term".to_string());
558 return;
559 }
560
561 self.last_opcode_search = Some(query.to_string());
562 self.search_opcode(query, SearchDirection::Forward);
563 }
564
565 fn repeat_opcode_search(&mut self, direction: SearchDirection) {
566 let Some(query) = self.last_opcode_search.clone() else {
567 self.set_error("No previous opcode search".to_string());
568 return;
569 };
570
571 self.search_opcode(&query, direction);
572 }
573
574 fn search_opcode(&mut self, query: &str, direction: SearchDirection) {
575 let Some((node_index, step_index)) = find_opcode_match(
576 self.debug_arena(),
577 self.draw_memory.inner_call_index,
578 self.current_step,
579 query,
580 direction,
581 ) else {
582 self.set_error(format!("No opcode matching `{query}` in current call"));
583 return;
584 };
585
586 self.draw_memory.inner_call_index = node_index;
587 self.current_step = step_index;
588 self.update_scroll_positions();
589
590 let pc = self.current_step().pc;
591 let opcode = pretty_opcode(self.current_step());
592 self.set_info(format!("Found `{query}` at PC 0x{pc:x} ({pc}): {opcode}"));
593 }
594
595 fn goto_pc_from_input(&mut self, input: &str) {
596 self.move_to_pc_from_input(input, find_pc_target);
597 }
598
599 fn move_to_pc_from_input(
600 &mut self,
601 input: &str,
602 find_target: impl Fn(&[DebugNode], usize, usize, usize) -> Option<StepTarget>,
603 ) {
604 let candidates = match parse_pc_candidates(input) {
605 Ok(candidates) => candidates,
606 Err(err) => {
607 self.set_error(err);
608 return;
609 }
610 };
611
612 let mut found = Vec::new();
613 for &candidate in &candidates {
614 if let Some(target) = find_target(
615 self.debug_arena(),
616 self.draw_memory.inner_call_index,
617 self.current_step,
618 candidate.pc,
619 ) {
620 found.push((candidate, target));
621 }
622 }
623
624 match found.as_slice() {
625 [] => {
626 let current = self.debug_call();
627 let outside = candidates.iter().any(|candidate| {
628 pc_exists_outside_code_context(self.debug_arena(), current, candidate.pc)
629 });
630 let pc = if let [candidate] = candidates.as_slice() {
631 let pc = candidate.pc;
632 format!("PC 0x{pc:x} ({pc})")
633 } else {
634 format!("PC `{}`", input.trim())
635 };
636 let mut msg = format!("{pc} not found in current contract");
637 if outside {
638 msg.push_str("; it exists in another contract, switch calls first");
639 }
640 self.set_error(msg);
641 }
642 [(candidate, target)] => self.apply_pc_target(*candidate, *target),
643 _ => {
644 let input = input.trim();
645 let options = found
646 .iter()
647 .map(|(candidate, _)| candidate.describe())
648 .collect::<Vec<_>>()
649 .join(" and ");
650 self.set_error(format!(
651 "Ambiguous PC `{input}`: {options} both exist; use d:<pc> or 0x<pc>"
652 ));
653 }
654 }
655 }
656
657 fn apply_pc_target(&mut self, candidate: PcCandidate, target: StepTarget) {
658 let already_at_target = self.draw_memory.inner_call_index == target.node_index
659 && self.current_step == target.step_index;
660
661 self.draw_memory.inner_call_index = target.node_index;
662 self.current_step = target.step_index;
663 self.draw_memory.current_buf_startline = 0;
664 self.draw_memory.current_stack_startline = 0;
665 self.update_scroll_positions();
666 self.key_buffer.clear();
667
668 let pc = candidate.pc;
669 let scope = match target.scope {
670 StepTargetScope::CurrentNode => "current trace",
671 StepTargetScope::SameCodeContext => "same contract",
672 };
673 let action = if already_at_target { "Already at" } else { "Jumped to" };
674 self.set_info(format!("{action} PC 0x{pc:x} ({pc}) in {scope}"));
675 }
676
677 fn goto_buffer_offset_from_input(&mut self, input: &str) {
678 self.goto_buffer_offset(self.active_buffer, input);
679 }
680
681 fn goto_buffer_offset(&mut self, buffer: BufferKind, input: &str) {
682 let offset = match parse_buffer_offset(input) {
683 Ok(offset) => offset,
684 Err(err) => {
685 self.set_error(err);
686 return;
687 }
688 };
689
690 let buffer_name = buffer_name(&buffer);
691 let buffer_len = self.buffer(&buffer).len();
692 if buffer_len == 0 {
693 self.set_error(format!("Current {buffer_name} buffer is empty"));
694 return;
695 }
696
697 if offset >= buffer_len {
698 self.set_error(format!(
699 "{buffer_name} offset 0x{offset:x} ({offset}) is outside the {buffer_len}-byte buffer"
700 ));
701 return;
702 }
703
704 self.active_buffer = buffer;
705 self.active_storage = None;
706 self.apply_buffer_offset(offset);
707 }
708
709 fn run_command_from_input(&mut self, input: &str) {
710 let input = input.trim();
711 let input = input.strip_prefix(':').unwrap_or(input).trim_start();
712 if input.is_empty() {
713 self.set_error("Enter a debugger command".to_string());
714 return;
715 }
716
717 let mut parts = input.split_whitespace();
718 let command = parts.next().unwrap();
719 let is_continue_command = CONTINUE_COMMANDS.contains(&command);
720 if is_continue_command || PC_COMMANDS.contains(&command) {
721 let Some(pc) = parts.next() else {
722 return self.set_error(command_usage(command, "<pc>"));
723 };
724 if parts.next().is_some() {
725 return self.set_error(command_usage(command, "<pc>"));
726 }
727 if is_continue_command {
728 self.move_to_pc_from_input(pc, find_next_pc_target);
729 } else {
730 self.goto_pc_from_input(pc);
731 }
732 } else if MEMORY_COMMANDS.contains(&command) {
733 self.run_buffer_command(command, BufferKind::Memory, parts);
734 } else if CALLDATA_COMMANDS.contains(&command) {
735 self.run_buffer_command(command, BufferKind::Calldata, parts);
736 } else if RETURNDATA_COMMANDS.contains(&command) {
737 self.run_buffer_command(command, BufferKind::Returndata, parts);
738 } else if STORAGE_COMMANDS.contains(&command) {
739 self.run_storage_command(command, StorageSpace::Persistent, parts);
740 } else if TRANSIENT_STORAGE_COMMANDS.contains(&command) {
741 self.run_storage_command(command, StorageSpace::Transient, parts);
742 } else if LINE_COMMANDS.contains(&command) {
743 let Some(line) = parts.next() else {
744 return self.set_error(command_usage(command, "<line>"));
745 };
746 if parts.next().is_some() {
747 return self.set_error(command_usage(command, "<line>"));
748 }
749 self.goto_source_line_from_input(line);
750 } else if OPCODE_COMMANDS.contains(&command) {
751 self.run_pane_command(command, PaneCommand::Opcodes, parts);
752 } else if SOURCE_COMMANDS.contains(&command) {
753 self.run_pane_command(command, PaneCommand::Source, parts);
754 } else if VARIABLES_COMMANDS.contains(&command) {
755 self.run_pane_command(command, PaneCommand::Variables, parts);
756 } else if STACK_COMMANDS.contains(&command) {
757 self.run_pane_command(command, PaneCommand::Stack, parts);
758 } else if DATA_COMMANDS.contains(&command) {
759 self.run_pane_command(command, PaneCommand::Data, parts);
760 } else if HELP_COMMANDS.contains(&command) {
761 self.set_info(command_help());
762 } else {
763 self.set_error(format!("Unknown command `{command}`; try `help`"));
764 }
765 }
766
767 fn run_buffer_command<'a>(
768 &mut self,
769 command: &str,
770 buffer: BufferKind,
771 mut args: impl Iterator<Item = &'a str>,
772 ) {
773 let Some(offset) = args.next() else {
774 self.select_buffer(buffer);
775 return;
776 };
777 if args.next().is_some() {
778 return self.set_error(command_usage(command, "<offset>"));
779 }
780 self.goto_buffer_offset(buffer, offset);
781 }
782
783 fn run_storage_command<'a>(
784 &mut self,
785 command: &str,
786 space: StorageSpace,
787 mut args: impl Iterator<Item = &'a str>,
788 ) {
789 let Some(slot) = args.next() else {
790 self.select_storage(space);
791 return;
792 };
793 if args.next().is_some() {
794 return self.set_error(command_usage(command, "<slot>"));
795 }
796 self.goto_storage_slot_from_input(slot, space);
797 }
798
799 fn select_buffer(&mut self, buffer: BufferKind) {
800 self.active_buffer = buffer;
801 self.active_storage = None;
802 self.draw_memory.current_buf_startline = 0;
803 self.set_info(format!("Active buffer: {}", self.active_buffer_name()));
804 }
805
806 fn select_storage(&mut self, space: StorageSpace) {
807 self.active_storage = Some(space);
808 self.draw_memory.current_storage_startline = 0;
809 self.set_info(format!("Active data: {}", space.noun()));
810 }
811
812 fn goto_source_line_from_input(&mut self, input: &str) {
813 let line = match input.parse::<usize>() {
814 Ok(line) if line > 0 => line,
815 _ => {
816 self.set_error(format!(
817 "Invalid source line `{input}`; use a positive decimal line number"
818 ));
819 return;
820 }
821 };
822
823 let (source_path, source_line, contract_name) = {
824 let (_, source) = match self.src_map() {
825 Ok(source) => source,
826 Err(err) => {
827 self.set_error(err);
828 return;
829 }
830 };
831 let Some(source_line) = source_line_range(&source.source, line) else {
832 let line_count = source.source.lines().count().max(1);
833 self.set_error(format!(
834 "Source line {line} is outside {} ({line_count} lines)",
835 source.path.display()
836 ));
837 return;
838 };
839 let contract_name = self
840 .debugger_context
841 .identified_contracts
842 .get(self.address())
843 .expect("source mapping requires an identified contract")
844 .clone();
845 (source.path.clone(), source_line, contract_name)
846 };
847
848 let sources = &self.debugger_context.contracts_sources;
849 let Some(target) = find_step_target(
850 self.debug_arena(),
851 self.draw_memory.inner_call_index,
852 self.current_step,
853 |node, step| {
854 let Some((source_element, source)) = sources.find_source_mapping(
855 &contract_name,
856 step.pc as u32,
857 node.kind.is_any_create(),
858 ) else {
859 return false;
860 };
861 source.path == source_path
862 && source_line.contains(&(source_element.offset() as usize))
863 },
864 ) else {
865 self.set_error(format!(
866 "No opcode mapped to {}:{line} in current contract",
867 source_path.display()
868 ));
869 return;
870 };
871
872 let already_at_target = self.draw_memory.inner_call_index == target.node_index
873 && self.current_step == target.step_index;
874 self.draw_memory.inner_call_index = target.node_index;
875 self.current_step = target.step_index;
876 self.draw_memory.current_buf_startline = 0;
877 self.draw_memory.current_stack_startline = 0;
878 self.update_scroll_positions();
879 self.key_buffer.clear();
880
881 let pc = self.current_step().pc;
882 let action = if already_at_target { "Already at" } else { "Jumped to" };
883 self.set_info(format!("{action} {}:{line} at PC 0x{pc:x} ({pc})", source_path.display()));
884 }
885
886 fn run_pane_command<'a>(
887 &mut self,
888 command: &str,
889 pane: PaneCommand,
890 mut args: impl Iterator<Item = &'a str>,
891 ) {
892 if args.next().is_some() {
893 return self.set_error(command_usage(command, ""));
894 }
895 let shown = match pane {
896 PaneCommand::Opcodes => {
897 self.show_opcodes = !self.show_opcodes;
898 self.show_opcodes
899 }
900 PaneCommand::Source => {
901 self.show_source = !self.show_source;
902 self.show_source
903 }
904 PaneCommand::Variables => {
905 self.show_variables = !self.show_variables;
906 self.show_variables
907 }
908 PaneCommand::Stack => {
909 self.show_stack = !self.show_stack;
910 self.show_stack
911 }
912 PaneCommand::Data => {
913 self.show_data = !self.show_data;
914 self.show_data
915 }
916 };
917 let state = if shown { "shown" } else { "hidden" };
918 self.set_info(format!("{} pane: {state}", pane.label()));
919 }
920
921 fn goto_storage_slot_from_input(&mut self, input: &str, space: StorageSpace) {
922 let slot = match parse_storage_slot(input) {
923 Ok(slot) => slot,
924 Err(err) => {
925 self.set_error(err);
926 return;
927 }
928 };
929
930 let Some(target) = find_storage_target(
931 self.debug_arena(),
932 self.draw_memory.inner_call_index,
933 self.current_step,
934 slot,
935 space,
936 ) else {
937 self.set_error(format!(
938 "{} slot {} not accessed in current call",
939 space.label(),
940 hex_u256(slot)
941 ));
942 return;
943 };
944
945 let access = target.access;
946 self.draw_memory.inner_call_index = target.node_index;
947 self.current_step = access.step_index();
948 self.draw_memory.current_buf_startline = 0;
949 self.draw_memory.current_stack_startline = 0;
950 self.active_storage = Some(space);
951 self.draw_memory.current_storage_startline =
952 self.storage_accesses(space).get_index_of(&access.slot()).unwrap_or_default();
953 self.update_scroll_positions();
954 self.key_buffer.clear();
955 self.set_info(format!(
956 "Jumped to {} at PC 0x{:x} ({})",
957 access.describe(),
958 access.pc(),
959 access.pc()
960 ));
961 }
962
963 fn apply_buffer_offset(&mut self, offset: usize) {
964 self.draw_memory.current_buf_startline = offset / 32;
965 self.key_buffer.clear();
966 let buffer_name = self.active_buffer_name();
967 self.set_info(format!("Jumped to {buffer_name} offset 0x{offset:x} ({offset})"));
968 }
969
970 fn set_info(&mut self, text: String) {
971 self.status = Some(StatusMessage { kind: StatusKind::Info, text });
972 }
973
974 fn set_error(&mut self, text: String) {
975 self.status = Some(StatusMessage { kind: StatusKind::Error, text });
976 }
977
978 fn handle_breakpoint(&mut self, c: char) {
979 self.key_buffer.clear();
980
981 let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c).copied() else {
982 self.set_error(format!("Breakpoint '{c}' not found"));
983 return;
984 };
985
986 let Some((inner_call_index, step_index)) = find_next_step_target(
987 self.debug_arena(),
988 self.draw_memory.inner_call_index,
989 self.current_step,
990 |node, step| node.address == caller && step.pc == pc,
991 ) else {
992 self.set_error(format!("Breakpoint '{c}' target not found in trace"));
993 return;
994 };
995
996 let already_at_target = self.draw_memory.inner_call_index == inner_call_index
997 && self.current_step == step_index;
998
999 self.draw_memory.inner_call_index = inner_call_index;
1000 self.current_step = step_index;
1001 self.update_scroll_positions();
1002
1003 let action = if already_at_target { "Already at" } else { "Jumped to" };
1004 self.set_info(format!("{action} breakpoint '{c}' at PC 0x{pc:x} ({pc})"));
1005 }
1006
1007 fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
1008 if self.prompt.is_some() {
1009 return ControlFlow::Continue(());
1010 }
1011
1012 match event.kind {
1013 MouseEventKind::ScrollUp => self.step_back(),
1014 MouseEventKind::ScrollDown => self.step(),
1015 _ => {}
1016 }
1017
1018 ControlFlow::Continue(())
1019 }
1020
1021 fn step_back(&mut self) {
1022 if self.current_step > 0 {
1023 self.current_step -= 1;
1024 } else if self.draw_memory.inner_call_index > 0 {
1025 self.draw_memory.inner_call_index -= 1;
1026 self.current_step = self.n_steps() - 1;
1027 }
1028 self.update_scroll_positions();
1029 }
1030
1031 fn step(&mut self) {
1032 if self.current_step < self.n_steps() - 1 {
1033 self.current_step += 1;
1034 } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
1035 self.draw_memory.inner_call_index += 1;
1036 self.current_step = 0;
1037 }
1038 self.update_scroll_positions();
1039 }
1040
1041 fn update_scroll_positions(&mut self) {
1042 if let Some(stack) = &self.current_step().stack
1043 && !stack.is_empty()
1044 {
1045 self.draw_memory.current_stack_startline =
1046 self.draw_memory.current_stack_startline.min(stack.len().saturating_sub(1));
1047 }
1048
1049 if self.active_buffer == BufferKind::Memory
1050 && let Some(line) = self.current_memory_write_line()
1051 {
1052 self.draw_memory.current_buf_startline = line;
1053 }
1054
1055 let buffer_len = self.active_buffer().len();
1056 if buffer_len > 0 {
1057 let max_line = buffer_len.div_ceil(32) - 1;
1058 self.draw_memory.current_buf_startline =
1059 self.draw_memory.current_buf_startline.min(max_line);
1060 }
1061 }
1062
1063 fn current_memory_write_line(&self) -> Option<usize> {
1064 let memory_len = self.current_step().memory.as_ref()?.len();
1065
1066 if self.current_step > 0 {
1067 let prev_step = &self.debug_steps()[self.current_step - 1];
1068 if let Some(line) = bounded_memory_write_start_line(prev_step, memory_len) {
1069 return Some(line);
1070 }
1071 }
1072
1073 bounded_memory_write_start_line(self.current_step(), memory_len)
1074 }
1075
1076 fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
1078 for _ in 0..buffer_as_number(&self.key_buffer) {
1079 f(self);
1080 }
1081 }
1082
1083 fn n_steps(&self) -> usize {
1084 self.debug_steps().len()
1085 }
1086
1087 fn cycle_layout(&mut self) {
1088 let layout = self.debugger_context.layout.next();
1089 self.debugger_context.layout = layout;
1090 self.status = Some(StatusMessage {
1091 kind: StatusKind::Info,
1092 text: format!("Debugger layout: {}", layout.as_str()),
1093 });
1094 }
1095}
1096
1097impl TuiApp for TUIContext<'_> {
1098 type Exit = ExitReason;
1099
1100 fn draw(&mut self, frame: &mut Frame<'_>) {
1101 self.draw_layout(frame);
1102 }
1103
1104 fn handle_event(&mut self, event: Event) -> ControlFlow<Self::Exit> {
1105 TUIContext::handle_event(self, event)
1106 }
1107}
1108
1109fn buffer_as_number(s: &str) -> usize {
1111 const MIN: usize = 1;
1112 const MAX: usize = 100_000;
1113 s.parse().unwrap_or(MIN).clamp(MIN, MAX)
1114}
1115
1116const fn toggle_state(enabled: bool) -> &'static str {
1117 if enabled { "on" } else { "off" }
1118}
1119
1120const fn buffer_name(buffer: &BufferKind) -> &'static str {
1121 match buffer {
1122 BufferKind::Memory => "memory",
1123 BufferKind::Calldata => "calldata",
1124 BufferKind::Returndata => "returndata",
1125 }
1126}
1127
1128const CONTINUE_COMMANDS: &[&str] = &["continue", "cont", "c"];
1129const PC_COMMANDS: &[&str] = &["pc", "p"];
1130const MEMORY_COMMANDS: &[&str] = &["mem", "memory"];
1131const CALLDATA_COMMANDS: &[&str] = &["calldata", "cd"];
1132const RETURNDATA_COMMANDS: &[&str] = &["returndata", "ret", "rd"];
1133const STORAGE_COMMANDS: &[&str] = &["storage", "store", "slot"];
1134const TRANSIENT_STORAGE_COMMANDS: &[&str] = &["transient", "tslot"];
1135const LINE_COMMANDS: &[&str] = &["line", "ln"];
1136const OPCODE_COMMANDS: &[&str] = &["opcodes", "opcode", "ops"];
1137const SOURCE_COMMANDS: &[&str] = &["source", "src"];
1138const VARIABLES_COMMANDS: &[&str] = &["variables", "vars"];
1139const STACK_COMMANDS: &[&str] = &["stack"];
1140const DATA_COMMANDS: &[&str] = &["data"];
1141const HELP_COMMANDS: &[&str] = &["help", "h"];
1142
1143#[derive(Clone, Copy)]
1144enum PaneCommand {
1145 Opcodes,
1146 Source,
1147 Variables,
1148 Stack,
1149 Data,
1150}
1151
1152impl PaneCommand {
1153 const fn label(self) -> &'static str {
1154 match self {
1155 Self::Opcodes => "Opcodes",
1156 Self::Source => "Source",
1157 Self::Variables => "Variables",
1158 Self::Stack => "Stack",
1159 Self::Data => "Data",
1160 }
1161 }
1162}
1163
1164fn command_usage(command: &str, arg: &str) -> String {
1165 if arg.is_empty() { format!("Usage: :{command}") } else { format!("Usage: :{command} {arg}") }
1166}
1167
1168fn command_help() -> String {
1169 format!(
1170 "Commands: {} <pc>, {} <pc>, {} [<offset>], {} [<offset>], {} [<offset>], {} [<slot>], {} [<slot>], {} <line>, {}, {}, {}, {}, {}",
1171 command_aliases(CONTINUE_COMMANDS),
1172 command_aliases(PC_COMMANDS),
1173 command_aliases(MEMORY_COMMANDS),
1174 command_aliases(CALLDATA_COMMANDS),
1175 command_aliases(RETURNDATA_COMMANDS),
1176 command_aliases(STORAGE_COMMANDS),
1177 command_aliases(TRANSIENT_STORAGE_COMMANDS),
1178 command_aliases(LINE_COMMANDS),
1179 command_aliases(OPCODE_COMMANDS),
1180 command_aliases(SOURCE_COMMANDS),
1181 command_aliases(VARIABLES_COMMANDS),
1182 command_aliases(STACK_COMMANDS),
1183 command_aliases(DATA_COMMANDS)
1184 )
1185}
1186
1187fn command_aliases(commands: &[&str]) -> String {
1188 commands.iter().map(|command| format!(":{command}")).collect::<Vec<_>>().join("/")
1189}
1190
1191fn is_buffer_offset_input_char(input: &str, c: char) -> bool {
1192 if !(c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':')) {
1193 return false;
1194 }
1195
1196 let mut next = String::with_capacity(input.len() + c.len_utf8());
1197 next.push_str(input);
1198 next.push(c);
1199 is_buffer_offset_input_prefix(&next)
1200}
1201
1202fn is_buffer_offset_input_prefix(input: &str) -> bool {
1203 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1204 return rest.chars().all(|c| c.is_ascii_hexdigit());
1205 }
1206
1207 if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1208 return rest.chars().all(|c| c.is_ascii_digit());
1209 }
1210
1211 input.chars().all(|c| c.is_ascii_hexdigit())
1212 || "d:".starts_with(input)
1213 || "dec:".starts_with(input)
1214}
1215
1216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1217enum SearchDirection {
1218 Forward,
1219 Backward,
1220}
1221
1222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1223enum PcBase {
1224 Hex,
1225 Decimal,
1226}
1227
1228#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1229struct PcCandidate {
1230 pc: usize,
1231 base: PcBase,
1232}
1233
1234impl PcCandidate {
1235 fn describe(self) -> String {
1236 match self.base {
1237 PcBase::Hex => format!("hex 0x{:x}", self.pc),
1238 PcBase::Decimal => format!("decimal {}", self.pc),
1239 }
1240 }
1241}
1242
1243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1244enum StepTargetScope {
1245 CurrentNode,
1246 SameCodeContext,
1247}
1248
1249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1250struct StepTarget {
1251 node_index: usize,
1252 step_index: usize,
1253 scope: StepTargetScope,
1254}
1255
1256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1257struct StorageTarget {
1258 node_index: usize,
1259 access: StorageAccess,
1260}
1261
1262fn parse_pc_candidates(input: &str) -> Result<Vec<PcCandidate>, String> {
1263 let input = input.trim();
1264 if input.is_empty() {
1265 return Err("Enter a program counter".to_string());
1266 }
1267
1268 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1269 return parse_pc_candidate(rest, 16, PcBase::Hex, input);
1270 }
1271
1272 if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1273 return parse_pc_candidate(rest, 10, PcBase::Decimal, input);
1274 }
1275
1276 if input.chars().any(|c| c.is_ascii_hexdigit() && c.is_ascii_alphabetic()) {
1277 return parse_pc_candidate(input, 16, PcBase::Hex, input);
1278 }
1279
1280 if input.chars().all(|c| c.is_ascii_digit()) {
1281 let decimal = parse_pc(input, 10, input)?;
1282 let hex = parse_pc(input, 16, input)?;
1283 if decimal == hex {
1284 return Ok(vec![PcCandidate { pc: decimal, base: PcBase::Decimal }]);
1285 }
1286 return Ok(vec![
1287 PcCandidate { pc: decimal, base: PcBase::Decimal },
1288 PcCandidate { pc: hex, base: PcBase::Hex },
1289 ]);
1290 }
1291
1292 Err(format!("Invalid PC `{input}`; use 0x2a, 2a, or d:42"))
1293}
1294
1295fn parse_pc_candidate(
1296 input: &str,
1297 radix: u32,
1298 base: PcBase,
1299 original: &str,
1300) -> Result<Vec<PcCandidate>, String> {
1301 Ok(vec![PcCandidate { pc: parse_pc(input, radix, original)?, base }])
1302}
1303
1304fn parse_pc(input: &str, radix: u32, original: &str) -> Result<usize, String> {
1305 if input.is_empty() {
1306 return Err(format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"));
1307 }
1308 usize::from_str_radix(input, radix)
1309 .map_err(|_| format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"))
1310}
1311
1312fn parse_buffer_offset(input: &str) -> Result<usize, String> {
1313 let input = input.trim();
1314 if input.is_empty() {
1315 return Err("Enter a buffer offset".to_string());
1316 }
1317
1318 let (digits, radix) =
1319 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1320 (rest, 16)
1321 } else if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1322 (rest, 10)
1323 } else {
1324 (input, 16)
1325 };
1326
1327 if digits.is_empty() {
1328 return Err(invalid_buffer_offset(input));
1329 }
1330
1331 usize::from_str_radix(digits, radix).map_err(|_| invalid_buffer_offset(input))
1332}
1333
1334fn invalid_buffer_offset(input: &str) -> String {
1335 format!("Invalid buffer offset `{input}`; use hex 0x20/20 or decimal d:32")
1336}
1337
1338fn parse_storage_slot(input: &str) -> Result<U256, String> {
1339 let input = input.trim();
1340 if input.is_empty() {
1341 return Err("Enter a storage slot".to_string());
1342 }
1343
1344 let (digits, radix) =
1345 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
1346 (rest, 16)
1347 } else if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
1348 (rest, 10)
1349 } else {
1350 (input, 16)
1351 };
1352
1353 let valid_digits = match radix {
1354 10 => digits.bytes().all(|b| b.is_ascii_digit()),
1355 16 => digits.bytes().all(|b| b.is_ascii_hexdigit()),
1356 _ => unreachable!(),
1357 };
1358 if digits.is_empty() || !valid_digits {
1359 return Err(invalid_storage_slot(input));
1360 }
1361
1362 U256::from_str_radix(digits, radix).map_err(|_| invalid_storage_slot(input))
1363}
1364
1365fn invalid_storage_slot(input: &str) -> String {
1366 format!("Invalid storage slot `{input}`; use hex 0x20/20 or decimal d:32")
1367}
1368
1369fn find_storage_target(
1370 arena: &[DebugNode],
1371 current_node_index: usize,
1372 current_step: usize,
1373 slot: U256,
1374 space: StorageSpace,
1375) -> Option<StorageTarget> {
1376 let current_node = arena.get(current_node_index)?;
1377 let trace_node_idx = current_node.trace_node_idx;
1378 let current_absolute_step = current_node.step_offset.saturating_add(current_step);
1379
1380 storage_target_at(arena, current_node_index, current_step, slot, space)
1381 .or_else(|| {
1382 find_storage_target_after(arena, trace_node_idx, current_absolute_step, slot, space)
1383 })
1384 .or_else(|| {
1385 find_storage_target_before(arena, trace_node_idx, current_absolute_step, slot, space)
1386 })
1387}
1388
1389fn storage_target_at(
1390 arena: &[DebugNode],
1391 node_index: usize,
1392 step_index: usize,
1393 slot: U256,
1394 space: StorageSpace,
1395) -> Option<StorageTarget> {
1396 let node = arena.get(node_index)?;
1397 storage_access_at(&node.steps, step_index)
1398 .filter(|access| access.slot() == slot && access.space() == space)
1399 .map(|access| StorageTarget { node_index, access })
1400}
1401
1402fn find_storage_target_after(
1403 arena: &[DebugNode],
1404 trace_node_idx: usize,
1405 current_absolute_step: usize,
1406 slot: U256,
1407 space: StorageSpace,
1408) -> Option<StorageTarget> {
1409 let mut best = None;
1410
1411 for (node_index, node) in arena.iter().enumerate() {
1412 if node.trace_node_idx != trace_node_idx {
1413 continue;
1414 }
1415
1416 for step_index in 0..node.steps.len() {
1417 let absolute_step = node.step_offset.saturating_add(step_index);
1418 if absolute_step <= current_absolute_step {
1419 continue;
1420 }
1421
1422 let Some(access) = storage_access_at(&node.steps, step_index)
1423 .filter(|access| access.slot() == slot && access.space() == space)
1424 else {
1425 continue;
1426 };
1427
1428 match best {
1429 Some((best_absolute_step, _, _)) if absolute_step >= best_absolute_step => {}
1430 _ => best = Some((absolute_step, node_index, access)),
1431 }
1432 break;
1433 }
1434 }
1435
1436 best.map(|(_, node_index, access)| StorageTarget { node_index, access })
1437}
1438
1439fn find_storage_target_before(
1440 arena: &[DebugNode],
1441 trace_node_idx: usize,
1442 current_absolute_step: usize,
1443 slot: U256,
1444 space: StorageSpace,
1445) -> Option<StorageTarget> {
1446 let mut best = None;
1447
1448 for (node_index, node) in arena.iter().enumerate() {
1449 if node.trace_node_idx != trace_node_idx {
1450 continue;
1451 }
1452
1453 for step_index in (0..node.steps.len()).rev() {
1454 let absolute_step = node.step_offset.saturating_add(step_index);
1455 if absolute_step >= current_absolute_step {
1456 continue;
1457 }
1458
1459 let Some(access) = storage_access_at(&node.steps, step_index)
1460 .filter(|access| access.slot() == slot && access.space() == space)
1461 else {
1462 continue;
1463 };
1464
1465 match best {
1466 Some((best_absolute_step, _, _)) if absolute_step <= best_absolute_step => {}
1467 _ => best = Some((absolute_step, node_index, access)),
1468 }
1469 break;
1470 }
1471 }
1472
1473 best.map(|(_, node_index, access)| StorageTarget { node_index, access })
1474}
1475
1476fn find_pc_target(
1477 arena: &[DebugNode],
1478 current_node_index: usize,
1479 current_step: usize,
1480 pc: usize,
1481) -> Option<StepTarget> {
1482 find_step_target(arena, current_node_index, current_step, |_, step| step.pc == pc)
1483}
1484
1485fn find_next_pc_target(
1486 arena: &[DebugNode],
1487 current_node_index: usize,
1488 current_step: usize,
1489 pc: usize,
1490) -> Option<StepTarget> {
1491 let current_node = arena.get(current_node_index)?;
1492 let (node_index, step_index) =
1493 find_next_step_target(arena, current_node_index, current_step, |node, step| {
1494 same_code_context(current_node, node) && step.pc == pc
1495 })?;
1496 let scope = if node_index == current_node_index {
1497 StepTargetScope::CurrentNode
1498 } else {
1499 StepTargetScope::SameCodeContext
1500 };
1501
1502 Some(StepTarget { node_index, step_index, scope })
1503}
1504
1505fn find_next_step_target(
1506 arena: &[DebugNode],
1507 current_node_index: usize,
1508 current_step: usize,
1509 mut matches: impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1510) -> Option<(usize, usize)> {
1511 let current_node = arena.get(current_node_index)?;
1512
1513 if let Some(step_index) = current_node
1514 .steps
1515 .iter()
1516 .enumerate()
1517 .skip(current_step.saturating_add(1))
1518 .find_map(|(i, step)| matches(current_node, step).then_some(i))
1519 {
1520 return Some((current_node_index, step_index));
1521 }
1522
1523 for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
1524 if let Some(step_index) = node.steps.iter().position(|step| matches(node, step)) {
1525 return Some((node_index, step_index));
1526 }
1527 }
1528
1529 for (node_index, node) in arena.iter().enumerate().take(current_node_index) {
1530 if let Some(step_index) = node.steps.iter().position(|step| matches(node, step)) {
1531 return Some((node_index, step_index));
1532 }
1533 }
1534
1535 current_node
1536 .steps
1537 .iter()
1538 .enumerate()
1539 .take(current_step.saturating_add(1))
1540 .find_map(|(i, step)| matches(current_node, step).then_some((current_node_index, i)))
1541}
1542
1543fn find_step_target(
1544 arena: &[DebugNode],
1545 current_node_index: usize,
1546 current_step: usize,
1547 mut matches: impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1548) -> Option<StepTarget> {
1549 let current_node = arena.get(current_node_index)?;
1550
1551 if let Some(step_index) = find_step_in_current_node(current_node, current_step, &mut matches) {
1552 return Some(StepTarget {
1553 node_index: current_node_index,
1554 step_index,
1555 scope: StepTargetScope::CurrentNode,
1556 });
1557 }
1558
1559 for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
1560 if same_code_context(current_node, node)
1561 && let Some(step_index) = node.steps.iter().position(|step| matches(node, step))
1562 {
1563 return Some(StepTarget {
1564 node_index,
1565 step_index,
1566 scope: StepTargetScope::SameCodeContext,
1567 });
1568 }
1569 }
1570
1571 for (node_index, node) in arena.iter().enumerate().take(current_node_index).rev() {
1572 if same_code_context(current_node, node)
1573 && let Some(step_index) = node.steps.iter().rposition(|step| matches(node, step))
1574 {
1575 return Some(StepTarget {
1576 node_index,
1577 step_index,
1578 scope: StepTargetScope::SameCodeContext,
1579 });
1580 }
1581 }
1582
1583 None
1584}
1585
1586fn find_step_in_current_node(
1587 node: &DebugNode,
1588 current_step: usize,
1589 matches: &mut impl FnMut(&DebugNode, &CallTraceStep) -> bool,
1590) -> Option<usize> {
1591 if node.steps.get(current_step).is_some_and(|step| matches(node, step)) {
1592 return Some(current_step);
1593 }
1594
1595 node.steps
1596 .iter()
1597 .enumerate()
1598 .skip(current_step.saturating_add(1))
1599 .find_map(|(i, step)| matches(node, step).then_some(i))
1600 .or_else(|| {
1601 node.steps[..current_step.min(node.steps.len())]
1602 .iter()
1603 .enumerate()
1604 .rev()
1605 .find_map(|(i, step)| matches(node, step).then_some(i))
1606 })
1607}
1608
1609fn source_line_range(source: &str, line: usize) -> Option<std::ops::Range<usize>> {
1610 if line == 0 {
1611 return None;
1612 }
1613
1614 let mut start = 0;
1615 for _ in 1..line {
1616 start += source.get(start..)?.find('\n')? + 1;
1617 }
1618 if start >= source.len() {
1619 return None;
1620 }
1621 let end = source[start..].find('\n').map_or(source.len(), |offset| start + offset + 1);
1622 Some(start..end)
1623}
1624
1625fn same_code_context(a: &DebugNode, b: &DebugNode) -> bool {
1626 a.address == b.address && a.kind.is_any_create() == b.kind.is_any_create()
1627}
1628
1629fn pc_exists_outside_code_context(arena: &[DebugNode], current: &DebugNode, pc: usize) -> bool {
1630 arena.iter().any(|node| {
1631 !same_code_context(current, node) && node.steps.iter().any(|step| step.pc == pc)
1632 })
1633}
1634
1635fn find_opcode_match(
1636 arena: &[DebugNode],
1637 current_node_index: usize,
1638 current_step: usize,
1639 query: &str,
1640 direction: SearchDirection,
1641) -> Option<(usize, usize)> {
1642 let needle = query.trim().to_ascii_lowercase();
1643 if needle.is_empty() {
1644 return None;
1645 }
1646
1647 let current_node = arena.get(current_node_index)?;
1648 let trace_node_idx = current_node.trace_node_idx;
1649 let current_absolute_step = current_node.step_offset.saturating_add(current_step);
1650 let steps = || {
1651 arena.iter().enumerate().filter(|(_, node)| node.trace_node_idx == trace_node_idx).flat_map(
1652 |(node_index, node)| {
1653 node.steps.iter().enumerate().map(move |(step_index, step)| {
1654 (node_index, step_index, node.step_offset.saturating_add(step_index), step)
1655 })
1656 },
1657 )
1658 };
1659 let target = match direction {
1660 SearchDirection::Forward => steps()
1661 .filter(|(_, _, absolute_step, _)| *absolute_step > current_absolute_step)
1662 .chain(
1663 steps().filter(|(_, _, absolute_step, _)| *absolute_step <= current_absolute_step),
1664 )
1665 .find(|(_, _, _, step)| pretty_opcode(step).to_ascii_lowercase().contains(&needle)),
1666 SearchDirection::Backward => steps()
1667 .rev()
1668 .filter(|(_, _, absolute_step, _)| *absolute_step < current_absolute_step)
1669 .chain(
1670 steps()
1671 .rev()
1672 .filter(|(_, _, absolute_step, _)| *absolute_step >= current_absolute_step),
1673 )
1674 .find(|(_, _, _, step)| pretty_opcode(step).to_ascii_lowercase().contains(&needle)),
1675 };
1676 target.map(|(node_index, step_index, _, _)| (node_index, step_index))
1677}
1678
1679pub(super) fn pretty_opcode(step: &CallTraceStep) -> String {
1680 let mut buf = String::new();
1681 write_pretty_opcode(&mut buf, step);
1682 buf
1683}
1684
1685pub(super) fn write_pretty_opcode(buf: &mut String, step: &CallTraceStep) {
1686 if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
1687 write!(buf, "{}(0x{})", step.op, hex::encode(immediate)).unwrap();
1688 } else {
1689 write!(buf, "{}", step.op).unwrap();
1690 }
1691}
1692
1693fn memory_write_start_line(step: &CallTraceStep) -> Option<usize> {
1694 let stack = step.stack.as_ref()?;
1695 let access = get_buffer_accesses(step.op.get(), stack)?.write?;
1696 if access.len == 0 {
1697 return None;
1698 }
1699 Some(access.offset / 32)
1700}
1701
1702fn bounded_memory_write_start_line(step: &CallTraceStep, memory_len: usize) -> Option<usize> {
1703 let line = memory_write_start_line(step)?;
1704 (line < memory_len.div_ceil(32)).then_some(line)
1705}
1706
1707fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
1708 if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
1709 return false;
1710 }
1711
1712 let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
1713
1714 step.pc != prev.pc + 1 + immediate_len
1715}
1716
1717#[cfg(test)]
1718mod tests {
1719 use super::*;
1720 use crate::tui::storage::storage_values;
1721 use alloy_primitives::{Bytes, keccak256, map::AddressHashMap};
1722 use foundry_common::slot_identifier::{ENCODING_BYTES, SlotIdentifier};
1723 use foundry_compilers::artifacts::{Storage, StorageLayout, StorageType, sourcemap::Parser};
1724 use foundry_evm_core::{Breakpoints, ic::PcIcMap};
1725 use foundry_evm_traces::debug::{ArtifactData, ContractSources};
1726 use revm::interpreter::InstructionResult;
1727 use revm_inspectors::tracing::types::{StorageChange, StorageChangeReason};
1728 use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
1729
1730 fn step(pc: usize) -> CallTraceStep {
1731 step_with_stack(pc, OpCode::STOP, &[])
1732 }
1733
1734 fn step_with_immediate(pc: usize, op: OpCode, immediate: &'static [u8]) -> CallTraceStep {
1735 CallTraceStep {
1736 immediate_bytes: Some(Bytes::from_static(immediate)),
1737 ..step_with_stack(pc, op, &[])
1738 }
1739 }
1740
1741 fn step_with_stack(pc: usize, op: OpCode, stack: &[usize]) -> CallTraceStep {
1742 CallTraceStep {
1743 pc,
1744 op,
1745 stack: (!stack.is_empty()).then(|| {
1746 stack.iter().copied().map(U256::from).collect::<Vec<_>>().into_boxed_slice()
1747 }),
1748 push_stack: None,
1749 memory: None,
1750 returndata: Bytes::new(),
1751 gas_remaining: 0,
1752 gas_refund_counter: 0,
1753 gas_used: 0,
1754 gas_cost: 0,
1755 state_gas_cost: None,
1756 state_gas_reservoir: None,
1757 state_gas_spent: 0,
1758 storage_change: None,
1759 status: Some(InstructionResult::Stop),
1760 immediate_bytes: None,
1761 decoded: None,
1762 }
1763 }
1764
1765 fn node(address: Address, kind: CallKind, pcs: &[usize]) -> DebugNode {
1766 DebugNode::new(
1767 address,
1768 kind,
1769 pcs.iter().copied().map(step).collect(),
1770 Bytes::new(),
1771 0,
1772 None,
1773 )
1774 }
1775
1776 fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1777 DebuggerContext {
1778 debug_arena: arena,
1779 stats: None,
1780 identified_contracts: Default::default(),
1781 slot_identifiers: None,
1782 contracts_sources: ContractSources::default(),
1783 breakpoints: Breakpoints::default(),
1784 layout: Default::default(),
1785 }
1786 }
1787
1788 fn context_with_source_lines(address: Address) -> DebuggerContext {
1789 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[0, 1, 2])]);
1790 context.identified_contracts.insert(address, "Test".to_string());
1791
1792 let build_id = "test-build".to_string();
1793 context.contracts_sources.sources_by_id.entry(build_id.clone()).or_default().insert(
1794 0,
1795 Arc::new(SourceData {
1796 source: Arc::new("line one\nline two\nline three\n".to_string()),
1797 language: Default::default(),
1798 path: PathBuf::from("src/Test.sol"),
1799 contract_definitions: Vec::new(),
1800 debug_scopes: Vec::new(),
1801 }),
1802 );
1803 context.contracts_sources.artifacts_by_name.insert(
1804 "Test".to_string(),
1805 vec![ArtifactData {
1806 source_map: None,
1807 source_map_runtime: Some(
1808 Parser::new("0:8:0;9:8:0;18:10:0").collect::<Result<_, _>>().unwrap(),
1809 ),
1810 pc_ic_map: None,
1811 pc_ic_map_runtime: Some(PcIcMap::new(&[0x00, 0x00, 0x00])),
1812 build_id,
1813 file_id: 0,
1814 }],
1815 );
1816 context
1817 }
1818
1819 fn key(code: KeyCode) -> KeyEvent {
1820 KeyEvent::new(code, KeyModifiers::empty())
1821 }
1822
1823 fn ctrl_key(code: KeyCode) -> KeyEvent {
1824 KeyEvent::new(code, KeyModifiers::CONTROL)
1825 }
1826
1827 #[test]
1828 fn layout_shortcut_cycles_only_concrete_layouts() {
1829 let address = Address::repeat_byte(1);
1830 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1831 let mut tui = TUIContext::new(&mut context);
1832 tui.init();
1833
1834 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Auto);
1835
1836 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1837 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Horizontal);
1838 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: horizontal");
1839
1840 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1841 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Vertical);
1842 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: vertical");
1843
1844 let _ = tui.handle_key_event(key(KeyCode::Char('l')));
1845 assert_eq!(tui.debugger_context.layout, DebuggerLayout::Horizontal);
1846 assert_eq!(tui.status.as_ref().unwrap().text, "Debugger layout: horizontal");
1847 }
1848
1849 #[test]
1850 fn view_shortcuts_report_status() {
1851 let address = Address::repeat_byte(1);
1852 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1853 let mut tui = TUIContext::new(&mut context);
1854 tui.init();
1855
1856 assert_eq!(tui.active_buffer, BufferKind::Memory);
1857 let _ = tui.handle_key_event(key(KeyCode::Char('b')));
1858 assert_eq!(tui.active_buffer, BufferKind::Calldata);
1859 assert_eq!(tui.status.as_ref().unwrap().text, "Active buffer: calldata");
1860
1861 let _ = tui.handle_key_event(key(KeyCode::Char('t')));
1862 assert!(tui.stack_labels);
1863 assert_eq!(tui.status.as_ref().unwrap().text, "Stack labels: on");
1864 let _ = tui.handle_key_event(key(KeyCode::Char('t')));
1865 assert!(!tui.stack_labels);
1866 assert_eq!(tui.status.as_ref().unwrap().text, "Stack labels: off");
1867
1868 let _ = tui.handle_key_event(key(KeyCode::Char('m')));
1869 assert!(tui.buf_utf);
1870 assert_eq!(tui.status.as_ref().unwrap().text, "UTF-8 decoding: on");
1871 let _ = tui.handle_key_event(key(KeyCode::Char('m')));
1872 assert!(!tui.buf_utf);
1873 assert_eq!(tui.status.as_ref().unwrap().text, "UTF-8 decoding: off");
1874
1875 let _ = tui.handle_key_event(key(KeyCode::Char('h')));
1876 assert!(!tui.show_shortcuts);
1877 assert_eq!(tui.status.as_ref().unwrap().text, "Shortcut help: hidden");
1878 let _ = tui.handle_key_event(key(KeyCode::Char('h')));
1879 assert!(tui.show_shortcuts);
1880 assert_eq!(tui.status.as_ref().unwrap().text, "Shortcut help: shown");
1881 }
1882
1883 #[test]
1884 fn previous_call_shortcut_respects_root_boundary() {
1885 let address = Address::repeat_byte(1);
1886 let mut context = context_with_arena(vec![
1887 node(address, CallKind::Call, &[1, 2]),
1888 node(address, CallKind::Call, &[3]),
1889 ]);
1890 let mut tui = TUIContext::new(&mut context);
1891 tui.init();
1892
1893 let _ = tui.handle_key_event(key(KeyCode::Char('c')));
1894 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (0, 0));
1895
1896 tui.draw_memory.inner_call_index = 1;
1897 let _ = tui.handle_key_event(key(KeyCode::Char('c')));
1898 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (0, 1));
1899 }
1900
1901 #[test]
1902 fn breakpoint_shortcut_cycles_trace_hits() {
1903 let address = Address::repeat_byte(1);
1904 let other = Address::repeat_byte(2);
1905 let mut context = context_with_arena(vec![
1906 node(other, CallKind::Call, &[1]),
1907 node(address, CallKind::Call, &[42, 7, 42]),
1908 node(address, CallKind::Call, &[8, 42]),
1909 ]);
1910 context.breakpoints.insert('a', (address, 42));
1911 let mut tui = TUIContext::new(&mut context);
1912 tui.init();
1913
1914 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1915 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1916
1917 assert_eq!(tui.draw_memory.inner_call_index, 1);
1918 assert_eq!(tui.current_step, 0);
1919 let status = tui.status.as_ref().unwrap();
1920 assert_eq!(status.kind, StatusKind::Info);
1921 assert_eq!(status.text, "Jumped to breakpoint 'a' at PC 0x2a (42)");
1922
1923 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1924 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1925 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (1, 2));
1926
1927 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1928 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1929 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (2, 1));
1930
1931 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1932 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1933 assert_eq!((tui.draw_memory.inner_call_index, tui.current_step), (1, 0));
1934 }
1935
1936 #[test]
1937 fn breakpoint_shortcut_reports_missing_key() {
1938 let address = Address::repeat_byte(1);
1939 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1940 let mut tui = TUIContext::new(&mut context);
1941 tui.init();
1942
1943 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1944 let _ = tui.handle_key_event(key(KeyCode::Char('z')));
1945
1946 assert_eq!(tui.draw_memory.inner_call_index, 0);
1947 assert_eq!(tui.current_step, 0);
1948 let status = tui.status.as_ref().unwrap();
1949 assert_eq!(status.kind, StatusKind::Error);
1950 assert_eq!(status.text, "Breakpoint 'z' not found");
1951 }
1952
1953 #[test]
1954 fn breakpoint_shortcut_reports_missing_trace_target() {
1955 let address = Address::repeat_byte(1);
1956 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
1957 context.breakpoints.insert('a', (address, 42));
1958 let mut tui = TUIContext::new(&mut context);
1959 tui.init();
1960
1961 let _ = tui.handle_key_event(key(KeyCode::Char('\'')));
1962 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
1963
1964 assert_eq!(tui.draw_memory.inner_call_index, 0);
1965 assert_eq!(tui.current_step, 0);
1966 let status = tui.status.as_ref().unwrap();
1967 assert_eq!(status.kind, StatusKind::Error);
1968 assert_eq!(status.text, "Breakpoint 'a' target not found in trace");
1969 }
1970
1971 #[test]
1972 fn parses_prefixed_hex_pc() {
1973 assert_eq!(
1974 parse_pc_candidates("0x2a").unwrap(),
1975 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1976 );
1977 assert_eq!(
1978 parse_pc_candidates("0X2A").unwrap(),
1979 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1980 );
1981 }
1982
1983 #[test]
1984 fn parses_bare_hex_pc_with_letters() {
1985 assert_eq!(
1986 parse_pc_candidates("2a").unwrap(),
1987 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
1988 );
1989 }
1990
1991 #[test]
1992 fn parses_explicit_decimal_pc() {
1993 assert_eq!(
1994 parse_pc_candidates("d:42").unwrap(),
1995 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
1996 );
1997 assert_eq!(
1998 parse_pc_candidates("dec:42").unwrap(),
1999 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
2000 );
2001 }
2002
2003 #[test]
2004 fn parses_bare_digits_as_decimal_and_hex_candidates() {
2005 assert_eq!(
2006 parse_pc_candidates("10").unwrap(),
2007 vec![
2008 PcCandidate { pc: 10, base: PcBase::Decimal },
2009 PcCandidate { pc: 16, base: PcBase::Hex },
2010 ]
2011 );
2012 assert_eq!(
2013 parse_pc_candidates("9").unwrap(),
2014 vec![PcCandidate { pc: 9, base: PcBase::Decimal }]
2015 );
2016 }
2017
2018 #[test]
2019 fn rejects_invalid_pc_input() {
2020 assert!(parse_pc_candidates("").is_err());
2021 assert!(parse_pc_candidates("0x").is_err());
2022 assert!(parse_pc_candidates("xyz").is_err());
2023 assert!(parse_pc_candidates("184467440737095516160").is_err());
2024 }
2025
2026 #[test]
2027 fn parses_buffer_offsets_as_visible_hex_labels() {
2028 assert_eq!(parse_buffer_offset("0x20").unwrap(), 32);
2029 assert_eq!(parse_buffer_offset("d:32").unwrap(), 32);
2030 assert_eq!(parse_buffer_offset("dec:32").unwrap(), 32);
2031 assert_eq!(parse_buffer_offset("20").unwrap(), 32);
2032 assert_eq!(parse_buffer_offset("2a").unwrap(), 42);
2033 assert_eq!(parse_buffer_offset("a").unwrap(), 10);
2034
2035 assert_eq!(parse_buffer_offset("").unwrap_err(), "Enter a buffer offset");
2036 assert_eq!(
2037 parse_buffer_offset("0x").unwrap_err(),
2038 "Invalid buffer offset `0x`; use hex 0x20/20 or decimal d:32"
2039 );
2040 assert_eq!(
2041 parse_buffer_offset("2x3").unwrap_err(),
2042 "Invalid buffer offset `2x3`; use hex 0x20/20 or decimal d:32"
2043 );
2044 }
2045
2046 #[test]
2047 fn parses_storage_slots_as_visible_hex_labels() {
2048 assert_eq!(parse_storage_slot("0x20").unwrap(), U256::from(32));
2049 assert_eq!(parse_storage_slot("d:32").unwrap(), U256::from(32));
2050 assert_eq!(parse_storage_slot("dec:32").unwrap(), U256::from(32));
2051 assert_eq!(parse_storage_slot("20").unwrap(), U256::from(32));
2052 assert_eq!(parse_storage_slot("2a").unwrap(), U256::from(42));
2053 assert_eq!(parse_storage_slot("a").unwrap(), U256::from(10));
2054
2055 assert_eq!(parse_storage_slot("").unwrap_err(), "Enter a storage slot");
2056 assert_eq!(
2057 parse_storage_slot("0x").unwrap_err(),
2058 "Invalid storage slot `0x`; use hex 0x20/20 or decimal d:32"
2059 );
2060 assert_eq!(
2061 parse_storage_slot("2x3").unwrap_err(),
2062 "Invalid storage slot `2x3`; use hex 0x20/20 or decimal d:32"
2063 );
2064 assert_eq!(
2065 parse_storage_slot("1_0").unwrap_err(),
2066 "Invalid storage slot `1_0`; use hex 0x20/20 or decimal d:32"
2067 );
2068 assert_eq!(
2069 parse_storage_slot("_").unwrap_err(),
2070 "Invalid storage slot `_`; use hex 0x20/20 or decimal d:32"
2071 );
2072 assert_eq!(
2073 parse_storage_slot("0x_").unwrap_err(),
2074 "Invalid storage slot `0x_`; use hex 0x20/20 or decimal d:32"
2075 );
2076 assert_eq!(
2077 parse_storage_slot("d:_").unwrap_err(),
2078 "Invalid storage slot `d:_`; use hex 0x20/20 or decimal d:32"
2079 );
2080 }
2081
2082 #[test]
2083 fn filters_buffer_offset_input_to_parser_prefixes() {
2084 assert!(is_buffer_offset_input_char("", '0'));
2085 assert!(is_buffer_offset_input_char("0", 'x'));
2086 assert!(is_buffer_offset_input_char("0x", '2'));
2087 assert!(is_buffer_offset_input_char("2", 'a'));
2088 assert!(is_buffer_offset_input_char("d", ':'));
2089 assert!(is_buffer_offset_input_char("dec", ':'));
2090 assert!(is_buffer_offset_input_char("dec:", '3'));
2091
2092 assert!(!is_buffer_offset_input_char("", 'x'));
2093 assert!(!is_buffer_offset_input_char("2", 'x'));
2094 assert!(!is_buffer_offset_input_char("1", ':'));
2095 assert!(!is_buffer_offset_input_char("DEC", ':'));
2096 assert!(!is_buffer_offset_input_char("d:", 'a'));
2097 }
2098
2099 #[test]
2100 fn finds_pc_in_current_node() {
2101 let address = Address::repeat_byte(1);
2102 let arena = vec![node(address, CallKind::Call, &[1, 2, 3])];
2103
2104 assert_eq!(
2105 find_pc_target(&arena, 0, 0, 3),
2106 Some(StepTarget { node_index: 0, step_index: 2, scope: StepTargetScope::CurrentNode })
2107 );
2108 }
2109
2110 #[test]
2111 fn repeated_pc_stays_current_then_prefers_next_then_previous() {
2112 let address = Address::repeat_byte(1);
2113 let arena = vec![node(address, CallKind::Call, &[1, 2, 3, 2])];
2114
2115 assert_eq!(find_pc_target(&arena, 0, 1, 2).unwrap().step_index, 1);
2116 assert_eq!(find_pc_target(&arena, 0, 0, 2).unwrap().step_index, 1);
2117 assert_eq!(find_pc_target(&arena, 0, 3, 2).unwrap().step_index, 3);
2118 assert_eq!(find_pc_target(&arena, 0, 2, 2).unwrap().step_index, 3);
2119 }
2120
2121 #[test]
2122 fn searches_later_then_earlier_same_code_context() {
2123 let address = Address::repeat_byte(1);
2124 let arena = vec![
2125 node(address, CallKind::Call, &[1]),
2126 node(address, CallKind::Call, &[2]),
2127 node(address, CallKind::Call, &[3]),
2128 ];
2129
2130 assert_eq!(find_pc_target(&arena, 1, 0, 3).unwrap().node_index, 2);
2131 assert_eq!(find_pc_target(&arena, 1, 0, 1).unwrap().node_index, 0);
2132 }
2133
2134 #[test]
2135 fn does_not_search_different_address_or_creation_context() {
2136 let address = Address::repeat_byte(1);
2137 let other = Address::repeat_byte(2);
2138 let arena = vec![
2139 node(address, CallKind::Call, &[1]),
2140 node(other, CallKind::Call, &[2]),
2141 node(address, CallKind::Create, &[3]),
2142 ];
2143
2144 assert!(find_pc_target(&arena, 0, 0, 2).is_none());
2145 assert!(find_pc_target(&arena, 0, 0, 3).is_none());
2146 assert!(pc_exists_outside_code_context(&arena, &arena[0], 2));
2147 assert!(pc_exists_outside_code_context(&arena, &arena[0], 3));
2148 }
2149
2150 #[test]
2151 fn goto_resolves_unambiguous_bare_digits_and_reports_ambiguity() {
2152 let address = Address::repeat_byte(1);
2153 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[10, 16, 42])]);
2154 let mut tui = TUIContext::new(&mut context);
2155 tui.init();
2156
2157 tui.goto_pc_from_input("2a");
2158 assert_eq!(tui.current_step, 2);
2159 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2160
2161 tui.current_step = 0;
2162 tui.goto_pc_from_input("10");
2163 assert_eq!(tui.current_step, 0);
2164 assert!(tui.status.as_ref().unwrap().text.contains("Ambiguous PC"));
2165
2166 tui.goto_pc_from_input("d:10");
2167 assert_eq!(tui.current_step, 0);
2168 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2169 }
2170
2171 #[test]
2172 fn goto_reports_pc_in_other_contract_without_moving() {
2173 let address = Address::repeat_byte(1);
2174 let other = Address::repeat_byte(2);
2175 let mut context = context_with_arena(vec![
2176 node(address, CallKind::Call, &[1]),
2177 node(other, CallKind::Call, &[42]),
2178 ]);
2179 let mut tui = TUIContext::new(&mut context);
2180 tui.init();
2181
2182 tui.goto_pc_from_input("2a");
2183 assert_eq!(tui.draw_memory.inner_call_index, 0);
2184 assert_eq!(tui.current_step, 0);
2185 let status = tui.status.as_ref().unwrap();
2186 assert_eq!(status.kind, StatusKind::Error);
2187 assert!(status.text.contains("exists in another contract"));
2188 }
2189
2190 #[test]
2191 fn goto_reports_ambiguous_input_in_other_contract_without_choosing_first_candidate() {
2192 let address = Address::repeat_byte(1);
2193 let other = Address::repeat_byte(2);
2194 let mut context = context_with_arena(vec![
2195 node(address, CallKind::Call, &[1]),
2196 node(other, CallKind::Call, &[16]),
2197 ]);
2198 let mut tui = TUIContext::new(&mut context);
2199 tui.init();
2200
2201 tui.goto_pc_from_input("10");
2202 let status = tui.status.as_ref().unwrap();
2203 assert_eq!(status.kind, StatusKind::Error);
2204 assert!(status.text.starts_with("PC `10` not found"));
2205 assert!(status.text.contains("exists in another contract"));
2206 }
2207
2208 #[test]
2209 fn prompts_preserve_character_filters_and_block_navigation() {
2210 for (shortcut, expected) in [
2211 ('p', Prompt::Pc("a".into())),
2212 ('o', Prompt::BufferOffset("a".into())),
2213 (':', Prompt::Command("aé".into())),
2214 ('/', Prompt::OpcodeSearch("aé\n".into())),
2215 ] {
2216 let address = Address::repeat_byte(1);
2217 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2218 let mut tui = TUIContext::new(&mut context);
2219 tui.init();
2220
2221 let _ = tui.handle_key_event(key(KeyCode::Char(shortcut)));
2222 for c in ['a', 'é', '\n'] {
2223 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2224 }
2225 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('c')));
2226 let _ = tui.handle_key_event(key(KeyCode::Down));
2227 let _ = tui.handle_mouse_event(MouseEvent {
2228 kind: MouseEventKind::ScrollDown,
2229 column: 0,
2230 row: 0,
2231 modifiers: KeyModifiers::empty(),
2232 });
2233 assert_eq!(tui.prompt, Some(expected));
2234 assert_eq!(tui.current_step, 0);
2235
2236 let _ = tui.handle_key_event(key(KeyCode::Esc));
2237 assert_eq!(tui.prompt, None);
2238 assert_eq!(tui.current_step, 0);
2239 assert_eq!(tui.status, None);
2240 let _ = tui.handle_key_event(key(KeyCode::Down));
2241 assert_eq!(tui.current_step, 1);
2242 }
2243 }
2244
2245 #[test]
2246 fn pc_input_mode_handles_keys_and_blocks_normal_commands() {
2247 let address = Address::repeat_byte(1);
2248 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2249 let mut tui = TUIContext::new(&mut context);
2250 tui.init();
2251
2252 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('p'))), ControlFlow::Continue(())));
2253 assert_eq!(tui.prompt, Some(Prompt::Pc(String::new())));
2254
2255 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2256 assert_eq!(tui.prompt, Some(Prompt::Pc(String::new())));
2257 assert_eq!(tui.current_step, 0);
2258
2259 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2260 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
2261 assert_eq!(tui.prompt, Some(Prompt::Pc("2a".into())));
2262
2263 let _ = tui.handle_key_event(key(KeyCode::Backspace));
2264 assert_eq!(tui.prompt, Some(Prompt::Pc("2".into())));
2265 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
2266 let _ = tui.handle_key_event(key(KeyCode::Enter));
2267
2268 assert_eq!(tui.prompt, None);
2269 assert_eq!(tui.current_step, 1);
2270 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2271 }
2272
2273 #[test]
2274 fn pc_input_escape_cancels_without_moving() {
2275 let address = Address::repeat_byte(1);
2276 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2277 let mut tui = TUIContext::new(&mut context);
2278 tui.init();
2279
2280 let _ = tui.handle_key_event(key(KeyCode::Char('p')));
2281 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2282 let _ = tui.handle_key_event(key(KeyCode::Esc));
2283
2284 assert_eq!(tui.prompt, None);
2285 assert_eq!(tui.current_step, 0);
2286 assert_eq!(tui.status, None);
2287 }
2288
2289 #[test]
2290 fn command_input_mode_handles_keys_and_blocks_normal_commands() {
2291 let address = Address::repeat_byte(1);
2292 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2293 let mut tui = TUIContext::new(&mut context);
2294 tui.init();
2295
2296 assert!(matches!(tui.handle_key_event(key(KeyCode::Char(':'))), ControlFlow::Continue(())));
2297 assert_eq!(tui.prompt, Some(Prompt::Command(String::new())));
2298
2299 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2300 assert_eq!(tui.prompt, Some(Prompt::Command("q".into())));
2301 assert_eq!(tui.current_step, 0);
2302
2303 let _ = tui.handle_key_event(key(KeyCode::Backspace));
2304 for c in "continue 2a".chars() {
2305 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2306 }
2307 let _ = tui.handle_key_event(key(KeyCode::Enter));
2308
2309 assert_eq!(tui.prompt, None);
2310 assert_eq!(tui.current_step, 1);
2311 let status = tui.status.as_ref().unwrap();
2312 assert_eq!(status.kind, StatusKind::Info);
2313 assert_eq!(status.text, "Jumped to PC 0x2a (42) in current trace");
2314 }
2315
2316 #[test]
2317 fn command_prompt_jumps_to_named_buffer_offset() {
2318 let address = Address::repeat_byte(1);
2319 let mut context = context_with_arena(vec![DebugNode::new(
2320 address,
2321 CallKind::Call,
2322 vec![step(1)],
2323 Bytes::from(vec![0; 96]),
2324 0,
2325 None,
2326 )]);
2327 let mut tui = TUIContext::new(&mut context);
2328 tui.init();
2329
2330 tui.run_command_from_input("calldata 40");
2331
2332 assert_eq!(tui.active_buffer, BufferKind::Calldata);
2333 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2334 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x40 (64)");
2335 }
2336
2337 #[test]
2338 fn labels_long_string_data_slots_written_before_length() {
2339 let address = Address::repeat_byte(1);
2340 let type_id = "t_string_storage".to_string();
2341 let identifier = SlotIdentifier::new(Arc::new(StorageLayout {
2342 storage: vec![Storage {
2343 ast_id: 1,
2344 contract: "StorageTest".to_string(),
2345 label: "text".to_string(),
2346 offset: 0,
2347 slot: "0".to_string(),
2348 storage_type: type_id.clone(),
2349 }],
2350 types: BTreeMap::from([(
2351 type_id,
2352 StorageType {
2353 encoding: ENCODING_BYTES.to_string(),
2354 key: None,
2355 label: "string".to_string(),
2356 number_of_bytes: "32".to_string(),
2357 value: None,
2358 other: BTreeMap::new(),
2359 },
2360 )]),
2361 }));
2362 let data_slot = U256::from_be_bytes(keccak256(B256::ZERO).0);
2363 let mut data_access = step(1);
2364 data_access.storage_change = Some(Box::new(StorageChange {
2365 key: data_slot,
2366 value: U256::ZERO,
2367 had_value: None,
2368 reason: StorageChangeReason::SSTORE,
2369 }));
2370 let mut base_access = step(2);
2371 base_access.storage_change = Some(Box::new(StorageChange {
2372 key: U256::ZERO,
2373 value: U256::from(40 * 2 + 1),
2374 had_value: None,
2375 reason: StorageChangeReason::SSTORE,
2376 }));
2377 let mut context = context_with_arena(vec![DebugNode::new(
2378 address,
2379 CallKind::Call,
2380 vec![data_access, base_access],
2381 Bytes::new(),
2382 0,
2383 None,
2384 )]);
2385 context.slot_identifiers = Some(AddressHashMap::from_iter([(address, identifier)]));
2386 let mut tui = TUIContext::new(&mut context);
2387 tui.current_step = 0;
2388 let accesses = tui.storage_accesses(StorageSpace::Persistent);
2389 let values = storage_values(&accesses);
2390 let next_values = tui.next_storage_write_values();
2391
2392 assert_eq!(
2393 tui.storage_label(
2394 StorageSpace::Persistent,
2395 data_slot,
2396 Some(&values),
2397 Some(&next_values),
2398 )
2399 .as_deref(),
2400 Some("text[0]")
2401 );
2402 }
2403
2404 #[test]
2405 fn command_prompt_jumps_to_storage_slot_access() {
2406 let address = Address::repeat_byte(1);
2407 let mut first_store = step(2);
2408 first_store.storage_change = Some(Box::new(StorageChange {
2409 key: U256::ZERO,
2410 value: U256::from(7),
2411 had_value: None,
2412 reason: StorageChangeReason::SSTORE,
2413 }));
2414 let mut store = step(42);
2415 store.storage_change = Some(Box::new(StorageChange {
2416 key: U256::from(1),
2417 value: U256::from(42),
2418 had_value: Some(U256::from(7)),
2419 reason: StorageChangeReason::SSTORE,
2420 }));
2421 let mut context = context_with_arena(vec![DebugNode::new(
2422 address,
2423 CallKind::Call,
2424 vec![step(1), first_store, store],
2425 Bytes::new(),
2426 0,
2427 None,
2428 )]);
2429 let mut tui = TUIContext::new(&mut context);
2430 tui.init();
2431
2432 tui.run_command_from_input("storage 1");
2433
2434 assert_eq!(tui.current_step, 2);
2435 assert_eq!(tui.active_storage, Some(StorageSpace::Persistent));
2436 assert_eq!(tui.storage_accesses(StorageSpace::Persistent).len(), 2);
2437 assert_eq!(
2438 tui.status.as_ref().unwrap().text,
2439 "Jumped to storage SSTORE slot 0x1: 0x7 -> 0x2a at PC 0x2a (42)"
2440 );
2441 }
2442
2443 #[test]
2444 fn command_prompt_jumps_to_transient_storage_slot_access() {
2445 let address = Address::repeat_byte(1);
2446 let steps = vec![step(1), step_with_stack(42, OpCode::TSTORE, &[0xbeef, 0x2a])];
2447 let mut context = context_with_arena(vec![DebugNode::new(
2448 address,
2449 CallKind::Call,
2450 steps,
2451 Bytes::new(),
2452 0,
2453 None,
2454 )]);
2455 let mut tui = TUIContext::new(&mut context);
2456 tui.init();
2457
2458 tui.run_command_from_input("transient 2a");
2459
2460 assert_eq!(tui.current_step, 1);
2461 assert_eq!(tui.active_storage, Some(StorageSpace::Transient));
2462 assert_eq!(
2463 tui.status.as_ref().unwrap().text,
2464 "Jumped to transient storage TSTORE slot 0x2a = 0xbeef at PC 0x2a (42)"
2465 );
2466
2467 tui.run_command_from_input("storage 2a");
2468 assert_eq!(tui.current_step, 1);
2469 assert_eq!(
2470 tui.status.as_ref().unwrap().text,
2471 "Storage slot 0x2a not accessed in current call"
2472 );
2473 }
2474
2475 #[test]
2476 fn command_prompt_searches_storage_across_split_call_segments() {
2477 let address = Address::repeat_byte(1);
2478 let mut store = step(42);
2479 store.storage_change = Some(Box::new(StorageChange {
2480 key: U256::from(1),
2481 value: U256::from(42),
2482 had_value: None,
2483 reason: StorageChangeReason::SSTORE,
2484 }));
2485
2486 let mut first_store = step(1);
2487 first_store.storage_change = Some(Box::new(StorageChange {
2488 key: U256::ZERO,
2489 value: U256::from(7),
2490 had_value: None,
2491 reason: StorageChangeReason::SSTORE,
2492 }));
2493 let mut first =
2494 DebugNode::new(address, CallKind::Call, vec![first_store], Bytes::new(), 0, None);
2495 first.trace_node_idx = 7;
2496 first.step_offset = 0;
2497
2498 let mut child_store = step(2);
2499 child_store.storage_change = Some(Box::new(StorageChange {
2500 key: U256::from(9),
2501 value: U256::from(99),
2502 had_value: None,
2503 reason: StorageChangeReason::SSTORE,
2504 }));
2505 let mut child = DebugNode::new(
2506 Address::repeat_byte(2),
2507 CallKind::Call,
2508 vec![child_store],
2509 Bytes::new(),
2510 0,
2511 None,
2512 );
2513 child.trace_node_idx = 8;
2514 child.step_offset = 1;
2515
2516 let mut second =
2517 DebugNode::new(address, CallKind::Call, vec![store], Bytes::new(), 0, None);
2518 second.trace_node_idx = 7;
2519 second.step_offset = 2;
2520
2521 let mut context = context_with_arena(vec![first, child, second]);
2522 let mut tui = TUIContext::new(&mut context);
2523 tui.init();
2524
2525 tui.run_command_from_input("storage 1");
2526
2527 assert_eq!(tui.draw_memory.inner_call_index, 2);
2528 assert_eq!(tui.current_step, 0);
2529 let accesses = tui.storage_accesses(StorageSpace::Persistent);
2530 assert_eq!(accesses.len(), 2);
2531 assert!(!accesses.contains_key(&U256::from(9)));
2532 assert_eq!(
2533 tui.status.as_ref().unwrap().text,
2534 "Jumped to storage SSTORE slot 0x1 = 0x2a at PC 0x2a (42)"
2535 );
2536 }
2537
2538 #[test]
2539 fn command_prompt_finds_warm_sload_from_stack_snapshots() {
2540 let address = Address::repeat_byte(1);
2541 let steps =
2542 vec![step_with_stack(1, OpCode::SLOAD, &[1]), step_with_stack(2, OpCode::STOP, &[42])];
2543 let mut context = context_with_arena(vec![DebugNode::new(
2544 address,
2545 CallKind::Call,
2546 steps,
2547 Bytes::new(),
2548 0,
2549 None,
2550 )]);
2551 let mut tui = TUIContext::new(&mut context);
2552 tui.init();
2553
2554 tui.run_command_from_input("store 1");
2555
2556 assert_eq!(tui.current_step, 0);
2557 assert_eq!(
2558 tui.status.as_ref().unwrap().text,
2559 "Jumped to storage SLOAD slot 0x1 = 0x2a at PC 0x1 (1)"
2560 );
2561 }
2562
2563 #[test]
2564 fn command_prompt_finds_warm_sstore_from_stack_snapshot() {
2565 let address = Address::repeat_byte(1);
2566 let steps = vec![step_with_stack(42, OpCode::SSTORE, &[42, 1])];
2567 let mut context = context_with_arena(vec![DebugNode::new(
2568 address,
2569 CallKind::Call,
2570 steps,
2571 Bytes::new(),
2572 0,
2573 None,
2574 )]);
2575 let mut tui = TUIContext::new(&mut context);
2576 tui.init();
2577
2578 tui.run_command_from_input("slot 1");
2579
2580 assert_eq!(tui.current_step, 0);
2581 assert_eq!(
2582 tui.status.as_ref().unwrap().text,
2583 "Jumped to storage SSTORE slot 0x1 = 0x2a at PC 0x2a (42)"
2584 );
2585 }
2586
2587 #[test]
2588 fn command_prompt_ignores_failed_sstore_stack_snapshot() {
2589 let address = Address::repeat_byte(1);
2590 let mut store = step_with_stack(42, OpCode::SSTORE, &[42, 1]);
2591 store.status = Some(InstructionResult::StateChangeDuringStaticCall);
2592 let mut context = context_with_arena(vec![DebugNode::new(
2593 address,
2594 CallKind::Call,
2595 vec![store],
2596 Bytes::new(),
2597 0,
2598 None,
2599 )]);
2600 let mut tui = TUIContext::new(&mut context);
2601 tui.init();
2602
2603 tui.run_command_from_input("slot 1");
2604
2605 assert_eq!(tui.current_step, 0);
2606 let status = tui.status.as_ref().unwrap();
2607 assert_eq!(status.kind, StatusKind::Error);
2608 assert_eq!(status.text, "Storage slot 0x1 not accessed in current call");
2609 }
2610
2611 #[test]
2612 fn command_prompt_ignores_failed_sstore_storage_change() {
2613 let address = Address::repeat_byte(1);
2614 let mut store = step_with_stack(42, OpCode::SSTORE, &[42, 1]);
2615 store.storage_change = Some(Box::new(StorageChange {
2616 key: U256::from(1),
2617 value: U256::from(42),
2618 had_value: Some(U256::ZERO),
2619 reason: StorageChangeReason::SSTORE,
2620 }));
2621 store.status = Some(InstructionResult::OutOfGas);
2622 let mut context = context_with_arena(vec![DebugNode::new(
2623 address,
2624 CallKind::Call,
2625 vec![store],
2626 Bytes::new(),
2627 0,
2628 None,
2629 )]);
2630 let mut tui = TUIContext::new(&mut context);
2631 tui.init();
2632
2633 tui.run_command_from_input("slot 1");
2634
2635 assert_eq!(tui.current_step, 0);
2636 let status = tui.status.as_ref().unwrap();
2637 assert_eq!(status.kind, StatusKind::Error);
2638 assert_eq!(status.text, "Storage slot 0x1 not accessed in current call");
2639 }
2640
2641 #[test]
2642 fn command_prompt_accepts_optional_leading_colon() {
2643 let address = Address::repeat_byte(1);
2644 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
2645 let mut tui = TUIContext::new(&mut context);
2646 tui.init();
2647
2648 tui.run_command_from_input(":pc 2a");
2649
2650 assert_eq!(tui.current_step, 1);
2651 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to PC 0x2a (42) in current trace");
2652 }
2653
2654 #[test]
2655 fn command_prompt_continues_to_next_pc_hit() {
2656 let address = Address::repeat_byte(1);
2657 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42, 2, 42])]);
2658 let mut tui = TUIContext::new(&mut context);
2659 tui.init();
2660 tui.current_step = 1;
2661
2662 tui.run_command_from_input("pc 2a");
2663 assert_eq!(tui.current_step, 1);
2664
2665 tui.run_command_from_input("continue 2a");
2666 assert_eq!(tui.current_step, 3);
2667 }
2668
2669 #[test]
2670 fn command_prompt_jumps_to_source_line() {
2671 let address = Address::repeat_byte(1);
2672 let mut context = context_with_source_lines(address);
2673 let mut tui = TUIContext::new(&mut context);
2674 tui.init();
2675
2676 tui.run_command_from_input("line 2");
2677
2678 assert_eq!(tui.current_step, 1);
2679 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to src/Test.sol:2 at PC 0x1 (1)");
2680 }
2681
2682 #[test]
2683 fn command_prompt_reports_help_and_usage_errors() {
2684 let address = Address::repeat_byte(1);
2685 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2686 let mut tui = TUIContext::new(&mut context);
2687 tui.init();
2688
2689 tui.run_command_from_input("help");
2690 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
2691 let help = &tui.status.as_ref().unwrap().text;
2692 for commands in [
2693 CONTINUE_COMMANDS,
2694 PC_COMMANDS,
2695 MEMORY_COMMANDS,
2696 CALLDATA_COMMANDS,
2697 RETURNDATA_COMMANDS,
2698 STORAGE_COMMANDS,
2699 TRANSIENT_STORAGE_COMMANDS,
2700 LINE_COMMANDS,
2701 OPCODE_COMMANDS,
2702 SOURCE_COMMANDS,
2703 VARIABLES_COMMANDS,
2704 STACK_COMMANDS,
2705 DATA_COMMANDS,
2706 ] {
2707 assert!(help.contains(&command_aliases(commands)));
2708 }
2709
2710 tui.run_command_from_input("mem");
2711 let status = tui.status.as_ref().unwrap();
2712 assert_eq!(status.kind, StatusKind::Info);
2713 assert_eq!(status.text, "Active buffer: memory");
2714
2715 tui.run_command_from_input("store");
2716 let status = tui.status.as_ref().unwrap();
2717 assert_eq!(status.kind, StatusKind::Info);
2718 assert_eq!(status.text, "Active data: storage");
2719
2720 tui.run_command_from_input("store 1 2");
2721 let status = tui.status.as_ref().unwrap();
2722 assert_eq!(status.kind, StatusKind::Error);
2723 assert_eq!(status.text, "Usage: :store <slot>");
2724
2725 tui.run_command_from_input("line");
2726 let status = tui.status.as_ref().unwrap();
2727 assert_eq!(status.kind, StatusKind::Error);
2728 assert_eq!(status.text, "Usage: :line <line>");
2729 }
2730
2731 #[test]
2732 fn command_prompt_toggles_panes() {
2733 let address = Address::repeat_byte(1);
2734 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2735 let mut tui = TUIContext::new(&mut context);
2736 tui.init();
2737
2738 tui.run_command_from_input("opcodes");
2739 assert!(!tui.show_opcodes);
2740 assert_eq!(tui.status.as_ref().unwrap().text, "Opcodes pane: hidden");
2741
2742 tui.run_command_from_input(":ops");
2743 assert!(tui.show_opcodes);
2744 assert_eq!(tui.status.as_ref().unwrap().text, "Opcodes pane: shown");
2745
2746 tui.run_command_from_input("source");
2747 assert!(!tui.show_source);
2748 assert_eq!(tui.status.as_ref().unwrap().text, "Source pane: hidden");
2749
2750 tui.run_command_from_input(":src");
2751 assert!(tui.show_source);
2752 assert_eq!(tui.status.as_ref().unwrap().text, "Source pane: shown");
2753
2754 tui.run_command_from_input("variables");
2755 assert!(!tui.show_variables);
2756 assert_eq!(tui.status.as_ref().unwrap().text, "Variables pane: hidden");
2757
2758 tui.run_command_from_input(":vars");
2759 assert!(tui.show_variables);
2760 assert_eq!(tui.status.as_ref().unwrap().text, "Variables pane: shown");
2761
2762 tui.run_command_from_input("stack");
2763 assert!(!tui.show_stack);
2764 assert_eq!(tui.status.as_ref().unwrap().text, "Stack pane: hidden");
2765
2766 tui.run_command_from_input(":stack");
2767 assert!(tui.show_stack);
2768 assert_eq!(tui.status.as_ref().unwrap().text, "Stack pane: shown");
2769
2770 tui.run_command_from_input("data");
2771 assert!(!tui.show_data);
2772 assert_eq!(tui.status.as_ref().unwrap().text, "Data pane: hidden");
2773
2774 tui.run_command_from_input(":data");
2775 assert!(tui.show_data);
2776 assert_eq!(tui.status.as_ref().unwrap().text, "Data pane: shown");
2777
2778 tui.run_command_from_input("stack extra");
2779 let status = tui.status.as_ref().unwrap();
2780 assert_eq!(status.kind, StatusKind::Error);
2781 assert_eq!(status.text, "Usage: :stack");
2782 }
2783
2784 #[test]
2785 fn buffer_offset_input_mode_handles_calldata_offsets_and_blocks_normal_commands() {
2786 let address = Address::repeat_byte(1);
2787 let mut context = context_with_arena(vec![DebugNode::new(
2788 address,
2789 CallKind::Call,
2790 vec![step(1)],
2791 Bytes::from(vec![0; 96]),
2792 0,
2793 None,
2794 )]);
2795 let mut tui = TUIContext::new(&mut context);
2796 tui.init();
2797 tui.active_buffer = BufferKind::Calldata;
2798
2799 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('o'))), ControlFlow::Continue(())));
2800 assert_eq!(tui.prompt, Some(Prompt::BufferOffset(String::new())));
2801
2802 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
2803 assert_eq!(tui.prompt, Some(Prompt::BufferOffset(String::new())));
2804 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2805
2806 for c in "40".chars() {
2807 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
2808 }
2809 let _ = tui.handle_key_event(key(KeyCode::Enter));
2810
2811 assert_eq!(tui.prompt, None);
2812 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2813 let status = tui.status.as_ref().unwrap();
2814 assert_eq!(status.kind, StatusKind::Info);
2815 assert_eq!(status.text, "Jumped to calldata offset 0x40 (64)");
2816 }
2817
2818 #[test]
2819 fn buffer_offset_jumps_in_active_calldata_buffer() {
2820 let address = Address::repeat_byte(1);
2821 let mut context = context_with_arena(vec![DebugNode::new(
2822 address,
2823 CallKind::Call,
2824 vec![step(1)],
2825 Bytes::from(vec![0; 96]),
2826 0,
2827 None,
2828 )]);
2829 let mut tui = TUIContext::new(&mut context);
2830 tui.init();
2831 tui.active_buffer = BufferKind::Calldata;
2832
2833 tui.goto_buffer_offset_from_input("20");
2834
2835 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2836 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x20 (32)");
2837 }
2838
2839 #[test]
2840 fn buffer_offset_jumps_to_visible_hex_label_on_partial_last_line() {
2841 let address = Address::repeat_byte(1);
2842 let mut context = context_with_arena(vec![DebugNode::new(
2843 address,
2844 CallKind::Call,
2845 vec![step(1)],
2846 Bytes::from(vec![0; 65]),
2847 0,
2848 None,
2849 )]);
2850 let mut tui = TUIContext::new(&mut context);
2851 tui.init();
2852 tui.active_buffer = BufferKind::Calldata;
2853
2854 tui.goto_buffer_offset_from_input("40");
2855
2856 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2857 assert_eq!(tui.status.as_ref().unwrap().text, "Jumped to calldata offset 0x40 (64)");
2858 }
2859
2860 #[test]
2861 fn buffer_offset_reports_out_of_range_offsets_without_moving() {
2862 let address = Address::repeat_byte(1);
2863 let mut context = context_with_arena(vec![DebugNode::new(
2864 address,
2865 CallKind::Call,
2866 vec![step(1)],
2867 Bytes::from(vec![0; 64]),
2868 0,
2869 None,
2870 )]);
2871 let mut tui = TUIContext::new(&mut context);
2872 tui.init();
2873 tui.active_buffer = BufferKind::Calldata;
2874 tui.draw_memory.current_buf_startline = 1;
2875
2876 tui.goto_buffer_offset_from_input("20");
2877 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2878 let status = tui.status.as_ref().unwrap();
2879 assert_eq!(status.kind, StatusKind::Info);
2880 assert_eq!(status.text, "Jumped to calldata offset 0x20 (32)");
2881
2882 tui.draw_memory.current_buf_startline = 0;
2883 tui.goto_buffer_offset_from_input("0x80");
2884 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2885 let status = tui.status.as_ref().unwrap();
2886 assert_eq!(status.kind, StatusKind::Error);
2887 assert_eq!(status.text, "calldata offset 0x80 (128) is outside the 64-byte buffer");
2888 }
2889
2890 #[test]
2891 fn buffer_offset_escape_cancels_and_empty_buffer_reports_error() {
2892 let address = Address::repeat_byte(1);
2893 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1])]);
2894 let mut tui = TUIContext::new(&mut context);
2895 tui.init();
2896
2897 let _ = tui.handle_key_event(key(KeyCode::Char('o')));
2898 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2899 let _ = tui.handle_key_event(key(KeyCode::Esc));
2900
2901 assert_eq!(tui.prompt, None);
2902 assert_eq!(tui.draw_memory.current_buf_startline, 0);
2903 assert_eq!(tui.status, None);
2904
2905 tui.goto_buffer_offset_from_input("0");
2906 let status = tui.status.as_ref().unwrap();
2907 assert_eq!(status.kind, StatusKind::Error);
2908 assert_eq!(status.text, "Current memory buffer is empty");
2909 }
2910
2911 #[test]
2912 fn buffer_scroll_reaches_partial_last_line() {
2913 let address = Address::repeat_byte(1);
2914 let mut context = context_with_arena(vec![DebugNode::new(
2915 address,
2916 CallKind::Call,
2917 vec![step(1)],
2918 Bytes::from(vec![0; 65]),
2919 0,
2920 None,
2921 )]);
2922 let mut tui = TUIContext::new(&mut context);
2923 tui.init();
2924 tui.active_buffer = BufferKind::Calldata;
2925
2926 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2927 assert_eq!(tui.draw_memory.current_buf_startline, 1);
2928 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2929 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2930 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2931 assert_eq!(tui.draw_memory.current_buf_startline, 2);
2932 }
2933
2934 #[test]
2935 fn storage_scroll_repeats_without_exceeding_last_slot() {
2936 let steps =
2937 (0..3).map(|slot| step_with_stack(slot, OpCode::TSTORE, &[slot, slot])).collect();
2938 let mut context = context_with_arena(vec![DebugNode::new(
2939 Address::ZERO,
2940 CallKind::Call,
2941 steps,
2942 Bytes::new(),
2943 0,
2944 None,
2945 )]);
2946 let mut tui = TUIContext::new(&mut context);
2947 tui.current_step = 2;
2948 tui.run_command_from_input("transient");
2949
2950 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
2951 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2952 assert_eq!(tui.draw_memory.current_storage_startline, 2);
2953
2954 let _ = tui.handle_key_event(ctrl_key(KeyCode::Char('j')));
2955 assert_eq!(tui.draw_memory.current_storage_startline, 2);
2956 }
2957
2958 #[test]
2959 fn opcode_search_wraps_and_is_case_insensitive() {
2960 let arena = vec![DebugNode::new(
2961 Address::repeat_byte(1),
2962 CallKind::Call,
2963 vec![
2964 step(1),
2965 step_with_immediate(2, OpCode::PUSH4, &[0x95, 0xd8, 0x9b, 0x41]),
2966 step_with_stack(3, OpCode::MSTORE, &[]),
2967 ],
2968 Bytes::new(),
2969 0,
2970 None,
2971 )];
2972
2973 assert_eq!(
2974 find_opcode_match(&arena, 0, 0, "push4", SearchDirection::Forward),
2975 Some((0, 1))
2976 );
2977 assert_eq!(
2978 find_opcode_match(&arena, 0, 0, "95D89B41", SearchDirection::Forward),
2979 Some((0, 1))
2980 );
2981 assert_eq!(
2982 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Backward),
2983 Some((0, 2))
2984 );
2985 assert_eq!(find_opcode_match(&arena, 0, 0, "sload", SearchDirection::Forward), None);
2986 }
2987
2988 #[test]
2989 fn opcode_search_input_mode_handles_keys_and_blocks_normal_commands() {
2990 let address = Address::repeat_byte(1);
2991 let mut context = context_with_arena(vec![DebugNode::new(
2992 address,
2993 CallKind::Call,
2994 vec![
2995 step(1),
2996 step_with_immediate(2, OpCode::PUSH4, &[0x95, 0xd8, 0x9b, 0x41]),
2997 step_with_stack(3, OpCode::MSTORE, &[]),
2998 ],
2999 Bytes::new(),
3000 0,
3001 None,
3002 )]);
3003 let mut tui = TUIContext::new(&mut context);
3004 tui.init();
3005
3006 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('/'))), ControlFlow::Continue(())));
3007 assert_eq!(tui.prompt, Some(Prompt::OpcodeSearch(String::new())));
3008
3009 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
3010 assert_eq!(tui.prompt, Some(Prompt::OpcodeSearch("q".into())));
3011 assert_eq!(tui.current_step, 0);
3012
3013 let _ = tui.handle_key_event(key(KeyCode::Backspace));
3014 let _ = tui.handle_key_event(key(KeyCode::Char('9')));
3015 let _ = tui.handle_key_event(key(KeyCode::Char('5')));
3016 let _ = tui.handle_key_event(key(KeyCode::Enter));
3017
3018 assert_eq!(tui.prompt, None);
3019 assert_eq!(tui.last_opcode_search.as_deref(), Some("95"));
3020 assert_eq!(tui.current_step, 1);
3021 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
3022 }
3023
3024 #[test]
3025 fn opcode_search_repeats_forward_and_backward() {
3026 let address = Address::repeat_byte(1);
3027 let mut context = context_with_arena(vec![DebugNode::new(
3028 address,
3029 CallKind::Call,
3030 vec![
3031 step_with_stack(1, OpCode::MSTORE, &[]),
3032 step(2),
3033 step_with_stack(3, OpCode::MSTORE, &[]),
3034 ],
3035 Bytes::new(),
3036 0,
3037 None,
3038 )]);
3039 let mut tui = TUIContext::new(&mut context);
3040 tui.init();
3041
3042 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3043 for c in "mstore".chars() {
3044 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
3045 }
3046 let _ = tui.handle_key_event(key(KeyCode::Enter));
3047 assert_eq!(tui.current_step, 2);
3048
3049 let _ = tui.handle_key_event(key(KeyCode::Char('n')));
3050 assert_eq!(tui.current_step, 0);
3051
3052 let _ = tui.handle_key_event(key(KeyCode::Char('N')));
3053 assert_eq!(tui.current_step, 2);
3054 }
3055
3056 #[test]
3057 fn opcode_search_crosses_split_call_segments() {
3058 let address = Address::repeat_byte(1);
3059 let node = |address, trace_node_idx, step_offset, steps| {
3060 let mut node = DebugNode::new(address, CallKind::Call, steps, Bytes::new(), 0, None);
3061 node.trace_node_idx = trace_node_idx;
3062 node.step_offset = step_offset;
3063 node
3064 };
3065 let arena = vec![
3066 node(address, 0, 0, vec![step_with_stack(1, OpCode::MSTORE, &[]), step(2)]),
3067 node(Address::repeat_byte(2), 1, 0, vec![step_with_stack(3, OpCode::MSTORE, &[])]),
3068 node(address, 0, 2, vec![step(4), step_with_stack(5, OpCode::MSTORE, &[])]),
3069 ];
3070
3071 assert_eq!(
3072 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Forward),
3073 Some((2, 1))
3074 );
3075 assert_eq!(
3076 find_opcode_match(&arena, 2, 1, "mstore", SearchDirection::Forward),
3077 Some((0, 0))
3078 );
3079 assert_eq!(
3080 find_opcode_match(&arena, 0, 0, "mstore", SearchDirection::Backward),
3081 Some((2, 1))
3082 );
3083 }
3084
3085 #[test]
3086 fn opcode_search_escape_cancels_without_moving() {
3087 let address = Address::repeat_byte(1);
3088 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3089 let mut tui = TUIContext::new(&mut context);
3090 tui.init();
3091
3092 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3093 let _ = tui.handle_key_event(key(KeyCode::Char('s')));
3094 let _ = tui.handle_key_event(key(KeyCode::Esc));
3095
3096 assert_eq!(tui.prompt, None);
3097 assert_eq!(tui.last_opcode_search, None);
3098 assert_eq!(tui.current_step, 0);
3099 assert_eq!(tui.status, None);
3100 }
3101
3102 #[test]
3103 fn opcode_search_reports_empty_input_without_remembering_search() {
3104 let address = Address::repeat_byte(1);
3105 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3106 let mut tui = TUIContext::new(&mut context);
3107 tui.init();
3108
3109 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3110 let _ = tui.handle_key_event(key(KeyCode::Enter));
3111
3112 assert_eq!(tui.current_step, 0);
3113 assert_eq!(tui.last_opcode_search, None);
3114 let status = tui.status.as_ref().unwrap();
3115 assert_eq!(status.kind, StatusKind::Error);
3116 assert_eq!(status.text, "Enter an opcode search term");
3117 }
3118
3119 #[test]
3120 fn opcode_search_reports_repeat_without_previous_search() {
3121 let address = Address::repeat_byte(1);
3122 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3123 let mut tui = TUIContext::new(&mut context);
3124 tui.init();
3125
3126 let _ = tui.handle_key_event(key(KeyCode::Char('n')));
3127
3128 assert_eq!(tui.current_step, 0);
3129 let status = tui.status.as_ref().unwrap();
3130 assert_eq!(status.kind, StatusKind::Error);
3131 assert_eq!(status.text, "No previous opcode search");
3132 }
3133
3134 #[test]
3135 fn opcode_search_reports_no_match_without_moving() {
3136 let address = Address::repeat_byte(1);
3137 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
3138 let mut tui = TUIContext::new(&mut context);
3139 tui.init();
3140
3141 let _ = tui.handle_key_event(key(KeyCode::Char('/')));
3142 for c in "sload".chars() {
3143 let _ = tui.handle_key_event(key(KeyCode::Char(c)));
3144 }
3145 let _ = tui.handle_key_event(key(KeyCode::Enter));
3146
3147 assert_eq!(tui.current_step, 0);
3148 assert_eq!(tui.last_opcode_search.as_deref(), Some("sload"));
3149 let status = tui.status.as_ref().unwrap();
3150 assert_eq!(status.kind, StatusKind::Error);
3151 assert_eq!(status.text, "No opcode matching `sload` in current call");
3152 }
3153
3154 #[test]
3155 fn memory_write_start_line_uses_write_offset() {
3156 assert_eq!(memory_write_start_line(&step_with_stack(0, OpCode::MSTORE, &[0, 96])), Some(3));
3157 assert_eq!(
3158 memory_write_start_line(&step_with_stack(0, OpCode::MSTORE8, &[0, 33])),
3159 Some(1)
3160 );
3161 assert_eq!(memory_write_start_line(&step(0)), None);
3162 }
3163
3164 #[test]
3165 fn bounded_memory_write_start_line_requires_visible_non_empty_write() {
3166 let write_at_128 = step_with_stack(0, OpCode::MSTORE, &[0, 128]);
3167 assert_eq!(bounded_memory_write_start_line(&write_at_128, 160), Some(4));
3168 assert_eq!(bounded_memory_write_start_line(&write_at_128, 128), None);
3169
3170 let zero_len_copy = step_with_stack(0, OpCode::CALLDATACOPY, &[0, 0, 1_000_000]);
3171 assert_eq!(memory_write_start_line(&zero_len_copy), None);
3172 assert_eq!(bounded_memory_write_start_line(&zero_len_copy, 32), None);
3173 }
3174
3175 #[test]
3176 fn stepping_past_memory_write_without_memory_snapshot_keeps_scroll_position() {
3177 let address = Address::repeat_byte(1);
3178 let mut context = context_with_arena(vec![DebugNode::new(
3179 address,
3180 CallKind::Call,
3181 vec![step_with_stack(1, OpCode::MSTORE, &[0, 128]), step(2)],
3182 Bytes::new(),
3183 0,
3184 None,
3185 )]);
3186 let mut tui = TUIContext::new(&mut context);
3187 tui.init();
3188 tui.draw_memory.current_buf_startline = 99;
3189
3190 tui.step();
3191
3192 assert_eq!(tui.current_step, 1);
3193 assert_eq!(tui.draw_memory.current_buf_startline, 99);
3194 }
3195
3196 #[test]
3197 fn memory_write_autoscroll_only_applies_to_memory_buffer() {
3198 let address = Address::repeat_byte(1);
3199 let mut context = context_with_arena(vec![DebugNode::new(
3200 address,
3201 CallKind::Call,
3202 vec![step_with_stack(1, OpCode::MSTORE, &[0, 128]), step(2)],
3203 Bytes::from(vec![0; 256]),
3204 0,
3205 None,
3206 )]);
3207 let mut tui = TUIContext::new(&mut context);
3208 tui.init();
3209 tui.active_buffer = BufferKind::Calldata;
3210 tui.draw_memory.current_buf_startline = 7;
3211
3212 tui.step();
3213
3214 assert_eq!(tui.current_step, 1);
3215 assert_eq!(tui.draw_memory.current_buf_startline, 7);
3216 }
3217
3218 #[test]
3219 fn navigation_clamps_scroll_positions_to_non_empty_data() {
3220 let address = Address::repeat_byte(1);
3221 let mut context = context_with_arena(vec![
3222 DebugNode::new(
3223 address,
3224 CallKind::Call,
3225 vec![step_with_stack(1, OpCode::STOP, &[0, 1, 2])],
3226 Bytes::from(vec![0; 64]),
3227 0,
3228 None,
3229 ),
3230 DebugNode::new(
3231 address,
3232 CallKind::Call,
3233 vec![step_with_stack(2, OpCode::STOP, &[0])],
3234 Bytes::from(vec![0; 4]),
3235 0,
3236 None,
3237 ),
3238 ]);
3239 let mut tui = TUIContext::new(&mut context);
3240 tui.init();
3241 tui.active_buffer = BufferKind::Calldata;
3242 tui.draw_memory.current_buf_startline = 1;
3243 tui.draw_memory.current_stack_startline = 2;
3244
3245 let _ = tui.handle_key_event(key(KeyCode::Char('C')));
3246
3247 assert_eq!(tui.draw_memory.inner_call_index, 1);
3248 assert_eq!(tui.draw_memory.current_buf_startline, 0);
3249 assert_eq!(tui.draw_memory.current_stack_startline, 0);
3250 }
3251
3252 #[test]
3253 fn navigation_preserves_stack_scroll_across_empty_snapshot() {
3254 let address = Address::repeat_byte(1);
3255 let mut empty_stack = step(2);
3256 empty_stack.stack = Some(Vec::new().into_boxed_slice());
3257 let mut context = context_with_arena(vec![DebugNode::new(
3258 address,
3259 CallKind::Call,
3260 vec![
3261 step_with_stack(1, OpCode::STOP, &[0, 1, 2]),
3262 empty_stack,
3263 step_with_stack(3, OpCode::STOP, &[0, 1, 2]),
3264 ],
3265 Bytes::new(),
3266 0,
3267 None,
3268 )]);
3269 let mut tui = TUIContext::new(&mut context);
3270 tui.init();
3271 tui.draw_memory.current_stack_startline = 2;
3272
3273 tui.step();
3274 assert_eq!(tui.draw_memory.current_stack_startline, 2);
3275
3276 tui.step();
3277 assert_eq!(tui.draw_memory.current_stack_startline, 2);
3278 }
3279}