1use crate::{DebugNode, ExitReason, debugger::DebuggerContext};
4use alloy_primitives::{Address, hex};
5use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6use foundry_evm_core::buffer::BufferKind;
7use foundry_tui::TuiApp;
8use ratatui::Frame;
9use revm::bytecode::opcode::OpCode;
10use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
11use std::ops::ControlFlow;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub(crate) enum StatusKind {
15 Info,
16 Error,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub(crate) struct StatusMessage {
21 pub(crate) kind: StatusKind,
22 pub(crate) text: String,
23}
24
25#[derive(Default)]
27pub(crate) struct DrawMemory {
28 pub(crate) inner_call_index: usize,
29 pub(crate) current_buf_startline: usize,
30 pub(crate) current_stack_startline: usize,
31}
32
33pub(crate) struct TUIContext<'a> {
34 pub(crate) debugger_context: &'a mut DebuggerContext,
35
36 pub(crate) key_buffer: String,
38 pub(crate) pc_input: Option<String>,
40 pub(crate) status: Option<StatusMessage>,
42 pub(crate) current_step: usize,
44 pub(crate) draw_memory: DrawMemory,
45 pub(crate) opcode_list: Vec<String>,
46 pub(crate) last_index: usize,
47
48 pub(crate) stack_labels: bool,
49 pub(crate) buf_utf: bool,
51 pub(crate) show_shortcuts: bool,
52 pub(crate) active_buffer: BufferKind,
54}
55
56impl<'a> TUIContext<'a> {
57 pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
58 TUIContext {
59 debugger_context,
60
61 key_buffer: String::with_capacity(64),
62 pc_input: None,
63 status: None,
64 current_step: 0,
65 draw_memory: DrawMemory::default(),
66 opcode_list: Vec::new(),
67 last_index: 0,
68
69 stack_labels: false,
70 buf_utf: false,
71 show_shortcuts: true,
72 active_buffer: BufferKind::Memory,
73 }
74 }
75
76 pub(crate) fn init(&mut self) {
77 self.gen_opcode_list();
78 }
79
80 pub(crate) fn debug_arena(&self) -> &[DebugNode] {
81 &self.debugger_context.debug_arena
82 }
83
84 pub(crate) fn debug_call(&self) -> &DebugNode {
85 &self.debug_arena()[self.draw_memory.inner_call_index]
86 }
87
88 pub(crate) fn address(&self) -> &Address {
90 &self.debug_call().address
91 }
92
93 pub(crate) fn call_kind(&self) -> CallKind {
95 self.debug_call().kind
96 }
97
98 pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
100 &self.debug_call().steps
101 }
102
103 pub(crate) fn current_step(&self) -> &CallTraceStep {
105 &self.debug_steps()[self.current_step]
106 }
107
108 fn gen_opcode_list(&mut self) {
109 self.opcode_list.clear();
110 let debug_steps =
111 &self.debugger_context.debug_arena[self.draw_memory.inner_call_index].steps;
112 for step in debug_steps {
113 self.opcode_list.push(pretty_opcode(step));
114 }
115 }
116
117 fn gen_opcode_list_if_necessary(&mut self) {
118 if self.last_index != self.draw_memory.inner_call_index {
119 self.gen_opcode_list();
120 self.last_index = self.draw_memory.inner_call_index;
121 }
122 }
123
124 fn active_buffer(&self) -> &[u8] {
125 match self.active_buffer {
126 BufferKind::Memory => self.current_step().memory.as_ref().unwrap().as_bytes(),
127 BufferKind::Calldata => &self.debug_call().calldata,
128 BufferKind::Returndata => &self.current_step().returndata,
129 }
130 }
131}
132
133impl TUIContext<'_> {
134 pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
135 let ret = match event {
136 Event::Key(event) => self.handle_key_event(event),
137 Event::Mouse(event) => self.handle_mouse_event(event),
138 _ => ControlFlow::Continue(()),
139 };
140 self.gen_opcode_list_if_necessary();
142 ret
143 }
144
145 fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
146 if self.pc_input.is_some() {
147 self.handle_pc_input_key_event(event);
148 return ControlFlow::Continue(());
149 }
150
151 if let KeyCode::Char(c) = event.code
153 && c.is_alphabetic()
154 && self.key_buffer.starts_with('\'')
155 {
156 self.handle_breakpoint(c);
157 return ControlFlow::Continue(());
158 }
159
160 let control = event.modifiers.contains(KeyModifiers::CONTROL);
161
162 match event.code {
163 KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
165
166 KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
168 this.draw_memory.current_buf_startline =
169 this.draw_memory.current_buf_startline.saturating_sub(1);
170 }),
171 KeyCode::Char('j') | KeyCode::Down if control => self.repeat(|this| {
173 let max_buf = (this.active_buffer().len() / 32).saturating_sub(1);
174 if this.draw_memory.current_buf_startline < max_buf {
175 this.draw_memory.current_buf_startline += 1;
176 }
177 }),
178
179 KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
181 KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
183
184 KeyCode::Char('K') => self.repeat(|this| {
186 this.draw_memory.current_stack_startline =
187 this.draw_memory.current_stack_startline.saturating_sub(1);
188 }),
189 KeyCode::Char('J') => self.repeat(|this| {
191 let max_stack =
192 this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
193 if this.draw_memory.current_stack_startline < max_stack {
194 this.draw_memory.current_stack_startline += 1;
195 }
196 }),
197
198 KeyCode::Char('b') => {
200 self.active_buffer = self.active_buffer.next();
201 self.draw_memory.current_buf_startline = 0;
202 }
203
204 KeyCode::Char('g') => {
206 self.draw_memory.inner_call_index = 0;
207 self.current_step = 0;
208 }
209
210 KeyCode::Char('G') => {
212 self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
213 self.current_step = self.n_steps() - 1;
214 }
215
216 KeyCode::Char('c') => {
218 self.draw_memory.inner_call_index =
219 self.draw_memory.inner_call_index.saturating_sub(1);
220 self.current_step = self.n_steps() - 1;
221 }
222
223 KeyCode::Char('C')
225 if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 =>
226 {
227 self.draw_memory.inner_call_index += 1;
228 self.current_step = 0;
229 }
230
231 KeyCode::Char('s') => self.repeat(|this| {
233 let remaining_steps = &this.debug_steps()[this.current_step..];
234 if let Some((i, _)) =
235 remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
236 let prev = &remaining_steps[*i - 1];
237 is_jump(step, prev)
238 })
239 {
240 this.current_step += i
241 }
242 }),
243
244 KeyCode::Char('a') => self.repeat(|this| {
246 let ops = &this.debug_steps()[..this.current_step];
247 this.current_step = ops
248 .iter()
249 .enumerate()
250 .skip(1)
251 .rev()
252 .find(|&(i, op)| {
253 let prev = &ops[i - 1];
254 is_jump(op, prev)
255 })
256 .map(|(i, _)| i)
257 .unwrap_or_default();
258 }),
259
260 KeyCode::Char('t') => self.stack_labels = !self.stack_labels,
262
263 KeyCode::Char('m') => self.buf_utf = !self.buf_utf,
265
266 KeyCode::Char('p') => {
268 self.key_buffer.clear();
269 self.status = None;
270 self.pc_input = Some(String::new());
271 }
272
273 KeyCode::Char('h') => self.show_shortcuts = !self.show_shortcuts,
275
276 KeyCode::Char(
278 other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
279 ) => {
280 self.key_buffer.push(other);
282 return ControlFlow::Continue(());
283 }
284
285 _ => {}
287 };
288
289 self.key_buffer.clear();
290 ControlFlow::Continue(())
291 }
292
293 fn handle_pc_input_key_event(&mut self, event: KeyEvent) {
294 match event.code {
295 KeyCode::Esc => {
296 self.pc_input = None;
297 }
298 KeyCode::Enter => {
299 let input = self.pc_input.take().unwrap_or_default();
300 self.goto_pc_from_input(&input);
301 }
302 KeyCode::Backspace => {
303 if let Some(input) = &mut self.pc_input {
304 input.pop();
305 }
306 }
307 KeyCode::Char(c)
308 if !event.modifiers.contains(KeyModifiers::CONTROL) && is_pc_input_char(c) =>
309 {
310 if let Some(input) = &mut self.pc_input {
311 input.push(c);
312 }
313 }
314 _ => {}
315 }
316 }
317
318 fn goto_pc_from_input(&mut self, input: &str) {
319 let candidates = match parse_pc_candidates(input) {
320 Ok(candidates) => candidates,
321 Err(err) => {
322 self.set_error(err);
323 return;
324 }
325 };
326
327 let mut found = Vec::new();
328 for &candidate in &candidates {
329 if let Some(target) = find_pc_target(
330 self.debug_arena(),
331 self.draw_memory.inner_call_index,
332 self.current_step,
333 candidate.pc,
334 ) {
335 found.push((candidate, target));
336 }
337 }
338
339 match found.as_slice() {
340 [] => {
341 let current = self.debug_call();
342 let outside = candidates.iter().any(|candidate| {
343 pc_exists_outside_code_context(self.debug_arena(), current, candidate.pc)
344 });
345 let pc = if let [candidate] = candidates.as_slice() {
346 let pc = candidate.pc;
347 format!("PC 0x{pc:x} ({pc})")
348 } else {
349 format!("PC `{}`", input.trim())
350 };
351 let mut msg = format!("{pc} not found in current contract");
352 if outside {
353 msg.push_str("; it exists in another contract, switch calls first");
354 }
355 self.set_error(msg);
356 }
357 [(candidate, target)] => self.apply_pc_target(*candidate, *target),
358 _ => {
359 let input = input.trim();
360 let options = found
361 .iter()
362 .map(|(candidate, _)| candidate.describe())
363 .collect::<Vec<_>>()
364 .join(" and ");
365 self.set_error(format!(
366 "Ambiguous PC `{input}`: {options} both exist; use d:<pc> or 0x<pc>"
367 ));
368 }
369 }
370 }
371
372 fn apply_pc_target(&mut self, candidate: PcCandidate, target: PcTarget) {
373 let already_at_target = self.draw_memory.inner_call_index == target.node_index
374 && self.current_step == target.step_index;
375
376 self.draw_memory.inner_call_index = target.node_index;
377 self.current_step = target.step_index;
378 self.draw_memory.current_buf_startline = 0;
379 self.draw_memory.current_stack_startline = 0;
380 self.key_buffer.clear();
381
382 let pc = candidate.pc;
383 let scope = match target.scope {
384 PcTargetScope::CurrentNode => "current trace",
385 PcTargetScope::SameCodeContext => "same contract",
386 };
387 let action = if already_at_target { "Already at" } else { "Jumped to" };
388 self.set_info(format!("{action} PC 0x{pc:x} ({pc}) in {scope}"));
389 }
390
391 fn set_info(&mut self, text: String) {
392 self.status = Some(StatusMessage { kind: StatusKind::Info, text });
393 }
394
395 fn set_error(&mut self, text: String) {
396 self.status = Some(StatusMessage { kind: StatusKind::Error, text });
397 }
398
399 fn handle_breakpoint(&mut self, c: char) {
400 if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) {
403 for (i, node) in self.debug_arena().iter().enumerate() {
404 if node.address == *caller
405 && let Some(step) = node.steps.iter().position(|step| step.pc == *pc)
406 {
407 self.draw_memory.inner_call_index = i;
408 self.current_step = step;
409 break;
410 }
411 }
412 }
413 self.key_buffer.clear();
414 }
415
416 fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
417 if self.pc_input.is_some() {
418 return ControlFlow::Continue(());
419 }
420
421 match event.kind {
422 MouseEventKind::ScrollUp => self.step_back(),
423 MouseEventKind::ScrollDown => self.step(),
424 _ => {}
425 }
426
427 ControlFlow::Continue(())
428 }
429
430 fn step_back(&mut self) {
431 if self.current_step > 0 {
432 self.current_step -= 1;
433 } else if self.draw_memory.inner_call_index > 0 {
434 self.draw_memory.inner_call_index -= 1;
435 self.current_step = self.n_steps() - 1;
436 }
437 }
438
439 fn step(&mut self) {
440 if self.current_step < self.n_steps() - 1 {
441 self.current_step += 1;
442 } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
443 self.draw_memory.inner_call_index += 1;
444 self.current_step = 0;
445 }
446 }
447
448 fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
450 for _ in 0..buffer_as_number(&self.key_buffer) {
451 f(self);
452 }
453 }
454
455 fn n_steps(&self) -> usize {
456 self.debug_steps().len()
457 }
458}
459
460impl TuiApp for TUIContext<'_> {
461 type Exit = ExitReason;
462
463 fn draw(&mut self, frame: &mut Frame<'_>) {
464 self.draw_layout(frame);
465 }
466
467 fn handle_event(&mut self, event: Event) -> ControlFlow<Self::Exit> {
468 TUIContext::handle_event(self, event)
469 }
470}
471
472fn buffer_as_number(s: &str) -> usize {
474 const MIN: usize = 1;
475 const MAX: usize = 100_000;
476 s.parse().unwrap_or(MIN).clamp(MIN, MAX)
477}
478
479const fn is_pc_input_char(c: char) -> bool {
480 c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | ':')
481}
482
483#[derive(Clone, Copy, Debug, PartialEq, Eq)]
484enum PcBase {
485 Hex,
486 Decimal,
487}
488
489#[derive(Clone, Copy, Debug, PartialEq, Eq)]
490struct PcCandidate {
491 pc: usize,
492 base: PcBase,
493}
494
495impl PcCandidate {
496 fn describe(self) -> String {
497 match self.base {
498 PcBase::Hex => format!("hex 0x{:x}", self.pc),
499 PcBase::Decimal => format!("decimal {}", self.pc),
500 }
501 }
502}
503
504#[derive(Clone, Copy, Debug, PartialEq, Eq)]
505enum PcTargetScope {
506 CurrentNode,
507 SameCodeContext,
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511struct PcTarget {
512 node_index: usize,
513 step_index: usize,
514 scope: PcTargetScope,
515}
516
517fn parse_pc_candidates(input: &str) -> Result<Vec<PcCandidate>, String> {
518 let input = input.trim();
519 if input.is_empty() {
520 return Err("Enter a program counter".to_string());
521 }
522
523 if let Some(rest) = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")) {
524 return parse_pc_candidate(rest, 16, PcBase::Hex, input);
525 }
526
527 if let Some(rest) = input.strip_prefix("d:").or_else(|| input.strip_prefix("dec:")) {
528 return parse_pc_candidate(rest, 10, PcBase::Decimal, input);
529 }
530
531 if input.chars().any(|c| c.is_ascii_hexdigit() && c.is_ascii_alphabetic()) {
532 return parse_pc_candidate(input, 16, PcBase::Hex, input);
533 }
534
535 if input.chars().all(|c| c.is_ascii_digit()) {
536 let decimal = parse_pc(input, 10, input)?;
537 let hex = parse_pc(input, 16, input)?;
538 if decimal == hex {
539 return Ok(vec![PcCandidate { pc: decimal, base: PcBase::Decimal }]);
540 }
541 return Ok(vec![
542 PcCandidate { pc: decimal, base: PcBase::Decimal },
543 PcCandidate { pc: hex, base: PcBase::Hex },
544 ]);
545 }
546
547 Err(format!("Invalid PC `{input}`; use 0x2a, 2a, or d:42"))
548}
549
550fn parse_pc_candidate(
551 input: &str,
552 radix: u32,
553 base: PcBase,
554 original: &str,
555) -> Result<Vec<PcCandidate>, String> {
556 Ok(vec![PcCandidate { pc: parse_pc(input, radix, original)?, base }])
557}
558
559fn parse_pc(input: &str, radix: u32, original: &str) -> Result<usize, String> {
560 if input.is_empty() {
561 return Err(format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"));
562 }
563 usize::from_str_radix(input, radix)
564 .map_err(|_| format!("Invalid PC `{original}`; use 0x2a, 2a, or d:42"))
565}
566
567fn find_pc_target(
568 arena: &[DebugNode],
569 current_node_index: usize,
570 current_step: usize,
571 pc: usize,
572) -> Option<PcTarget> {
573 let current_node = arena.get(current_node_index)?;
574
575 if let Some(step_index) = find_pc_in_current_node(¤t_node.steps, current_step, pc) {
576 return Some(PcTarget {
577 node_index: current_node_index,
578 step_index,
579 scope: PcTargetScope::CurrentNode,
580 });
581 }
582
583 for (node_index, node) in arena.iter().enumerate().skip(current_node_index + 1) {
584 if same_code_context(current_node, node)
585 && let Some(step_index) = node.steps.iter().position(|step| step.pc == pc)
586 {
587 return Some(PcTarget {
588 node_index,
589 step_index,
590 scope: PcTargetScope::SameCodeContext,
591 });
592 }
593 }
594
595 for (node_index, node) in arena.iter().enumerate().take(current_node_index).rev() {
596 if same_code_context(current_node, node)
597 && let Some(step_index) = node.steps.iter().rposition(|step| step.pc == pc)
598 {
599 return Some(PcTarget {
600 node_index,
601 step_index,
602 scope: PcTargetScope::SameCodeContext,
603 });
604 }
605 }
606
607 None
608}
609
610fn find_pc_in_current_node(
611 steps: &[CallTraceStep],
612 current_step: usize,
613 pc: usize,
614) -> Option<usize> {
615 if steps.get(current_step).is_some_and(|step| step.pc == pc) {
616 return Some(current_step);
617 }
618
619 steps
620 .iter()
621 .enumerate()
622 .skip(current_step.saturating_add(1))
623 .find_map(|(i, step)| (step.pc == pc).then_some(i))
624 .or_else(|| {
625 steps[..current_step.min(steps.len())]
626 .iter()
627 .enumerate()
628 .rev()
629 .find_map(|(i, step)| (step.pc == pc).then_some(i))
630 })
631}
632
633fn same_code_context(a: &DebugNode, b: &DebugNode) -> bool {
634 a.address == b.address && a.kind.is_any_create() == b.kind.is_any_create()
635}
636
637fn pc_exists_outside_code_context(arena: &[DebugNode], current: &DebugNode, pc: usize) -> bool {
638 arena.iter().any(|node| {
639 !same_code_context(current, node) && node.steps.iter().any(|step| step.pc == pc)
640 })
641}
642
643fn pretty_opcode(step: &CallTraceStep) -> String {
644 if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
645 format!("{}(0x{})", step.op, hex::encode(immediate))
646 } else {
647 step.op.to_string()
648 }
649}
650
651fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
652 if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
653 return false;
654 }
655
656 let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
657
658 step.pc != prev.pc + 1 + immediate_len
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use alloy_primitives::Bytes;
665 use foundry_evm_core::Breakpoints;
666 use foundry_evm_traces::debug::ContractSources;
667 use revm::interpreter::InstructionResult;
668
669 fn step(pc: usize) -> CallTraceStep {
670 CallTraceStep {
671 pc,
672 op: OpCode::STOP,
673 stack: None,
674 push_stack: None,
675 memory: None,
676 returndata: Bytes::new(),
677 gas_remaining: 0,
678 gas_refund_counter: 0,
679 gas_used: 0,
680 gas_cost: 0,
681 storage_change: None,
682 status: Some(InstructionResult::Stop),
683 immediate_bytes: None,
684 decoded: None,
685 }
686 }
687
688 fn node(address: Address, kind: CallKind, pcs: &[usize]) -> DebugNode {
689 DebugNode::new(address, kind, pcs.iter().copied().map(step).collect(), Bytes::new(), 0)
690 }
691
692 fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
693 DebuggerContext {
694 debug_arena: arena,
695 stats: None,
696 identified_contracts: Default::default(),
697 contracts_sources: ContractSources::default(),
698 breakpoints: Breakpoints::default(),
699 }
700 }
701
702 fn key(code: KeyCode) -> KeyEvent {
703 KeyEvent::new(code, KeyModifiers::empty())
704 }
705
706 #[test]
707 fn parses_prefixed_hex_pc() {
708 assert_eq!(
709 parse_pc_candidates("0x2a").unwrap(),
710 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
711 );
712 assert_eq!(
713 parse_pc_candidates("0X2A").unwrap(),
714 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
715 );
716 }
717
718 #[test]
719 fn parses_bare_hex_pc_with_letters() {
720 assert_eq!(
721 parse_pc_candidates("2a").unwrap(),
722 vec![PcCandidate { pc: 42, base: PcBase::Hex }]
723 );
724 }
725
726 #[test]
727 fn parses_explicit_decimal_pc() {
728 assert_eq!(
729 parse_pc_candidates("d:42").unwrap(),
730 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
731 );
732 assert_eq!(
733 parse_pc_candidates("dec:42").unwrap(),
734 vec![PcCandidate { pc: 42, base: PcBase::Decimal }]
735 );
736 }
737
738 #[test]
739 fn parses_bare_digits_as_decimal_and_hex_candidates() {
740 assert_eq!(
741 parse_pc_candidates("10").unwrap(),
742 vec![
743 PcCandidate { pc: 10, base: PcBase::Decimal },
744 PcCandidate { pc: 16, base: PcBase::Hex },
745 ]
746 );
747 assert_eq!(
748 parse_pc_candidates("9").unwrap(),
749 vec![PcCandidate { pc: 9, base: PcBase::Decimal }]
750 );
751 }
752
753 #[test]
754 fn rejects_invalid_pc_input() {
755 assert!(parse_pc_candidates("").is_err());
756 assert!(parse_pc_candidates("0x").is_err());
757 assert!(parse_pc_candidates("xyz").is_err());
758 assert!(parse_pc_candidates("184467440737095516160").is_err());
759 }
760
761 #[test]
762 fn finds_pc_in_current_node() {
763 let address = Address::repeat_byte(1);
764 let arena = vec![node(address, CallKind::Call, &[1, 2, 3])];
765
766 assert_eq!(
767 find_pc_target(&arena, 0, 0, 3),
768 Some(PcTarget { node_index: 0, step_index: 2, scope: PcTargetScope::CurrentNode })
769 );
770 }
771
772 #[test]
773 fn repeated_pc_stays_current_then_prefers_next_then_previous() {
774 let address = Address::repeat_byte(1);
775 let arena = vec![node(address, CallKind::Call, &[1, 2, 3, 2])];
776
777 assert_eq!(find_pc_target(&arena, 0, 1, 2).unwrap().step_index, 1);
778 assert_eq!(find_pc_target(&arena, 0, 0, 2).unwrap().step_index, 1);
779 assert_eq!(find_pc_target(&arena, 0, 3, 2).unwrap().step_index, 3);
780 assert_eq!(find_pc_target(&arena, 0, 2, 2).unwrap().step_index, 3);
781 }
782
783 #[test]
784 fn searches_later_then_earlier_same_code_context() {
785 let address = Address::repeat_byte(1);
786 let arena = vec![
787 node(address, CallKind::Call, &[1]),
788 node(address, CallKind::Call, &[2]),
789 node(address, CallKind::Call, &[3]),
790 ];
791
792 assert_eq!(find_pc_target(&arena, 1, 0, 3).unwrap().node_index, 2);
793 assert_eq!(find_pc_target(&arena, 1, 0, 1).unwrap().node_index, 0);
794 }
795
796 #[test]
797 fn does_not_search_different_address_or_creation_context() {
798 let address = Address::repeat_byte(1);
799 let other = Address::repeat_byte(2);
800 let arena = vec![
801 node(address, CallKind::Call, &[1]),
802 node(other, CallKind::Call, &[2]),
803 node(address, CallKind::Create, &[3]),
804 ];
805
806 assert!(find_pc_target(&arena, 0, 0, 2).is_none());
807 assert!(find_pc_target(&arena, 0, 0, 3).is_none());
808 assert!(pc_exists_outside_code_context(&arena, &arena[0], 2));
809 assert!(pc_exists_outside_code_context(&arena, &arena[0], 3));
810 }
811
812 #[test]
813 fn goto_resolves_unambiguous_bare_digits_and_reports_ambiguity() {
814 let address = Address::repeat_byte(1);
815 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[10, 16, 42])]);
816 let mut tui = TUIContext::new(&mut context);
817 tui.init();
818
819 tui.goto_pc_from_input("2a");
820 assert_eq!(tui.current_step, 2);
821 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
822
823 tui.current_step = 0;
824 tui.goto_pc_from_input("10");
825 assert_eq!(tui.current_step, 0);
826 assert!(tui.status.as_ref().unwrap().text.contains("Ambiguous PC"));
827
828 tui.goto_pc_from_input("d:10");
829 assert_eq!(tui.current_step, 0);
830 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
831 }
832
833 #[test]
834 fn goto_reports_pc_in_other_contract_without_moving() {
835 let address = Address::repeat_byte(1);
836 let other = Address::repeat_byte(2);
837 let mut context = context_with_arena(vec![
838 node(address, CallKind::Call, &[1]),
839 node(other, CallKind::Call, &[42]),
840 ]);
841 let mut tui = TUIContext::new(&mut context);
842 tui.init();
843
844 tui.goto_pc_from_input("2a");
845 assert_eq!(tui.draw_memory.inner_call_index, 0);
846 assert_eq!(tui.current_step, 0);
847 let status = tui.status.as_ref().unwrap();
848 assert_eq!(status.kind, StatusKind::Error);
849 assert!(status.text.contains("exists in another contract"));
850 }
851
852 #[test]
853 fn goto_reports_ambiguous_input_in_other_contract_without_choosing_first_candidate() {
854 let address = Address::repeat_byte(1);
855 let other = Address::repeat_byte(2);
856 let mut context = context_with_arena(vec![
857 node(address, CallKind::Call, &[1]),
858 node(other, CallKind::Call, &[16]),
859 ]);
860 let mut tui = TUIContext::new(&mut context);
861 tui.init();
862
863 tui.goto_pc_from_input("10");
864 let status = tui.status.as_ref().unwrap();
865 assert_eq!(status.kind, StatusKind::Error);
866 assert!(status.text.starts_with("PC `10` not found"));
867 assert!(status.text.contains("exists in another contract"));
868 }
869
870 #[test]
871 fn pc_input_mode_handles_keys_and_blocks_normal_commands() {
872 let address = Address::repeat_byte(1);
873 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
874 let mut tui = TUIContext::new(&mut context);
875 tui.init();
876
877 assert!(matches!(tui.handle_key_event(key(KeyCode::Char('p'))), ControlFlow::Continue(())));
878 assert_eq!(tui.pc_input.as_deref(), Some(""));
879
880 let _ = tui.handle_key_event(key(KeyCode::Char('q')));
881 assert_eq!(tui.pc_input.as_deref(), Some(""));
882 assert_eq!(tui.current_step, 0);
883
884 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
885 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
886 assert_eq!(tui.pc_input.as_deref(), Some("2a"));
887
888 let _ = tui.handle_key_event(key(KeyCode::Backspace));
889 assert_eq!(tui.pc_input.as_deref(), Some("2"));
890 let _ = tui.handle_key_event(key(KeyCode::Char('a')));
891 let _ = tui.handle_key_event(key(KeyCode::Enter));
892
893 assert_eq!(tui.pc_input, None);
894 assert_eq!(tui.current_step, 1);
895 assert_eq!(tui.status.as_ref().unwrap().kind, StatusKind::Info);
896 }
897
898 #[test]
899 fn pc_input_escape_cancels_without_moving() {
900 let address = Address::repeat_byte(1);
901 let mut context = context_with_arena(vec![node(address, CallKind::Call, &[1, 42])]);
902 let mut tui = TUIContext::new(&mut context);
903 tui.init();
904
905 let _ = tui.handle_key_event(key(KeyCode::Char('p')));
906 let _ = tui.handle_key_event(key(KeyCode::Char('2')));
907 let _ = tui.handle_key_event(key(KeyCode::Esc));
908
909 assert_eq!(tui.pc_input, None);
910 assert_eq!(tui.current_step, 0);
911 assert_eq!(tui.status, None);
912 }
913}