Skip to main content

foundry_debugger/tui/
draw.rs

1//! TUI draw implementation.
2
3use super::{
4    context::{
5        ActiveInternalCallCache, ActiveInternalCallLocation, Prompt, StatusKind, TUIContext,
6        write_pretty_opcode,
7    },
8    storage::{StorageAccess, StorageSpace, hex_u256, storage_access_at, storage_values},
9};
10use crate::{DebuggerLayout, debugger::DebuggerStats, op::OpcodeParam};
11use alloy_dyn_abi::{DynSolType, Specifier, parser::Parameters};
12use alloy_primitives::{Address, U256, keccak256};
13use foundry_common::fmt::format_token;
14use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
15use foundry_evm_traces::debug::{
16    DebugSourceScope, DebugVariable, decode_step_parameters, function_signature,
17};
18use ratatui::{
19    Frame,
20    layout::{Alignment, Constraint, Direction, Layout, Rect},
21    style::{Color, Modifier, Style},
22    text::{Line, Span, Text},
23    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
24};
25use revm::interpreter::InstructionResult;
26use revm_inspectors::tracing::types::{CallKind, DecodedInternalCall, DecodedTraceStep};
27use std::{collections::VecDeque, fmt::Write};
28
29impl TUIContext<'_> {
30    pub(crate) fn draw_layout(&mut self, f: &mut Frame<'_>) {
31        // We need 100 columns to display a 32 byte word in the data and stack panes.
32        let area = f.area();
33        let min_width = if self.show_data || self.show_stack { 100 } else { 1 };
34        let min_height = 16;
35        if area.width < min_width || area.height < min_height {
36            self.size_too_small(f, min_width, min_height);
37            return;
38        }
39
40        match self.layout() {
41            DebuggerLayout::Horizontal => self.horizontal_layout(f),
42            DebuggerLayout::Vertical => self.vertical_layout(f),
43            DebuggerLayout::Auto => {
44                // The horizontal layout draws these panes at 50% width.
45                let min_column_width_for_horizontal = 200;
46                if area.width >= min_column_width_for_horizontal {
47                    self.horizontal_layout(f);
48                } else {
49                    self.vertical_layout(f);
50                }
51            }
52        }
53    }
54
55    fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
56        let mut lines = Vec::with_capacity(4);
57
58        let l1 = "Terminal size too small:";
59        lines.push(Line::from(l1));
60
61        let area = f.area();
62        let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
63        let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
64        let l2 = vec![
65            Span::raw("Width = "),
66            Span::styled(area.width.to_string(), Style::new().fg(width_color)),
67            Span::raw(" Height = "),
68            Span::styled(area.height.to_string(), Style::new().fg(height_color)),
69        ];
70        lines.push(Line::from(l2));
71
72        let l3 = "Needed for current config:";
73        lines.push(Line::from(l3));
74        let l4 = format!("Width = {min_width} Height = {min_height}");
75        lines.push(Line::from(l4));
76
77        let paragraph =
78            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
79        f.render_widget(paragraph, area)
80    }
81
82    /// Draws the layout in vertical mode.
83    ///
84    /// ```text
85    /// |-----------------------------|
86    /// |             op              |
87    /// |-----------------------------|
88    /// |          variables          |
89    /// |-----------------------------|
90    /// |            stack            |
91    /// |-----------------------------|
92    /// |             buf             |
93    /// |-----------------------------|
94    /// |                             |
95    /// |             src             |
96    /// |                             |
97    /// |-----------------------------|
98    /// ```
99    fn vertical_layout(&mut self, f: &mut Frame<'_>) {
100        let area = f.area();
101        let footer_height = self.footer_height();
102
103        // Split off footer.
104        let [app, footer] = Layout::new(
105            Direction::Vertical,
106            [Constraint::Min(0), Constraint::Length(footer_height)],
107        )
108        .split(area)[..] else {
109            unreachable!()
110        };
111
112        if footer_height > 0 {
113            self.draw_footer(f, footer);
114        }
115
116        let opcodes_weight = if self.show_opcodes { 1 } else { 0 };
117        let source_weight = if self.show_source { 3 } else { 0 };
118        let data_weight = if self.show_data { if self.show_source { 1 } else { 2 } } else { 0 };
119        let total_weight = opcodes_weight
120            + data_weight
121            + source_weight
122            + if self.show_variables { 1 } else { 0 }
123            + if self.show_stack { 1 } else { 0 };
124        let mut constraints = Vec::with_capacity(5);
125        if self.show_opcodes {
126            constraints.push(Constraint::Ratio(opcodes_weight, total_weight));
127        }
128        if self.show_variables {
129            constraints.push(Constraint::Ratio(1, total_weight));
130        }
131        if self.show_stack {
132            constraints.push(Constraint::Ratio(1, total_weight));
133        }
134        if self.show_data {
135            constraints.push(Constraint::Ratio(data_weight, total_weight));
136        }
137        if self.show_source {
138            constraints.push(Constraint::Ratio(source_weight, total_weight));
139        }
140
141        let panes = Layout::new(Direction::Vertical, constraints).split(app);
142        let mut panes = panes.iter();
143        if self.show_opcodes {
144            self.draw_op_list(f, *panes.next().expect("opcodes pane is visible"));
145        }
146        if self.show_variables {
147            self.draw_variables(f, *panes.next().expect("variables pane is visible"));
148        }
149        if self.show_stack {
150            self.draw_stack(f, *panes.next().expect("stack pane is visible"));
151        }
152        if self.show_data {
153            self.draw_data(f, *panes.next().expect("data pane is visible"));
154        }
155        if self.show_source {
156            self.draw_src(f, *panes.next().expect("source pane is visible"));
157        }
158    }
159
160    /// Draws the layout in horizontal mode.
161    ///
162    /// ```text
163    /// |-----------------|-----------|
164    /// |        op       | variables |
165    /// |-----------------|-----------|
166    /// |                 |   stack   |
167    /// |       src       |-----------|
168    /// |                 |           |
169    /// |                 |    buf    |
170    /// |                 |           |
171    /// |-----------------|-----------|
172    /// ```
173    fn horizontal_layout(&mut self, f: &mut Frame<'_>) {
174        let area = f.area();
175        let footer_height = self.footer_height();
176
177        // Split off footer.
178        let [app, footer] = Layout::new(
179            Direction::Vertical,
180            [Constraint::Min(0), Constraint::Length(footer_height)],
181        )
182        .split(area)[..] else {
183            unreachable!()
184        };
185
186        let has_left_pane = self.show_opcodes || self.show_source;
187        let has_right_pane = self.show_variables || self.show_stack || self.show_data;
188        let (app_left, app_right) = match (has_left_pane, has_right_pane) {
189            (true, true) => {
190                let [app_left, app_right] = Layout::new(
191                    Direction::Horizontal,
192                    [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
193                )
194                .split(app)[..] else {
195                    unreachable!()
196                };
197                (Some(app_left), Some(app_right))
198            }
199            (true, false) => (Some(app), None),
200            (false, true) => (None, Some(app)),
201            (false, false) => (None, None),
202        };
203
204        if footer_height > 0 {
205            self.draw_footer(f, footer);
206        }
207        if let Some(app_left) = app_left {
208            let total_weight =
209                if self.show_opcodes { 1 } else { 0 } + if self.show_source { 3 } else { 0 };
210            let mut constraints = Vec::with_capacity(2);
211            if self.show_opcodes {
212                constraints.push(Constraint::Ratio(1, total_weight));
213            }
214            if self.show_source {
215                constraints.push(Constraint::Ratio(3, total_weight));
216            }
217
218            let panes = Layout::new(Direction::Vertical, constraints).split(app_left);
219            let mut panes = panes.iter();
220            if self.show_opcodes {
221                self.draw_op_list(f, *panes.next().expect("opcodes pane is visible"));
222            }
223            if self.show_source {
224                self.draw_src(f, *panes.next().expect("source pane is visible"));
225            }
226        }
227
228        if let Some(app_right) = app_right {
229            let total_weight = if self.show_variables { 1 } else { 0 }
230                + if self.show_stack { 1 } else { 0 }
231                + if self.show_data { 2 } else { 0 };
232            let mut constraints = Vec::with_capacity(3);
233            if self.show_variables {
234                constraints.push(Constraint::Ratio(1, total_weight));
235            }
236            if self.show_stack {
237                constraints.push(Constraint::Ratio(1, total_weight));
238            }
239            if self.show_data {
240                constraints.push(Constraint::Ratio(2, total_weight));
241            }
242
243            let panes = Layout::new(Direction::Vertical, constraints).split(app_right);
244            let mut panes = panes.iter();
245            if self.show_variables {
246                self.draw_variables(f, *panes.next().expect("variables pane is visible"));
247            }
248            if self.show_stack {
249                self.draw_stack(f, *panes.next().expect("stack pane is visible"));
250            }
251            if self.show_data {
252                self.draw_data(f, *panes.next().expect("data pane is visible"));
253            }
254        }
255    }
256
257    fn footer_height(&self) -> u16 {
258        let status_or_input = if matches!(self.prompt, Some(Prompt::Command(_))) {
259            3
260        } else {
261            u16::from(self.prompt.is_some() || self.status.is_some())
262        };
263        let shortcuts = if self.show_shortcuts { 3 } else { 0 };
264        status_or_input + shortcuts
265    }
266
267    fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
268        if let Some(Prompt::Command(input)) = &self.prompt {
269            self.draw_command_prompt(f, area, input);
270            return;
271        }
272
273        let mut lines = Vec::with_capacity(self.footer_height() as usize);
274
275        if let Some(Prompt::Pc(input)) = &self.prompt {
276            lines.push(Line::from(vec![
277                Span::styled(
278                    "Goto PC: ",
279                    Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
280                ),
281                Span::raw(input.as_str()),
282                Span::styled("█", Style::new().fg(Color::Cyan)),
283                Span::styled(
284                    "  Enter: jump | Esc: cancel | hex: 0x2a/2a | decimal: d:42",
285                    Style::new().add_modifier(Modifier::DIM),
286                ),
287            ]));
288        } else if let Some(Prompt::BufferOffset(input)) = &self.prompt {
289            lines.push(Line::from(vec![
290                Span::styled(
291                    format!("Goto {} offset: ", self.active_buffer_name()),
292                    Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
293                ),
294                Span::raw(input.as_str()),
295                Span::styled("█", Style::new().fg(Color::Cyan)),
296                Span::styled(
297                    "  Enter: jump | Esc: cancel | hex: 0x20/20 | decimal: d:32",
298                    Style::new().add_modifier(Modifier::DIM),
299                ),
300            ]));
301        } else if let Some(Prompt::OpcodeSearch(input)) = &self.prompt {
302            lines.push(Line::from(vec![
303                Span::styled(
304                    "Search opcodes: /",
305                    Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
306                ),
307                Span::raw(input.as_str()),
308                Span::styled("█", Style::new().fg(Color::Cyan)),
309                Span::styled(
310                    "  Enter: jump | Esc: cancel | after search: n/N repeat",
311                    Style::new().add_modifier(Modifier::DIM),
312                ),
313            ]));
314        } else if let Some(status) = &self.status {
315            let style = match status.kind {
316                StatusKind::Info => Style::new().fg(Color::Green),
317                StatusKind::Error => Style::new().fg(Color::Red).add_modifier(Modifier::BOLD),
318            };
319            lines.push(Line::from(Span::styled(status.text.as_str(), style)));
320        }
321
322        if self.show_shortcuts {
323            lines.extend(shortcut_lines());
324        }
325
326        let paragraph =
327            Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
328        f.render_widget(paragraph, area);
329    }
330
331    fn draw_command_prompt(&self, f: &mut Frame<'_>, area: Rect, input: &str) {
332        let shortcuts = if self.show_shortcuts { 3 } else { 0 };
333        let [prompt, shortcuts_area] = Layout::new(
334            Direction::Vertical,
335            [Constraint::Length(3), Constraint::Length(shortcuts)],
336        )
337        .split(area)[..] else {
338            unreachable!()
339        };
340
341        let prompt_line = command_prompt_line(input, prompt.width.saturating_sub(2));
342        let block = Block::default().title("Command").borders(Borders::ALL);
343        let paragraph = Paragraph::new(prompt_line).block(block);
344        f.render_widget(paragraph, prompt);
345
346        if self.show_shortcuts {
347            self.draw_shortcuts(f, shortcuts_area);
348        }
349    }
350
351    fn draw_shortcuts(&self, f: &mut Frame<'_>, area: Rect) {
352        let paragraph = Paragraph::new(shortcut_lines())
353            .alignment(Alignment::Center)
354            .wrap(Wrap { trim: false });
355        f.render_widget(paragraph, area);
356    }
357
358    fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
359        let (text_output, source_name) = self.src_text(area);
360        let call_kind_text = match self.call_kind() {
361            CallKind::Create | CallKind::Create2 => "Contract creation",
362            CallKind::Call => "Contract call",
363            CallKind::StaticCall => "Contract staticcall",
364            CallKind::CallCode => "Contract callcode",
365            CallKind::DelegateCall => "Contract delegatecall",
366            CallKind::AuthCall => "Contract authcall",
367        };
368        let title = source_pane_title(
369            call_kind_text,
370            source_name,
371            self.current_step_notice_text().map(step_notice_title),
372        );
373        let block = Block::default().title(title).borders(Borders::ALL);
374        let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
375        f.render_widget(paragraph, area);
376    }
377
378    fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
379        let (source_element, source) = match self.src_map() {
380            Ok(r) => r,
381            Err(e) => return (Text::from(e), None),
382        };
383
384        // We are handed a vector of SourceElements that give us a span of sourcecode that is
385        // currently being executed. This includes an offset and length.
386        // This vector is in instruction pointer order, meaning the location of the instruction
387        // minus `sum(push_bytes[..pc])`.
388        let offset = source_element.offset() as usize;
389        let len = source_element.length() as usize;
390        let max = source.source.len();
391
392        // Split source into before, relevant, and after chunks, split by line, for formatting.
393        let actual_start = offset.min(max);
394        let actual_end = (offset + len).min(max);
395
396        let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
397        let actual: Vec<_> =
398            source.source[actual_start..actual_end].split_inclusive('\n').collect();
399        let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
400
401        let num_lines = before.len() + actual.len() + after.len();
402        let height = area.height as usize;
403        let needed_highlight = actual.len();
404        let mid_len = before.len() + actual.len();
405
406        // adjust what text we show of the source code
407        let (start_line, end_line) = if needed_highlight > height {
408            // highlighted section is more lines than we have available
409            let start_line = before.len().saturating_sub(1);
410            (start_line, before.len() + needed_highlight)
411        } else if height > num_lines {
412            // we can fit entire source
413            (0, num_lines)
414        } else {
415            let remaining = height - needed_highlight;
416            let mut above = remaining / 2;
417            let mut below = remaining / 2;
418            if below > after.len() {
419                // unused space below the highlight
420                above += below - after.len();
421            } else if above > before.len() {
422                // we have unused space above the highlight
423                below += above - before.len();
424            } else {
425                // no unused space
426            }
427
428            // since above is subtracted from before.len(), and the resulting
429            // start_line is used to index into before, above must be at least
430            // 1 to avoid out-of-range accesses.
431            if above == 0 {
432                above = 1;
433            }
434            (before.len().saturating_sub(above), mid_len + below)
435        };
436
437        // Unhighlighted line number: gray.
438        let u_num = Style::new().fg(Color::Gray);
439        // Unhighlighted text: default, dimmed.
440        let u_text = Style::new().add_modifier(Modifier::DIM);
441        // Highlighted line number: cyan.
442        let h_num = Style::new().fg(Color::Cyan);
443        // Highlighted text: cyan, bold.
444        let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
445
446        let mut lines = SourceLines::new(start_line, end_line);
447
448        // We check if there is other text on the same line before the highlight starts.
449        if let Some(last) = before.pop() {
450            let last_has_nl = last.ends_with('\n');
451
452            if last_has_nl {
453                before.push(last);
454            }
455            for line in &before[start_line..] {
456                lines.push(u_num, line, u_text);
457            }
458
459            let first = if last_has_nl {
460                0
461            } else {
462                lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
463                1
464            };
465
466            // Skip the first line if it has already been handled above.
467            for line in &actual[first..] {
468                lines.push(h_num, line, h_text);
469            }
470        } else {
471            // No text before the current line.
472            for line in &actual {
473                lines.push(h_num, line, h_text);
474            }
475        }
476
477        // Fill in the rest of the line as unhighlighted.
478        if let Some(last) = actual.last()
479            && !last.ends_with('\n')
480            && let Some(post) = after.pop_front()
481            && let Some(last) = lines.lines.last_mut()
482        {
483            last.spans.push(Span::raw(post));
484        }
485
486        // Add after highlighted text.
487        while mid_len + after.len() > end_line {
488            after.pop_back();
489        }
490        for line in after {
491            lines.push(u_num, line, u_text);
492        }
493
494        // pad with empty to each line to ensure the previous text is cleared
495        for line in &mut lines.lines {
496            // note that the \n is not included in the line length
497            if area.width as usize > line.width() + 1 {
498                line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
499            }
500        }
501
502        (Text::from(lines.lines), source.path.to_str())
503    }
504
505    fn current_step_notice_text(&self) -> Option<&str> {
506        let DecodedTraceStep::Line(line) = self.current_step().decoded.as_deref()? else {
507            return None;
508        };
509        (!line.is_empty()).then_some(line.as_str())
510    }
511
512    fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
513        let debug_steps = self.debug_steps();
514        // Opcode items are one line each; window them before `List::new` collects the iterator.
515        let visible_rows = area.height.saturating_sub(2) as usize;
516        let scroll_padding = usize::from(visible_rows >= 3);
517        let end = debug_steps
518            .len()
519            .min(visible_rows.max(self.current_step.saturating_add(scroll_padding + 1)));
520        let start = end.saturating_sub(visible_rows);
521        let pc_width = hex_digits(self.opcode_max_pc());
522        let items = debug_steps[start..end].iter().map(|step| {
523            let mut row = String::with_capacity(pc_width + 1 + 8);
524            write!(row, "{:0>pc_width$x}|", step.pc).unwrap();
525            write_pretty_opcode(&mut row, step);
526            ListItem::new(Span::styled(row, Style::new().fg(Color::White)))
527        });
528
529        let step = self.current_step();
530        let call_gas_used = self.debug_call().gas_limit.saturating_sub(step.gas_remaining);
531        let title = op_list_title(
532            self.address(),
533            step.pc,
534            step.gas_remaining,
535            call_gas_used,
536            step.gas_refund_counter,
537            self.debugger_context.stats,
538        );
539        let block = Block::default().title(title).borders(Borders::ALL);
540        let list = List::new(items)
541            .block(block)
542            .highlight_symbol("▶")
543            .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
544            .scroll_padding(scroll_padding);
545        let mut state =
546            ListState::default().with_selected(Some(self.current_step.saturating_sub(start)));
547        f.render_stateful_widget(list, area, &mut state);
548    }
549
550    fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
551        let step = self.current_step();
552        let stack = step.stack.as_ref();
553        let stack_len = stack.map_or(0, |s| s.len());
554
555        let min_len = decimal_digits(stack_len).max(2);
556
557        let params = OpcodeParam::of(step.op.get());
558
559        let text: Vec<Line<'_>> = stack
560            .map(|stack| {
561                stack
562                    .iter()
563                    .rev()
564                    .enumerate()
565                    .skip(self.draw_memory.current_stack_startline)
566                    .map(|(i, stack_item)| {
567                        let param = params.iter().find(|param| param.index == i);
568                        stack_item_line(i, min_len, stack_item, param, self.stack_labels)
569                    })
570                    .collect()
571            })
572            .unwrap_or_default();
573
574        let title = format!("Stack: {stack_len}");
575        let block = Block::default().title(title).borders(Borders::ALL);
576        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
577        f.render_widget(paragraph, area);
578    }
579
580    fn draw_variables(&mut self, f: &mut Frame<'_>, area: Rect) {
581        let variables = self.scope_variables();
582        let storage_access = self.current_storage_access_line();
583        let step_notice = self.current_step_notice_text().map(step_notice_line);
584        let known = variables.iter().filter(|variable| variable.value.is_some()).count();
585        let title = variables_title(
586            variables.len(),
587            known,
588            storage_access.is_some(),
589            step_notice.is_some(),
590        );
591
592        let mut text = variables.into_iter().map(scope_variable_line).collect::<Vec<_>>();
593        if let Some(step_notice) = step_notice {
594            if !text.is_empty() {
595                text.push(Line::from(""));
596            }
597            text.push(step_notice);
598        }
599        if let Some(storage_access) = storage_access {
600            if !text.is_empty() {
601                text.push(Line::from(""));
602            }
603            text.push(storage_access);
604        }
605
606        if text.is_empty() {
607            text.push(Line::from(Span::styled(
608                "No variables in current scope",
609                Style::new().add_modifier(Modifier::DIM),
610            )));
611        }
612
613        let block = Block::default().title(title).borders(Borders::ALL);
614        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
615        f.render_widget(paragraph, area);
616    }
617
618    fn current_storage_access_line(&self) -> Option<Line<'static>> {
619        storage_access_at(self.debug_steps(), self.current_step).map(|access| {
620            let values = (access.space() == StorageSpace::Persistent)
621                .then(|| storage_values(&self.storage_accesses(access.space())));
622            let next_values = (access.space() == StorageSpace::Persistent)
623                .then(|| self.next_storage_write_values());
624            let label = self.storage_label(
625                access.space(),
626                access.slot(),
627                values.as_ref(),
628                next_values.as_ref(),
629            );
630            storage_access_line(access, label.as_deref())
631        })
632    }
633
634    fn draw_data(&mut self, f: &mut Frame<'_>, area: Rect) {
635        if let Some(space) = self.active_storage() {
636            self.draw_storage(f, area, space);
637        } else {
638            self.draw_buffer(f, area);
639        }
640    }
641
642    fn draw_storage(&mut self, f: &mut Frame<'_>, area: Rect, space: StorageSpace) {
643        let accesses = self.storage_accesses(space);
644        let values = (space == StorageSpace::Persistent).then(|| storage_values(&accesses));
645        let next_values =
646            (space == StorageSpace::Persistent).then(|| self.next_storage_write_values());
647        let current_slot = storage_access_at(self.debug_steps(), self.current_step)
648            .filter(|access| access.space() == space)
649            .map(StorageAccess::slot);
650        self.draw_memory.current_storage_startline =
651            self.draw_memory.current_storage_startline.min(accesses.len().saturating_sub(1));
652
653        let index_width = decimal_digits(accesses.len()).max(2);
654        let mut lines = accesses
655            .values()
656            .copied()
657            .enumerate()
658            .skip(self.draw_memory.current_storage_startline)
659            .flat_map(|(index, access)| {
660                let label =
661                    self.storage_label(space, access.slot(), values.as_ref(), next_values.as_ref());
662                storage_slot_lines(
663                    index,
664                    index_width,
665                    access,
666                    label.as_deref(),
667                    current_slot == Some(access.slot()),
668                )
669            })
670            .collect::<Vec<_>>();
671        if lines.is_empty() {
672            lines.push(Line::from(Span::styled(
673                format!("No {} accessed in current call", space.noun()),
674                Style::new().add_modifier(Modifier::DIM),
675            )));
676        }
677
678        let count = accesses.len();
679        let suffix = if count == 1 { "slot" } else { "slots" };
680        let title = format!("{}: {count} accessed {suffix}", space.label());
681        let block = Block::default().title(title).borders(Borders::ALL);
682        let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
683        f.render_widget(paragraph, area);
684    }
685
686    fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
687        let call = self.debug_call();
688        let step = self.current_step();
689        let buf: &[u8] = match self.active_buffer {
690            BufferKind::Memory => step.memory.as_ref().map_or(&[], |memory| memory.as_ref()),
691            BufferKind::Calldata => call.calldata.as_ref(),
692            BufferKind::Returndata => step.returndata.as_ref(),
693        };
694
695        let min_len = hex_digits(buf.len());
696
697        // Color memory region based on read/write.
698        let mut offset = None;
699        let mut len = None;
700        let mut write_offset = None;
701        let mut write_size = None;
702        let mut color = None;
703        let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
704        if stack_len > 0
705            && let Some(stack) = step.stack.as_ref()
706            && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
707        {
708            if let Some(read_access) = accesses.read {
709                offset = Some(read_access.1.offset);
710                len = Some(read_access.1.len);
711                color = Some(Color::Cyan);
712            }
713            if let Some(write_access) = accesses.write
714                && self.active_buffer == BufferKind::Memory
715            {
716                write_offset = Some(write_access.offset);
717                write_size = Some(write_access.len);
718            }
719        }
720
721        // color word on previous write op
722        // TODO: technically it's possible for this to conflict with the current op, ie, with
723        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
724        // settings
725        if self.current_step > 0 {
726            let prev_step = self.current_step - 1;
727            let prev_step = &self.debug_steps()[prev_step];
728            if let Some(stack) = prev_step.stack.as_ref()
729                && let Some(write_access) =
730                    get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
731                && self.active_buffer == BufferKind::Memory
732            {
733                offset = Some(write_access.offset);
734                len = Some(write_access.len);
735                color = Some(Color::Green);
736            }
737        }
738
739        let height = area.height as usize;
740        let end_line = self.draw_memory.current_buf_startline + height;
741
742        let text: Vec<Line<'_>> = buf
743            .chunks(32)
744            .enumerate()
745            .skip(self.draw_memory.current_buf_startline)
746            .take_while(|(i, _)| *i < end_line)
747            .map(|(i, buf_word)| {
748                let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
749
750                // Buffer index.
751                spans.push(Span::styled(
752                    format!("{:0min_len$x}| ", i * 32),
753                    Style::new().fg(Color::White),
754                ));
755
756                // Word hex bytes.
757                hex_bytes_spans(buf_word, &mut spans, |j, _| {
758                    let mut byte_color = Color::White;
759                    let mut end = None;
760                    let idx = i * 32 + j;
761                    if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
762                        let offset_end = offset.saturating_add(len);
763                        end = Some(offset_end);
764                        if (offset..offset_end).contains(&idx) {
765                            // [offset, offset + len) is the memory region to be colored.
766                            // If a byte at row i and column j in the memory panel
767                            // falls in this region, set the color.
768                            byte_color = color;
769                        }
770                    }
771                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
772                        // check for overlap with read region
773                        let write_end = write_offset.saturating_add(write_size);
774                        if let Some(read_end) = end {
775                            let read_start = offset.unwrap();
776                            if (write_offset..write_end).contains(&read_end) {
777                                // if it contains end, start from write_start up to read_end
778                                if (write_offset..read_end).contains(&idx) {
779                                    return Style::new().fg(Color::Yellow);
780                                }
781                            } else if (write_offset..write_end).contains(&read_start) {
782                                // otherwise if it contains read start, start from read_start up to
783                                // write_end
784                                if (read_start..write_end).contains(&idx) {
785                                    return Style::new().fg(Color::Yellow);
786                                }
787                            }
788                        }
789                        if (write_offset..write_end).contains(&idx) {
790                            byte_color = Color::Red;
791                        }
792                    }
793
794                    Style::new().fg(byte_color)
795                });
796
797                if self.buf_utf {
798                    spans.push(Span::raw("|"));
799                    for utf in buf_word.chunks(4) {
800                        if let Ok(utf_str) = std::str::from_utf8(utf) {
801                            spans.push(Span::raw(utf_str.replace('\0', ".")));
802                        } else {
803                            spans.push(Span::raw("."));
804                        }
805                    }
806                }
807
808                spans.push(Span::raw("\n"));
809
810                Line::from(spans)
811            })
812            .collect();
813
814        let title = self.active_buffer.title(buf.len());
815        let block = Block::default().title(title).borders(Borders::ALL);
816        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
817        f.render_widget(paragraph, area);
818    }
819}
820
821#[derive(Clone, Debug, PartialEq, Eq)]
822struct ScopeVariable {
823    kind: ScopeVariableKind,
824    name: String,
825    value: Option<String>,
826}
827
828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
829enum ScopeVariableKind {
830    Parameter,
831    Return,
832    Local,
833}
834
835struct ActiveInternalCall<'a> {
836    trace_node_idx: usize,
837    entry_step: usize,
838    end_step: usize,
839    decoded: &'a DecodedInternalCall,
840}
841
842const PRECOMPILE_NOTICE_PREFIX: &str = "precompile:";
843
844impl ScopeVariableKind {
845    const fn label(self) -> &'static str {
846        match self {
847            Self::Parameter => "param",
848            Self::Return => "return",
849            Self::Local => "local",
850        }
851    }
852
853    const fn color(self) -> Color {
854        match self {
855            Self::Parameter => Color::Cyan,
856            Self::Return => Color::Green,
857            Self::Local => Color::White,
858        }
859    }
860}
861
862impl TUIContext<'_> {
863    fn scope_variables(&mut self) -> Vec<ScopeVariable> {
864        let (scope, start) = {
865            let Ok((source_element, source)) = self.src_map() else {
866                return Vec::new();
867            };
868            let start = source_element.offset() as usize;
869            let end = start.saturating_add(source_element.length() as usize);
870            let Some(scope) = source.find_debug_scope(start, end) else {
871                return Vec::new();
872            };
873            (scope.clone(), start)
874        };
875
876        let parameter_values = self.decode_parameter_values(&scope);
877        let return_values = self.decode_return_values(&scope);
878        let mut variables = Vec::new();
879
880        variables.extend(scope.parameters.iter().enumerate().map(|(i, variable)| ScopeVariable {
881            kind: ScopeVariableKind::Parameter,
882            name: variable_name(variable, i, "arg"),
883            value: parameter_values.as_ref().and_then(|values| values.get(i).cloned()),
884        }));
885
886        variables.extend(scope.returns.iter().enumerate().map(|(i, variable)| ScopeVariable {
887            kind: ScopeVariableKind::Return,
888            name: variable_name(variable, i, "ret"),
889            value: return_values.as_ref().and_then(|values| values.get(i).cloned()),
890        }));
891
892        variables.extend(scope.visible_locals(start).enumerate().map(|(i, variable)| {
893            ScopeVariable {
894                kind: ScopeVariableKind::Local,
895                name: variable_name(variable, i, "local"),
896                value: None,
897            }
898        }));
899
900        variables
901    }
902
903    fn decode_parameter_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
904        let scope_signature = scope_function_signature(scope);
905        self.decode_internal_parameter_values(scope)
906            .or_else(|| decode_external_parameter_values(scope, &self.debug_call().calldata))
907            .or_else(|| {
908                self.debug_call().decoded.as_ref().and_then(|decoded| {
909                    let call_data = decoded.call_data.as_ref()?;
910                    scope_signature
911                        .as_deref()
912                        .is_some_and(|signature| signature == call_data.signature)
913                        .then(|| call_data.args.clone())
914                })
915            })
916    }
917
918    fn decode_return_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
919        let current_step = self.absolute_current_step();
920        if let Some(values) = self
921            .active_internal_call()
922            .and_then(|active| {
923                (current_step >= active.end_step
924                    && decoded_internal_name_matches(&active.decoded.func_name, scope))
925                .then(|| active.decoded.return_data.clone())
926            })
927            .flatten()
928        {
929            return Some(values);
930        }
931
932        if self.current_step + 1 < self.debug_steps().len()
933            || !matches!(
934                self.current_step().status,
935                Some(InstructionResult::Return | InstructionResult::Stop)
936            )
937        {
938            return None;
939        }
940
941        decode_external_return_values(
942            scope,
943            &self.debug_call().calldata,
944            &self.debug_call().returndata,
945        )
946    }
947
948    fn decode_internal_parameter_values(
949        &mut self,
950        scope: &DebugSourceScope,
951    ) -> Option<Vec<String>> {
952        let (args, trace_node_idx, entry_step) = {
953            let active = self.active_internal_call()?;
954            if !decoded_internal_name_matches(&active.decoded.func_name, scope) {
955                return None;
956            }
957
958            (active.decoded.args.clone(), active.trace_node_idx, active.entry_step)
959        };
960
961        if let Some(args) = args {
962            return Some(args);
963        }
964
965        let parameters = Parameters::parse(&scope.parameters_src).ok()?;
966        let node = self.debug_arena().iter().find(|node| {
967            node.trace_node_idx == trace_node_idx
968                && entry_step >= node.step_offset
969                && entry_step < node.step_offset.saturating_add(node.steps.len())
970        })?;
971        let step = node.steps.get(entry_step.checked_sub(node.step_offset)?)?;
972        decode_step_parameters(&parameters, step, Some(node.calldata.as_ref()))
973    }
974
975    fn active_internal_call(&mut self) -> Option<ActiveInternalCall<'_>> {
976        let current_node_idx = self.draw_memory.inner_call_index;
977        let trace_node_idx = self.debug_call().trace_node_idx;
978        let current_step = self.absolute_current_step();
979        let location = if let Some(cache) = self.draw_memory.active_internal_call
980            && cache.matches(current_node_idx, trace_node_idx, current_step)
981        {
982            cache.location
983        } else {
984            let location =
985                self.find_active_internal_call(current_node_idx, trace_node_idx, current_step);
986            self.draw_memory.active_internal_call = Some(ActiveInternalCallCache {
987                current_node_idx,
988                trace_node_idx,
989                absolute_step: current_step,
990                location,
991            });
992            location
993        }?;
994
995        self.active_internal_call_at(location)
996    }
997
998    fn find_active_internal_call(
999        &self,
1000        current_node_idx: usize,
1001        trace_node_idx: usize,
1002        current_step: usize,
1003    ) -> Option<ActiveInternalCallLocation> {
1004        let mut active = None;
1005
1006        for (node_idx, node) in
1007            self.debug_arena().iter().enumerate().take(current_node_idx.saturating_add(1))
1008        {
1009            if node.trace_node_idx != trace_node_idx {
1010                continue;
1011            }
1012
1013            for (step_idx, step) in node.steps.iter().enumerate() {
1014                let marker_step = node.step_offset.saturating_add(step_idx);
1015                if marker_step > current_step {
1016                    break;
1017                }
1018
1019                let Some(decoded) = step.decoded.as_deref() else { continue };
1020                let DecodedTraceStep::InternalCall(_, end_step) = decoded else { continue };
1021                if current_step <= *end_step {
1022                    active = Some(ActiveInternalCallLocation {
1023                        trace_node_idx,
1024                        marker_node_idx: node_idx,
1025                        marker_step_idx: step_idx,
1026                        entry_step: marker_step.saturating_add(1),
1027                        end_step: *end_step,
1028                    });
1029                }
1030            }
1031        }
1032
1033        active
1034    }
1035
1036    fn active_internal_call_at(
1037        &self,
1038        location: ActiveInternalCallLocation,
1039    ) -> Option<ActiveInternalCall<'_>> {
1040        let step = self
1041            .debug_arena()
1042            .get(location.marker_node_idx)?
1043            .steps
1044            .get(location.marker_step_idx)?;
1045        let DecodedTraceStep::InternalCall(decoded, end_step) = step.decoded.as_deref()? else {
1046            return None;
1047        };
1048        (*end_step == location.end_step).then_some(ActiveInternalCall {
1049            trace_node_idx: location.trace_node_idx,
1050            entry_step: location.entry_step,
1051            end_step: location.end_step,
1052            decoded,
1053        })
1054    }
1055
1056    fn absolute_current_step(&self) -> usize {
1057        self.debug_call().step_offset.saturating_add(self.current_step)
1058    }
1059}
1060
1061fn source_pane_title(
1062    call_kind_text: &str,
1063    source_name: Option<&str>,
1064    step_notice: Option<&str>,
1065) -> String {
1066    let mut title = call_kind_text.to_string();
1067    if let Some(step_notice) = step_notice {
1068        write!(title, " | {step_notice}").unwrap();
1069    }
1070    if let Some(source_name) = source_name {
1071        write!(title, " | {source_name}").unwrap();
1072    }
1073    title.push(' ');
1074    title
1075}
1076
1077fn variables_title(
1078    total_variables: usize,
1079    known_variables: usize,
1080    has_storage_access: bool,
1081    has_step_notice: bool,
1082) -> String {
1083    if total_variables == 0 && !has_storage_access && !has_step_notice {
1084        return "Variables".to_string();
1085    }
1086
1087    let mut title = format!("Variables: {known_variables}/{total_variables}");
1088    if has_step_notice {
1089        title.push_str(" | Trace");
1090    }
1091    if has_storage_access {
1092        title.push_str(" | Storage");
1093    }
1094    title
1095}
1096
1097fn shortcut_lines() -> Vec<Line<'static>> {
1098    let dimmed = Style::new().add_modifier(Modifier::DIM);
1099    vec![
1100        Line::from(Span::styled(
1101            "[q] quit | [j/k] op | [a/s] jump | [c/C] call | [g/G] start/end | [p] PC | [o] offset",
1102            dimmed,
1103        )),
1104        Line::from(Span::styled(
1105            "[/] search | [:] command | [n/N] repeat | [l] layout | [b] buffer",
1106            dimmed,
1107        )),
1108        Line::from(Span::styled(
1109            "[t] labels | [m] decode | [h] help | [J/K] stack scroll | [ctrl+j/k] data scroll | ['<char>] breakpoint",
1110            dimmed,
1111        )),
1112    ]
1113}
1114
1115const COMMAND_PROMPT_HINT: &str = "  Enter: run | Esc: cancel | help: command list";
1116
1117fn command_prompt_line(input: &str, width: u16) -> Line<'static> {
1118    let width = width as usize;
1119    let fixed_width = 2;
1120    let hint_width = text_width(COMMAND_PROMPT_HINT);
1121    let input_width = text_width(input);
1122    let include_hint = fixed_width + input_width + hint_width <= width;
1123    let input_width = if include_hint {
1124        width.saturating_sub(fixed_width + hint_width)
1125    } else {
1126        width.saturating_sub(fixed_width)
1127    };
1128
1129    let mut spans = vec![
1130        Span::styled(":", Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
1131        Span::raw(input_tail(input, input_width)),
1132        Span::styled("█", Style::new().fg(Color::Cyan)),
1133    ];
1134    if include_hint {
1135        spans.push(Span::styled(COMMAND_PROMPT_HINT, Style::new().add_modifier(Modifier::DIM)));
1136    }
1137    Line::from(spans)
1138}
1139
1140/// Returns the rendered display width of `text` in terminal cells.
1141fn text_width(text: &str) -> usize {
1142    Span::raw(text).width()
1143}
1144
1145fn input_tail(input: &str, max_width: usize) -> String {
1146    if text_width(input) <= max_width {
1147        return input.to_string();
1148    }
1149    if max_width == 0 {
1150        return String::new();
1151    }
1152    if max_width == 1 {
1153        return "<".to_string();
1154    }
1155
1156    // Reserve one cell for the leading `<` truncation indicator, then keep the widest
1157    // suffix that fits in the remaining width.
1158    let tail_width = max_width - 1;
1159    let mut start = input.len();
1160    for (idx, _) in input.char_indices().rev() {
1161        if text_width(&input[idx..]) > tail_width {
1162            break;
1163        }
1164        start = idx;
1165    }
1166    format!("<{}", &input[start..])
1167}
1168
1169fn step_notice_title(line: &str) -> &'static str {
1170    if line.starts_with(PRECOMPILE_NOTICE_PREFIX) { "precompile call" } else { "decoded step" }
1171}
1172
1173fn step_notice_line(line: &str) -> Line<'static> {
1174    Line::from(Span::styled(line.to_string(), Style::new().fg(Color::Magenta)))
1175}
1176
1177fn scope_variable_line(variable: ScopeVariable) -> Line<'static> {
1178    let color = variable.kind.color();
1179    let mut spans = Vec::with_capacity(6);
1180    spans.push(Span::styled(variable.kind.label(), Style::new().fg(Color::Gray)));
1181    spans.push(Span::raw(" "));
1182    spans.push(Span::styled(variable.name, Style::new().fg(color).add_modifier(Modifier::BOLD)));
1183    spans.push(Span::raw(" = "));
1184    if let Some(value) = variable.value {
1185        spans.push(Span::styled(value, Style::new().fg(color)));
1186    } else {
1187        spans.push(Span::styled("<unavailable>", Style::new().fg(Color::Gray)));
1188    }
1189    Line::from(spans)
1190}
1191
1192fn storage_access_line(access: StorageAccess, label: Option<&str>) -> Line<'static> {
1193    let mut spans = vec![Span::styled(access.describe(), Style::new().fg(Color::Yellow))];
1194    if let Some(label) = label {
1195        spans.extend([
1196            Span::raw(" ("),
1197            Span::styled(label.to_string(), Color::Cyan),
1198            Span::raw(")"),
1199        ]);
1200    }
1201    Line::from(spans)
1202}
1203
1204fn storage_slot_lines(
1205    index: usize,
1206    index_width: usize,
1207    access: StorageAccess,
1208    label: Option<&str>,
1209    current: bool,
1210) -> [Line<'static>; 2] {
1211    let value_style = if current {
1212        Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
1213    } else {
1214        Style::new().fg(Color::White)
1215    };
1216    let prefix_width = index_width + 2;
1217    let mut slot_spans = vec![
1218        Span::styled(format!("{index:0index_width$}| "), Style::new().fg(Color::Gray)),
1219        Span::styled(access.op(), value_style),
1220        Span::raw(" slot "),
1221        Span::styled(hex_u256(access.slot()), value_style),
1222    ];
1223    if let Some(label) = label {
1224        slot_spans.extend([
1225            Span::raw(" ("),
1226            Span::styled(label.to_string(), Style::new().fg(Color::Cyan)),
1227            Span::raw(")"),
1228        ]);
1229    }
1230    [
1231        Line::from(slot_spans),
1232        Line::from(vec![
1233            Span::raw(" ".repeat(prefix_width)),
1234            Span::raw("value "),
1235            Span::styled(hex_u256(access.value()), value_style),
1236        ]),
1237    ]
1238}
1239
1240fn variable_name(variable: &DebugVariable, index: usize, fallback_prefix: &str) -> String {
1241    variable
1242        .name
1243        .as_deref()
1244        .filter(|name| !name.is_empty())
1245        .map(ToOwned::to_owned)
1246        .unwrap_or_else(|| format!("{fallback_prefix}{index}"))
1247}
1248
1249fn decoded_internal_name_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1250    if let Some((contract_name, function_name)) = decoded_name.rsplit_once("::") {
1251        return contract_name == scope.contract_name
1252            && decoded_function_matches(function_name, scope);
1253    }
1254    decoded_function_matches(decoded_name, scope)
1255}
1256
1257fn decoded_function_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1258    if decoded_name == scope.function_name {
1259        return true;
1260    }
1261    scope_function_signature(scope).as_deref().is_some_and(|signature| decoded_name == signature)
1262}
1263
1264fn decode_external_parameter_values(
1265    scope: &DebugSourceScope,
1266    calldata: &[u8],
1267) -> Option<Vec<String>> {
1268    let types = external_scope_parameter_types(scope, calldata)?;
1269
1270    decode_abi_sequence(&types, &calldata[4..])
1271}
1272
1273fn decode_external_return_values(
1274    scope: &DebugSourceScope,
1275    calldata: &[u8],
1276    returndata: &[u8],
1277) -> Option<Vec<String>> {
1278    external_scope_parameter_types(scope, calldata)?;
1279    let returns_src = scope.returns_src.as_deref()?;
1280    let returns = Parameters::parse(returns_src).ok()?;
1281    let types = resolved_types(&returns)?;
1282    decode_abi_sequence(&types, returndata)
1283}
1284
1285fn external_scope_parameter_types(
1286    scope: &DebugSourceScope,
1287    calldata: &[u8],
1288) -> Option<Vec<DynSolType>> {
1289    if calldata.len() < 4 {
1290        return None;
1291    }
1292
1293    let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1294    let types = resolved_types(&parameters)?;
1295    let selector = function_selector(&scope.function_name, &types);
1296    if calldata.get(..4)? != selector.as_slice() {
1297        return None;
1298    }
1299
1300    Some(types)
1301}
1302
1303fn resolved_types(parameters: &Parameters<'_>) -> Option<Vec<DynSolType>> {
1304    parameters.params.iter().map(|param| param.resolve().ok()).collect()
1305}
1306
1307fn scope_function_signature(scope: &DebugSourceScope) -> Option<String> {
1308    let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1309    let types = resolved_types(&parameters)?;
1310    Some(function_signature(&scope.function_name, &types))
1311}
1312
1313fn function_selector(function_name: &str, types: &[DynSolType]) -> [u8; 4] {
1314    let signature = function_signature(function_name, types);
1315    keccak256(signature.as_bytes())[..4].try_into().unwrap()
1316}
1317
1318fn decode_abi_sequence(types: &[DynSolType], data: &[u8]) -> Option<Vec<String>> {
1319    if types.is_empty() {
1320        return Some(Vec::new());
1321    }
1322
1323    let value = DynSolType::Tuple(types.to_vec()).abi_decode_sequence(data).ok()?;
1324    let values = value.as_fixed_seq()?;
1325    Some(values.iter().map(format_token).collect())
1326}
1327
1328fn op_list_title(
1329    address: &Address,
1330    pc: usize,
1331    gas_remaining: u64,
1332    call_gas_used: u64,
1333    gas_refund_counter: u64,
1334    stats: Option<DebuggerStats>,
1335) -> String {
1336    let address = full_checksum_address(address);
1337    let mut title = format!(
1338        "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
1339         callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
1340    );
1341
1342    if let Some(stats) = stats {
1343        write!(
1344            title,
1345            " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
1346            stats.session_trace_gas_used, stats.session_subcalls
1347        )
1348        .unwrap();
1349    }
1350
1351    title
1352}
1353
1354fn full_checksum_address(address: &Address) -> String {
1355    address.to_string()
1356}
1357
1358fn stack_item_line(
1359    i: usize,
1360    min_len: usize,
1361    stack_item: &U256,
1362    param: Option<&OpcodeParam>,
1363    stack_labels: bool,
1364) -> Line<'static> {
1365    let value_style =
1366        if param.is_some() { Style::new().fg(Color::Cyan) } else { Style::new().fg(Color::White) };
1367    let mut spans = Vec::with_capacity(1 + 32 * 2 + 5);
1368
1369    // Stack index.
1370    spans.push(Span::styled(format!("{i:0min_len$}| "), Style::new().fg(Color::White)));
1371
1372    // Item hex bytes.
1373    hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| value_style);
1374
1375    spans.push(Span::raw(" | "));
1376    spans.push(Span::styled(stack_item.to_string(), value_style));
1377
1378    if stack_labels && let Some(param) = param {
1379        spans.push(Span::raw(" | "));
1380        spans.push(Span::raw(param.name));
1381    }
1382
1383    spans.push(Span::raw("\n"));
1384
1385    Line::from(spans)
1386}
1387
1388/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
1389struct SourceLines<'a> {
1390    lines: Vec<Line<'a>>,
1391    start_line: usize,
1392    max_line_num: usize,
1393}
1394
1395impl<'a> SourceLines<'a> {
1396    fn new(start_line: usize, end_line: usize) -> Self {
1397        Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
1398    }
1399
1400    fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
1401        self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
1402    }
1403
1404    fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
1405        let mut line_spans = Vec::with_capacity(4);
1406
1407        let line_number = format!(
1408            "{number: >width$} ",
1409            number = self.start_line + self.lines.len() + 1,
1410            width = self.max_line_num
1411        );
1412        line_spans.push(Span::styled(line_number, line_number_style));
1413
1414        // Space between line number and line text.
1415        line_spans.push(Span::raw("  "));
1416
1417        line_spans.extend_from_slice(spans);
1418
1419        self.lines.push(Line::from(line_spans));
1420    }
1421}
1422
1423fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
1424    for (i, &byte) in bytes.iter().enumerate() {
1425        if i > 0 {
1426            spans.push(Span::raw(" "));
1427        }
1428        spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
1429    }
1430}
1431
1432/// Returns the number of decimal digits in the given number.
1433///
1434/// This is the same as `n.to_string().len()`.
1435fn decimal_digits(n: usize) -> usize {
1436    n.checked_ilog10().unwrap_or(0) as usize + 1
1437}
1438
1439/// Returns the number of hexadecimal digits in the given number.
1440///
1441/// This is the same as `format!("{n:x}").len()`.
1442fn hex_digits(n: usize) -> usize {
1443    n.checked_ilog(16).unwrap_or(0) as usize + 1
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448    use super::{Prompt, TUIContext};
1449    use crate::{
1450        DebugNode, DebuggerLayout,
1451        debugger::{DebuggerContext, DebuggerStats},
1452        op::OpcodeParam,
1453    };
1454    use alloy_dyn_abi::parser::Parameters;
1455    use alloy_primitives::{Address, Bytes, U256, address};
1456    use foundry_evm_core::{Breakpoints, buffer::BufferKind};
1457    use foundry_evm_traces::debug::{ContractSources, DebugSourceScope, DebugVariable};
1458    use ratatui::{
1459        Terminal,
1460        backend::TestBackend,
1461        layout::Rect,
1462        style::{Color, Style},
1463        text::Line,
1464    };
1465    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
1466    use revm_inspectors::tracing::types::{
1467        CallKind, CallTraceStep, DecodedCallData, DecodedCallTrace, DecodedInternalCall,
1468        DecodedTraceStep, StorageChange, StorageChangeReason,
1469    };
1470
1471    fn line_text(line: &Line<'_>) -> String {
1472        line.spans.iter().map(|span| span.content.as_ref()).collect()
1473    }
1474
1475    fn scope(function_name: &str, parameters_src: &str) -> DebugSourceScope {
1476        DebugSourceScope {
1477            contract_name: "DebugMe".to_string(),
1478            function_name: function_name.to_string(),
1479            range: 0..100,
1480            body_range: 10..90,
1481            parameters_src: parameters_src.to_string(),
1482            returns_src: None,
1483            parameters: Vec::new(),
1484            returns: Vec::new(),
1485            locals: Vec::new(),
1486        }
1487    }
1488
1489    fn scope_with_returns(
1490        function_name: &str,
1491        parameters_src: &str,
1492        returns_src: &str,
1493    ) -> DebugSourceScope {
1494        DebugSourceScope {
1495            returns_src: Some(returns_src.to_string()),
1496            ..scope(function_name, parameters_src)
1497        }
1498    }
1499
1500    fn trace_step(stack: Vec<U256>) -> CallTraceStep {
1501        CallTraceStep {
1502            pc: 0,
1503            op: OpCode::STOP,
1504            stack: Some(stack.into_boxed_slice()),
1505            push_stack: None,
1506            memory: None,
1507            returndata: Bytes::new(),
1508            gas_remaining: 0,
1509            gas_refund_counter: 0,
1510            gas_used: 0,
1511            gas_cost: 0,
1512            state_gas_cost: None,
1513            state_gas_reservoir: None,
1514            state_gas_spent: 0,
1515            storage_change: None,
1516            status: Some(InstructionResult::Stop),
1517            immediate_bytes: None,
1518            decoded: None,
1519        }
1520    }
1521
1522    fn internal_call_step(end_step: usize, return_data: Vec<String>) -> CallTraceStep {
1523        internal_call_step_named("DebugMe::foo", end_step, Some(Vec::new()), Some(return_data))
1524    }
1525
1526    fn internal_call_step_named(
1527        func_name: &str,
1528        end_step: usize,
1529        args: Option<Vec<String>>,
1530        return_data: Option<Vec<String>>,
1531    ) -> CallTraceStep {
1532        let mut step = trace_step(Vec::new());
1533        step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1534            DecodedInternalCall { func_name: func_name.to_string(), args, return_data },
1535            end_step,
1536        )));
1537        step
1538    }
1539
1540    fn internal_call_step_without_args(end_step: usize) -> CallTraceStep {
1541        let mut step = trace_step(Vec::new());
1542        step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1543            DecodedInternalCall {
1544                func_name: "DebugMe::foo".to_string(),
1545                args: None,
1546                return_data: None,
1547            },
1548            end_step,
1549        )));
1550        step
1551    }
1552
1553    fn debug_node(
1554        trace_node_idx: usize,
1555        step_offset: usize,
1556        steps: Vec<CallTraceStep>,
1557    ) -> DebugNode {
1558        let mut node = DebugNode::new(Address::ZERO, CallKind::Call, steps, Bytes::new(), 0, None);
1559        node.trace_node_idx = trace_node_idx;
1560        node.step_offset = step_offset;
1561        node
1562    }
1563
1564    fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1565        DebuggerContext {
1566            debug_arena: arena,
1567            stats: None,
1568            identified_contracts: Default::default(),
1569            slot_identifiers: None,
1570            contracts_sources: ContractSources::default(),
1571            breakpoints: Breakpoints::default(),
1572            layout: Default::default(),
1573        }
1574    }
1575
1576    fn abi_word(value: U256) -> [u8; 32] {
1577        value.to_be_bytes::<32>()
1578    }
1579
1580    #[test]
1581    fn draw_buffer_handles_missing_memory_snapshot() {
1582        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1583        let tui = TUIContext::new(&mut context);
1584        let backend = TestBackend::new(80, 4);
1585        let mut terminal = Terminal::new(backend).unwrap();
1586
1587        terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 4))).unwrap();
1588
1589        let screen = terminal
1590            .backend()
1591            .buffer()
1592            .content()
1593            .iter()
1594            .map(|cell| cell.symbol())
1595            .collect::<String>();
1596        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1597    }
1598
1599    #[test]
1600    fn draw_buffer_does_not_overflow_on_saturated_stack_offsets() {
1601        // A stack of all-1s values (as produced by e.g. `not(0)` in inline assembly) saturates to
1602        // `usize::MAX` when read via `get_buffer_accesses`. Regression test for the unfixed half
1603        // of #6472: `offset + len` and `write_offset + write_size` used raw addition on values
1604        // that can legitimately be `usize::MAX`, which panics with "attempt to add with overflow"
1605        // in a debug build once a non-empty buffer reaches the affected closure.
1606        let stack = vec![U256::MAX, U256::MAX];
1607        let mut step = trace_step(stack);
1608        step.op = OpCode::LOG1;
1609
1610        let mut node = debug_node(0, 0, vec![step]);
1611        node.calldata = Bytes::from(vec![0u8; 64]);
1612        let mut context = context_with_arena(vec![node]);
1613        let mut tui = TUIContext::new(&mut context);
1614        // Route the non-empty buffer through Calldata (a publicly constructible `Bytes`) rather
1615        // than Memory (`RecordedMemory`'s content constructor is crate-private to
1616        // revm-inspectors) - the offset/len arithmetic under test doesn't depend on which pane is
1617        // active, only on the chunk loop actually running over a non-empty buffer.
1618        tui.active_buffer = BufferKind::Calldata;
1619
1620        let backend = TestBackend::new(80, 6);
1621        let mut terminal = Terminal::new(backend).unwrap();
1622
1623        // Must not panic with "attempt to add with overflow".
1624        terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 6))).unwrap();
1625    }
1626
1627    #[test]
1628    fn opcode_list_draws_visible_window() {
1629        let steps = (0..64)
1630            .chain([0x100])
1631            .map(|pc| {
1632                let mut step = trace_step(Vec::new());
1633                step.pc = pc;
1634                step.op = OpCode::ADD;
1635                step
1636            })
1637            .collect();
1638        let mut context = context_with_arena(vec![debug_node(0, 0, steps)]);
1639        let mut tui = TUIContext::new(&mut context);
1640        tui.init();
1641        tui.current_step = 50;
1642        let backend = TestBackend::new(80, 20);
1643        let mut terminal = Terminal::new(backend).unwrap();
1644
1645        terminal.draw(|f| tui.draw_op_list(f, Rect::new(0, 0, 80, 20))).unwrap();
1646
1647        let lines = terminal
1648            .backend()
1649            .buffer()
1650            .content()
1651            .chunks(80)
1652            .map(|cells| cells.iter().map(|cell| cell.symbol()).collect::<String>())
1653            .collect::<Vec<_>>();
1654        assert!(lines[1].contains("022|ADD"));
1655        assert!(lines[17].contains("▶032|ADD"));
1656        assert!(lines[18].contains("033|ADD"));
1657
1658        let backend = TestBackend::new(80, 3);
1659        let mut terminal = Terminal::new(backend).unwrap();
1660        terminal.draw(|f| tui.draw_op_list(f, Rect::new(0, 0, 80, 3))).unwrap();
1661        let screen = terminal
1662            .backend()
1663            .buffer()
1664            .content()
1665            .iter()
1666            .map(|cell| cell.symbol())
1667            .collect::<String>();
1668        assert!(screen.contains("▶032|ADD"));
1669    }
1670
1671    #[test]
1672    fn storage_explorer_draws_accessed_slots() {
1673        let mut first = trace_step(Vec::new());
1674        first.storage_change = Some(Box::new(StorageChange {
1675            key: U256::ZERO,
1676            value: U256::from(42),
1677            had_value: None,
1678            reason: StorageChangeReason::SSTORE,
1679        }));
1680        let mut second = trace_step(Vec::new());
1681        second.storage_change = Some(Box::new(StorageChange {
1682            key: U256::from(1),
1683            value: U256::from(0xbeef),
1684            had_value: None,
1685            reason: StorageChangeReason::SSTORE,
1686        }));
1687        let mut latest = trace_step(Vec::new());
1688        latest.storage_change = Some(Box::new(StorageChange {
1689            key: U256::ZERO,
1690            value: U256::from(43),
1691            had_value: Some(U256::from(42)),
1692            reason: StorageChangeReason::SSTORE,
1693        }));
1694        let mut context = context_with_arena(vec![debug_node(0, 0, vec![first, second, latest])]);
1695        let mut tui = TUIContext::new(&mut context);
1696        tui.current_step = 2;
1697        let backend = TestBackend::new(100, 6);
1698        let mut terminal = Terminal::new(backend).unwrap();
1699
1700        terminal
1701            .draw(|f| tui.draw_storage(f, Rect::new(0, 0, 100, 6), super::StorageSpace::Persistent))
1702            .unwrap();
1703
1704        let screen = terminal
1705            .backend()
1706            .buffer()
1707            .content()
1708            .iter()
1709            .map(|cell| cell.symbol())
1710            .collect::<String>();
1711        assert!(screen.contains("Storage: 2 accessed slots"));
1712        assert!(screen.contains("SSTORE slot 0x0"));
1713        assert!(screen.contains("value 0x2b"));
1714        assert!(screen.contains("SSTORE slot 0x1"));
1715        assert!(screen.contains("value 0xbeef"));
1716    }
1717
1718    #[test]
1719    fn hidden_source_pane_omits_source_panel() {
1720        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1721        context.layout = DebuggerLayout::Horizontal;
1722        let mut tui = TUIContext::new(&mut context);
1723        tui.init();
1724        tui.show_source = false;
1725        let backend = TestBackend::new(220, 30);
1726        let mut terminal = Terminal::new(backend).unwrap();
1727
1728        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1729
1730        let screen = terminal
1731            .backend()
1732            .buffer()
1733            .content()
1734            .iter()
1735            .map(|cell| cell.symbol())
1736            .collect::<String>();
1737        assert!(!screen.contains("Contract call"));
1738        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1739    }
1740
1741    #[test]
1742    fn hidden_opcodes_pane_omits_opcode_panel() {
1743        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1744        context.layout = DebuggerLayout::Horizontal;
1745        let mut tui = TUIContext::new(&mut context);
1746        tui.init();
1747        tui.show_opcodes = false;
1748        let backend = TestBackend::new(220, 30);
1749        let mut terminal = Terminal::new(backend).unwrap();
1750
1751        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1752
1753        let screen = terminal
1754            .backend()
1755            .buffer()
1756            .content()
1757            .iter()
1758            .map(|cell| cell.symbol())
1759            .collect::<String>();
1760        assert!(!screen.contains("address:"));
1761        assert!(screen.contains("Contract call"));
1762        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1763    }
1764
1765    #[test]
1766    fn hidden_variables_pane_omits_variables_panel() {
1767        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1768        context.layout = DebuggerLayout::Horizontal;
1769        let mut tui = TUIContext::new(&mut context);
1770        tui.init();
1771        tui.show_variables = false;
1772        let backend = TestBackend::new(220, 30);
1773        let mut terminal = Terminal::new(backend).unwrap();
1774
1775        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1776
1777        let screen = terminal
1778            .backend()
1779            .buffer()
1780            .content()
1781            .iter()
1782            .map(|cell| cell.symbol())
1783            .collect::<String>();
1784        assert!(!screen.contains("Variables"));
1785        assert!(screen.contains("Stack"));
1786        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1787    }
1788
1789    #[test]
1790    fn hidden_stack_pane_omits_stack_panel() {
1791        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1792        context.layout = DebuggerLayout::Horizontal;
1793        let mut tui = TUIContext::new(&mut context);
1794        tui.init();
1795        tui.show_stack = false;
1796        let backend = TestBackend::new(220, 30);
1797        let mut terminal = Terminal::new(backend).unwrap();
1798
1799        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1800
1801        let screen = terminal
1802            .backend()
1803            .buffer()
1804            .content()
1805            .iter()
1806            .map(|cell| cell.symbol())
1807            .collect::<String>();
1808        assert!(!screen.contains("Stack:"));
1809        assert!(screen.contains("Variables"));
1810        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1811    }
1812
1813    #[test]
1814    fn hidden_data_pane_omits_data_panel() {
1815        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1816        context.layout = DebuggerLayout::Horizontal;
1817        let mut tui = TUIContext::new(&mut context);
1818        tui.init();
1819        tui.show_opcodes = false;
1820        tui.show_variables = false;
1821        tui.show_stack = false;
1822        tui.show_data = false;
1823        let backend = TestBackend::new(80, 30);
1824        let mut terminal = Terminal::new(backend).unwrap();
1825
1826        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1827
1828        let screen = terminal
1829            .backend()
1830            .buffer()
1831            .content()
1832            .iter()
1833            .map(|cell| cell.symbol())
1834            .collect::<String>();
1835        assert!(!screen.contains("Memory (max expansion: 0 bytes)"));
1836        assert!(screen.contains("Contract call"));
1837    }
1838
1839    #[test]
1840    fn command_prompt_draws_in_bordered_block() {
1841        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1842        let mut tui = TUIContext::new(&mut context);
1843        tui.prompt = Some(Prompt::Command("pc 0".to_string()));
1844        let backend = TestBackend::new(120, 6);
1845        let mut terminal = Terminal::new(backend).unwrap();
1846
1847        terminal.draw(|f| tui.draw_footer(f, Rect::new(0, 0, 120, 6))).unwrap();
1848
1849        let screen = terminal
1850            .backend()
1851            .buffer()
1852            .content()
1853            .iter()
1854            .map(|cell| cell.symbol())
1855            .collect::<String>();
1856        assert!(screen.contains("Command"));
1857        assert!(screen.contains(":pc 0"));
1858        assert!(screen.contains("Enter: run"));
1859        assert!(screen.contains("[:] command"));
1860    }
1861
1862    #[test]
1863    fn command_prompt_line_clips_long_input_to_tail() {
1864        let input = "continue 0123456789abcdef";
1865        let line = super::command_prompt_line(input, 12);
1866        let text = line_text(&line);
1867
1868        assert!(line.width() <= 12);
1869        assert_eq!(text, ":<789abcdef█");
1870        assert!(!text.contains("Enter: run"));
1871    }
1872
1873    #[test]
1874    fn command_prompt_line_clips_wide_unicode_to_width() {
1875        // Full-width characters render two cells each, so clipping must respect display
1876        // width rather than character count.
1877        let input = "界界界界界界界界";
1878        let line = super::command_prompt_line(input, 8);
1879
1880        assert!(line.width() <= 8);
1881    }
1882
1883    #[test]
1884    fn decode_external_parameter_values_decodes_named_params() {
1885        let scope = scope("foo", "(uint256 amount, bool ok)");
1886        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1887        let types = super::resolved_types(&parameters).unwrap();
1888        let mut calldata = Vec::new();
1889        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1890        calldata.extend_from_slice(&abi_word(U256::from(42)));
1891        calldata.extend_from_slice(&abi_word(U256::from(1)));
1892
1893        let values = super::decode_external_parameter_values(&scope, &calldata).unwrap();
1894
1895        assert_eq!(values, ["42", "true"]);
1896    }
1897
1898    #[test]
1899    fn decode_external_parameter_values_rejects_selector_mismatch() {
1900        let scope = scope("foo", "(uint256 amount)");
1901        let parameters = Parameters::parse("(uint256 amount)").unwrap();
1902        let types = super::resolved_types(&parameters).unwrap();
1903        let mut calldata = Vec::new();
1904        calldata.extend_from_slice(&super::function_selector("bar", &types));
1905        calldata.extend_from_slice(&abi_word(U256::from(42)));
1906
1907        assert_eq!(super::decode_external_parameter_values(&scope, &calldata), None);
1908    }
1909
1910    #[test]
1911    fn decode_external_return_values_decodes_named_returns() {
1912        let scope = scope_with_returns("foo", "(uint256 amount)", "(uint256 total, bool ok)");
1913        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1914        let types = super::resolved_types(&parameters).unwrap();
1915        let mut calldata = Vec::new();
1916        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1917        calldata.extend_from_slice(&abi_word(U256::from(42)));
1918        let mut returndata = Vec::new();
1919        returndata.extend_from_slice(&abi_word(U256::from(99)));
1920        returndata.extend_from_slice(&abi_word(U256::from(1)));
1921
1922        let values = super::decode_external_return_values(&scope, &calldata, &returndata).unwrap();
1923
1924        assert_eq!(values, ["99", "true"]);
1925    }
1926
1927    #[test]
1928    fn decode_return_values_uses_external_output_at_frame_end() {
1929        let scope = scope_with_returns("foo", "()", "(uint256 total)");
1930        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1931        let types = super::resolved_types(&parameters).unwrap();
1932        let mut calldata = Vec::new();
1933        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1934        let mut node = debug_node(
1935            0,
1936            0,
1937            vec![trace_step(Vec::new()), {
1938                let mut step = trace_step(Vec::new());
1939                step.op = OpCode::RETURN;
1940                step.status = Some(InstructionResult::Return);
1941                step
1942            }],
1943        );
1944        node.calldata = Bytes::from(calldata);
1945        node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1946        let mut context = context_with_arena(vec![node]);
1947        let mut tui = TUIContext::new(&mut context);
1948        tui.current_step = 1;
1949
1950        assert_eq!(tui.decode_return_values(&scope), Some(vec!["123".to_string()]));
1951    }
1952
1953    #[test]
1954    fn decode_return_values_waits_for_external_frame_end() {
1955        let scope = scope_with_returns("foo", "()", "(uint256 total)");
1956        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1957        let types = super::resolved_types(&parameters).unwrap();
1958        let mut calldata = Vec::new();
1959        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1960        let mut node = debug_node(0, 0, vec![trace_step(Vec::new()), trace_step(Vec::new())]);
1961        node.calldata = Bytes::from(calldata);
1962        node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1963        let mut context = context_with_arena(vec![node]);
1964        let mut tui = TUIContext::new(&mut context);
1965
1966        assert_eq!(tui.decode_return_values(&scope), None);
1967    }
1968
1969    #[test]
1970    fn scope_function_signature_includes_resolved_parameter_types() {
1971        let scope = scope("foo", "(uint256 amount, bool ok)");
1972
1973        assert_eq!(super::scope_function_signature(&scope).as_deref(), Some("foo(uint256,bool)"));
1974    }
1975
1976    #[test]
1977    fn decode_parameter_values_rejects_decoded_call_data_for_wrong_overload() {
1978        let mut node = debug_node(0, 0, vec![trace_step(Vec::new())]);
1979        node.decoded = Some(Box::new(DecodedCallTrace {
1980            call_data: Some(DecodedCallData {
1981                signature: "foo(address)".to_string(),
1982                args: vec!["0x000000000000000000000000000000000000002a".to_string()],
1983            }),
1984            ..Default::default()
1985        }));
1986        let mut context = context_with_arena(vec![node]);
1987        let mut tui = TUIContext::new(&mut context);
1988
1989        assert_eq!(tui.decode_parameter_values(&scope("foo", "(uint256 amount)")), None);
1990    }
1991
1992    #[test]
1993    fn decode_step_parameters_reads_static_values_from_stack() {
1994        let step = trace_step(vec![U256::from(42), U256::from(1)]);
1995        let parameters = Parameters::parse("(uint256 amount, bool ok)").unwrap();
1996        let values = super::decode_step_parameters(&parameters, &step, None).unwrap();
1997
1998        assert_eq!(values, ["42", "true"]);
1999    }
2000
2001    #[test]
2002    fn decode_internal_parameter_values_uses_absolute_entry_step() {
2003        let mut context = context_with_arena(vec![
2004            debug_node(0, 0, vec![internal_call_step_without_args(2)]),
2005            debug_node(1, 0, vec![trace_step(Vec::new())]),
2006            debug_node(0, 1, vec![trace_step(vec![U256::from(42)])]),
2007        ]);
2008        let mut tui = TUIContext::new(&mut context);
2009        tui.draw_memory.inner_call_index = 2;
2010
2011        assert_eq!(
2012            tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
2013            Some(vec!["42".to_string()])
2014        );
2015    }
2016
2017    #[test]
2018    fn decode_internal_parameter_values_passes_calldata_to_fallback_decoder() {
2019        let digest = U256::from(0x1234);
2020        let offset = 0x44;
2021        let mut calldata = vec![0; offset];
2022        calldata.extend_from_slice(&[0x11, 0x22, 0x33]);
2023
2024        let mut entry_node =
2025            debug_node(0, 1, vec![trace_step(vec![digest, U256::from(offset), U256::from(3)])]);
2026        entry_node.calldata = Bytes::from(calldata);
2027
2028        let mut context = context_with_arena(vec![
2029            debug_node(0, 0, vec![internal_call_step_without_args(2)]),
2030            debug_node(1, 0, vec![trace_step(Vec::new())]),
2031            entry_node,
2032        ]);
2033        let mut tui = TUIContext::new(&mut context);
2034        tui.draw_memory.inner_call_index = 2;
2035
2036        assert_eq!(
2037            tui.decode_internal_parameter_values(&scope(
2038                "foo",
2039                "(bytes32 digest, bytes calldata signature)"
2040            )),
2041            Some(vec![
2042                "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
2043                "0x112233".to_string(),
2044            ])
2045        );
2046    }
2047
2048    #[test]
2049    fn decode_internal_parameter_values_accepts_matching_overload_args() {
2050        let mut context = context_with_arena(vec![debug_node(
2051            0,
2052            0,
2053            vec![internal_call_step_named(
2054                "DebugMe::foo(uint256)",
2055                2,
2056                Some(vec!["42".to_string()]),
2057                None,
2058            )],
2059        )]);
2060        let mut tui = TUIContext::new(&mut context);
2061
2062        assert_eq!(
2063            tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
2064            Some(vec!["42".to_string()])
2065        );
2066    }
2067
2068    #[test]
2069    fn decode_internal_parameter_values_rejects_wrong_overload_args() {
2070        let mut context = context_with_arena(vec![debug_node(
2071            0,
2072            0,
2073            vec![internal_call_step_named(
2074                "DebugMe::foo(address)",
2075                2,
2076                Some(vec!["0x000000000000000000000000000000000000002a".to_string()]),
2077                None,
2078            )],
2079        )]);
2080        let mut tui = TUIContext::new(&mut context);
2081
2082        assert_eq!(tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), None);
2083    }
2084
2085    #[test]
2086    fn decode_return_values_uses_absolute_internal_call_end_step() {
2087        let mut context = context_with_arena(vec![debug_node(
2088            0,
2089            3,
2090            vec![internal_call_step(4, vec!["99".to_string()]), trace_step(Vec::new())],
2091        )]);
2092        let mut tui = TUIContext::new(&mut context);
2093        tui.current_step = 1;
2094
2095        assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["99".to_string()]));
2096    }
2097
2098    #[test]
2099    fn decode_return_values_finds_internal_call_split_by_child_node() {
2100        let mut context = context_with_arena(vec![
2101            debug_node(0, 0, vec![internal_call_step(2, vec!["7".to_string()])]),
2102            debug_node(1, 0, vec![trace_step(Vec::new())]),
2103            debug_node(0, 2, vec![trace_step(Vec::new())]),
2104        ]);
2105        let mut tui = TUIContext::new(&mut context);
2106        tui.draw_memory.inner_call_index = 2;
2107
2108        assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["7".to_string()]));
2109    }
2110
2111    #[test]
2112    fn decode_return_values_rejects_wrong_overload() {
2113        let mut context = context_with_arena(vec![debug_node(
2114            0,
2115            0,
2116            vec![
2117                internal_call_step_named(
2118                    "DebugMe::foo(address)",
2119                    1,
2120                    None,
2121                    Some(vec!["99".to_string()]),
2122                ),
2123                trace_step(Vec::new()),
2124            ],
2125        )]);
2126        let mut tui = TUIContext::new(&mut context);
2127        tui.current_step = 1;
2128
2129        assert_eq!(tui.decode_return_values(&scope("foo", "(uint256 amount)")), None);
2130    }
2131
2132    #[test]
2133    fn active_internal_call_caches_by_current_node_and_step() {
2134        let mut context = context_with_arena(vec![debug_node(
2135            0,
2136            0,
2137            vec![internal_call_step(2, vec!["1".to_string()]), trace_step(Vec::new())],
2138        )]);
2139        let mut tui = TUIContext::new(&mut context);
2140
2141        assert!(tui.active_internal_call().is_some());
2142        let cache = tui.draw_memory.active_internal_call;
2143        assert!(cache.and_then(|cache| cache.location).is_some());
2144
2145        assert!(tui.active_internal_call().is_some());
2146        assert_eq!(tui.draw_memory.active_internal_call, cache);
2147
2148        tui.current_step = 1;
2149        assert!(tui.active_internal_call().is_some());
2150        assert_ne!(tui.draw_memory.active_internal_call, cache);
2151    }
2152
2153    #[test]
2154    fn decoded_internal_name_matches_exact_contract_and_function() {
2155        let scope = scope("foo", "()");
2156
2157        assert!(super::decoded_internal_name_matches("DebugMe::foo", &scope));
2158        assert!(!super::decoded_internal_name_matches("DebugMe::barfoo", &scope));
2159        assert!(!super::decoded_internal_name_matches("Other::foo", &scope));
2160    }
2161
2162    #[test]
2163    fn decoded_internal_name_matches_canonical_signature_for_overloads() {
2164        let scope = scope("foo", "(uint256 amount)");
2165
2166        assert!(super::decoded_internal_name_matches("DebugMe::foo(uint256)", &scope));
2167        assert!(!super::decoded_internal_name_matches("DebugMe::foo(address)", &scope));
2168        assert!(!super::decoded_internal_name_matches("Other::foo(uint256)", &scope));
2169    }
2170
2171    #[test]
2172    fn scope_variable_line_marks_unavailable_locals() {
2173        let variable = super::ScopeVariable {
2174            kind: super::ScopeVariableKind::Local,
2175            name: "sum".to_string(),
2176            value: None,
2177        };
2178
2179        assert_eq!(line_text(&super::scope_variable_line(variable)), "local sum = <unavailable>");
2180    }
2181
2182    #[test]
2183    fn storage_lines_format_sload_and_label() {
2184        let mut step = trace_step(Vec::new());
2185        step.storage_change = Some(Box::new(StorageChange {
2186            key: U256::from(1),
2187            value: U256::from(42),
2188            had_value: None,
2189            reason: StorageChangeReason::SLOAD,
2190        }));
2191        let steps = [step];
2192        let access = super::storage_access_at(&steps, 0).unwrap();
2193
2194        assert_eq!(
2195            line_text(&super::storage_access_line(access, Some("count"))),
2196            "storage SLOAD slot 0x1 = 0x2a (count)"
2197        );
2198        assert_eq!(
2199            line_text(&super::storage_slot_lines(0, 2, access, Some("count"), false)[0]),
2200            "00| SLOAD slot 0x1 (count)"
2201        );
2202    }
2203
2204    #[test]
2205    fn storage_access_line_formats_sstore_with_previous_value() {
2206        let mut step = trace_step(Vec::new());
2207        step.storage_change = Some(Box::new(StorageChange {
2208            key: U256::from(1),
2209            value: U256::from(42),
2210            had_value: Some(U256::from(7)),
2211            reason: StorageChangeReason::SSTORE,
2212        }));
2213        let steps = [step];
2214        let access = super::storage_access_at(&steps, 0).unwrap();
2215
2216        assert_eq!(
2217            line_text(&super::storage_access_line(access, None)),
2218            "storage SSTORE slot 0x1: 0x7 -> 0x2a"
2219        );
2220    }
2221
2222    #[test]
2223    fn current_storage_access_line_uses_next_stack_snapshot_for_warm_sload() {
2224        let mut step = trace_step(vec![U256::from(1)]);
2225        step.op = OpCode::SLOAD;
2226        step.storage_change = None;
2227        let next_step = trace_step(vec![U256::from(42)]);
2228        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2229        let tui = TUIContext::new(&mut context);
2230
2231        assert_eq!(
2232            line_text(&tui.current_storage_access_line().unwrap()),
2233            "storage SLOAD slot 0x1 = 0x2a"
2234        );
2235    }
2236
2237    #[test]
2238    fn current_storage_access_line_uses_next_stack_snapshot_for_tload() {
2239        let mut step = trace_step(vec![U256::from(1)]);
2240        step.op = OpCode::TLOAD;
2241        let next_step = trace_step(vec![U256::from(42)]);
2242        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2243        let tui = TUIContext::new(&mut context);
2244
2245        assert_eq!(
2246            line_text(&tui.current_storage_access_line().unwrap()),
2247            "transient storage TLOAD slot 0x1 = 0x2a"
2248        );
2249    }
2250
2251    #[test]
2252    fn variable_name_falls_back_for_unnamed_values() {
2253        let variable = DebugVariable { name: None, declaration: 0..1, scope: 0..2 };
2254
2255        assert_eq!(super::variable_name(&variable, 2, "arg"), "arg2");
2256    }
2257
2258    #[test]
2259    fn current_step_notice_text_reads_decoded_line_steps() {
2260        let mut step = trace_step(Vec::new());
2261        step.decoded = Some(Box::new(DecodedTraceStep::Line(
2262            "precompile: PRECOMPILES::sha256(0x68656c6c6f)".to_string(),
2263        )));
2264        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step])]);
2265        let tui = TUIContext::new(&mut context);
2266
2267        assert_eq!(
2268            tui.current_step_notice_text(),
2269            Some("precompile: PRECOMPILES::sha256(0x68656c6c6f)")
2270        );
2271    }
2272
2273    #[test]
2274    fn source_pane_title_includes_precompile_notice_before_source() {
2275        assert_eq!(
2276            super::source_pane_title(
2277                "Contract call",
2278                Some("test/Precompile.t.sol"),
2279                Some("precompile call")
2280            ),
2281            "Contract call | precompile call | test/Precompile.t.sol "
2282        );
2283    }
2284
2285    #[test]
2286    fn variables_title_tracks_decoded_step_notices() {
2287        assert_eq!(super::variables_title(0, 0, false, false), "Variables");
2288        assert_eq!(super::variables_title(0, 0, false, true), "Variables: 0/0 | Trace");
2289        assert_eq!(super::variables_title(2, 1, true, true), "Variables: 1/2 | Trace | Storage");
2290    }
2291
2292    #[test]
2293    fn step_notice_line_highlights_precompile_clue() {
2294        let line = super::step_notice_line("precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2295
2296        assert_eq!(line_text(&line), "precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2297        assert_eq!(line.spans[0].style, Style::new().fg(Color::Magenta));
2298        assert_eq!(super::step_notice_title(&line_text(&line)), "precompile call");
2299    }
2300
2301    #[test]
2302    fn op_list_title_includes_gas_and_subcall_stats() {
2303        let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
2304        let address = Address::from([0x42; 20]);
2305        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
2306
2307        assert!(title.contains("pc: 0x2a (42)"));
2308        assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
2309        assert!(title.contains("gasLeft: 123456"));
2310        assert!(title.contains("sessionTraceGasUsed: 789012"));
2311        assert!(title.contains("sessionSubcalls: 3"));
2312        assert!(title.contains("callGasUsed: 42"));
2313        assert!(title.contains("gasRefund: 7"));
2314    }
2315
2316    #[test]
2317    fn op_list_title_omits_aggregate_stats_when_unavailable() {
2318        let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
2319
2320        assert!(!title.contains("sessionTraceGasUsed"));
2321        assert!(!title.contains("sessionSubcalls"));
2322    }
2323
2324    #[test]
2325    fn op_list_title_uses_full_checksum_address() {
2326        let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
2327        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
2328
2329        assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2330        assert!(!title.contains('…'));
2331    }
2332
2333    #[test]
2334    fn stack_item_line_includes_decimal_preview() {
2335        let line = super::stack_item_line(0, 2, &U256::from(42), None, false);
2336        let text = line_text(&line);
2337
2338        assert!(text.starts_with("00| "));
2339        assert!(text.ends_with("2a | 42\n"));
2340    }
2341
2342    #[test]
2343    fn stack_item_line_keeps_stack_labels_after_decimal_preview() {
2344        let param = OpcodeParam { name: "offset", index: 0 };
2345        let line = super::stack_item_line(0, 2, &U256::from(16), Some(&param), true);
2346
2347        assert!(line_text(&line).ends_with("10 | 16 | offset\n"));
2348    }
2349
2350    #[test]
2351    fn stack_item_line_highlights_decimal_preview_for_opcode_params() {
2352        let param = OpcodeParam { name: "offset", index: 0 };
2353        let line = super::stack_item_line(0, 2, &U256::from(16), Some(&param), false);
2354        let decimal = line.spans.iter().find(|span| span.content.as_ref() == "16").unwrap();
2355
2356        assert_eq!(decimal.style, Style::new().fg(Color::Cyan));
2357    }
2358
2359    #[test]
2360    fn decimal_digits() {
2361        assert_eq!(super::decimal_digits(0), 1);
2362        assert_eq!(super::decimal_digits(1), 1);
2363        assert_eq!(super::decimal_digits(2), 1);
2364        assert_eq!(super::decimal_digits(9), 1);
2365        assert_eq!(super::decimal_digits(10), 2);
2366        assert_eq!(super::decimal_digits(11), 2);
2367        assert_eq!(super::decimal_digits(50), 2);
2368        assert_eq!(super::decimal_digits(99), 2);
2369        assert_eq!(super::decimal_digits(100), 3);
2370        assert_eq!(super::decimal_digits(101), 3);
2371        assert_eq!(super::decimal_digits(201), 3);
2372        assert_eq!(super::decimal_digits(999), 3);
2373        assert_eq!(super::decimal_digits(1000), 4);
2374        assert_eq!(super::decimal_digits(1001), 4);
2375    }
2376
2377    #[test]
2378    fn hex_digits() {
2379        assert_eq!(super::hex_digits(0), 1);
2380        assert_eq!(super::hex_digits(1), 1);
2381        assert_eq!(super::hex_digits(2), 1);
2382        assert_eq!(super::hex_digits(9), 1);
2383        assert_eq!(super::hex_digits(10), 1);
2384        assert_eq!(super::hex_digits(11), 1);
2385        assert_eq!(super::hex_digits(15), 1);
2386        assert_eq!(super::hex_digits(16), 2);
2387        assert_eq!(super::hex_digits(17), 2);
2388        assert_eq!(super::hex_digits(0xff), 2);
2389        assert_eq!(super::hex_digits(0x100), 3);
2390        assert_eq!(super::hex_digits(0x101), 3);
2391    }
2392}