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