Skip to main content

foundry_debugger/tui/
context.rs

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