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