Skip to main content

foundry_debugger/tui/
draw.rs

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