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