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_text().map(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_storage_access_line(&self) -> Option<Line<'static>> {
621        storage_access_at(self.debug_steps(), self.current_step).map(storage_access_line)
622    }
623
624    fn draw_data(&mut self, f: &mut Frame<'_>, area: Rect) {
625        if let Some(space) = self.active_storage() {
626            self.draw_storage(f, area, space);
627        } else {
628            self.draw_buffer(f, area);
629        }
630    }
631
632    fn draw_storage(&mut self, f: &mut Frame<'_>, area: Rect, space: StorageSpace) {
633        let accesses = self.storage_accesses(space);
634        let current_slot = storage_access_at(self.debug_steps(), self.current_step)
635            .filter(|access| access.space() == space)
636            .map(StorageAccess::slot);
637        self.draw_memory.current_storage_startline =
638            self.draw_memory.current_storage_startline.min(accesses.len().saturating_sub(1));
639
640        let index_width = decimal_digits(accesses.len()).max(2);
641        let mut lines = accesses
642            .values()
643            .copied()
644            .enumerate()
645            .skip(self.draw_memory.current_storage_startline)
646            .flat_map(|(index, access)| {
647                storage_slot_lines(index, index_width, access, current_slot == Some(access.slot()))
648            })
649            .collect::<Vec<_>>();
650        if lines.is_empty() {
651            lines.push(Line::from(Span::styled(
652                format!("No {} accessed in current call", space.noun()),
653                Style::new().add_modifier(Modifier::DIM),
654            )));
655        }
656
657        let count = accesses.len();
658        let suffix = if count == 1 { "slot" } else { "slots" };
659        let title = format!("{}: {count} accessed {suffix}", space.label());
660        let block = Block::default().title(title).borders(Borders::ALL);
661        let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
662        f.render_widget(paragraph, area);
663    }
664
665    fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
666        let call = self.debug_call();
667        let step = self.current_step();
668        let buf: &[u8] = match self.active_buffer {
669            BufferKind::Memory => step.memory.as_ref().map_or(&[], |memory| memory.as_ref()),
670            BufferKind::Calldata => call.calldata.as_ref(),
671            BufferKind::Returndata => step.returndata.as_ref(),
672        };
673
674        let min_len = hex_digits(buf.len());
675
676        // Color memory region based on read/write.
677        let mut offset = None;
678        let mut len = None;
679        let mut write_offset = None;
680        let mut write_size = None;
681        let mut color = None;
682        let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
683        if stack_len > 0
684            && let Some(stack) = step.stack.as_ref()
685            && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
686        {
687            if let Some(read_access) = accesses.read {
688                offset = Some(read_access.1.offset);
689                len = Some(read_access.1.len);
690                color = Some(Color::Cyan);
691            }
692            if let Some(write_access) = accesses.write
693                && self.active_buffer == BufferKind::Memory
694            {
695                write_offset = Some(write_access.offset);
696                write_size = Some(write_access.len);
697            }
698        }
699
700        // color word on previous write op
701        // TODO: technically it's possible for this to conflict with the current op, ie, with
702        // subsequent MCOPYs, but solc can't seem to generate that code even with high optimizer
703        // settings
704        if self.current_step > 0 {
705            let prev_step = self.current_step - 1;
706            let prev_step = &self.debug_steps()[prev_step];
707            if let Some(stack) = prev_step.stack.as_ref()
708                && let Some(write_access) =
709                    get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
710                && self.active_buffer == BufferKind::Memory
711            {
712                offset = Some(write_access.offset);
713                len = Some(write_access.len);
714                color = Some(Color::Green);
715            }
716        }
717
718        let height = area.height as usize;
719        let end_line = self.draw_memory.current_buf_startline + height;
720
721        let text: Vec<Line<'_>> = buf
722            .chunks(32)
723            .enumerate()
724            .skip(self.draw_memory.current_buf_startline)
725            .take_while(|(i, _)| *i < end_line)
726            .map(|(i, buf_word)| {
727                let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
728
729                // Buffer index.
730                spans.push(Span::styled(
731                    format!("{:0min_len$x}| ", i * 32),
732                    Style::new().fg(Color::White),
733                ));
734
735                // Word hex bytes.
736                hex_bytes_spans(buf_word, &mut spans, |j, _| {
737                    let mut byte_color = Color::White;
738                    let mut end = None;
739                    let idx = i * 32 + j;
740                    if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
741                        end = Some(offset + len);
742                        if (offset..offset + len).contains(&idx) {
743                            // [offset, offset + len] is the memory region to be colored.
744                            // If a byte at row i and column j in the memory panel
745                            // falls in this region, set the color.
746                            byte_color = color;
747                        }
748                    }
749                    if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
750                        // check for overlap with read region
751                        let write_end = write_offset + write_size;
752                        if let Some(read_end) = end {
753                            let read_start = offset.unwrap();
754                            if (write_offset..write_end).contains(&read_end) {
755                                // if it contains end, start from write_start up to read_end
756                                if (write_offset..read_end).contains(&idx) {
757                                    return Style::new().fg(Color::Yellow);
758                                }
759                            } else if (write_offset..write_end).contains(&read_start) {
760                                // otherwise if it contains read start, start from read_start up to
761                                // write_end
762                                if (read_start..write_end).contains(&idx) {
763                                    return Style::new().fg(Color::Yellow);
764                                }
765                            }
766                        }
767                        if (write_offset..write_end).contains(&idx) {
768                            byte_color = Color::Red;
769                        }
770                    }
771
772                    Style::new().fg(byte_color)
773                });
774
775                if self.buf_utf {
776                    spans.push(Span::raw("|"));
777                    for utf in buf_word.chunks(4) {
778                        if let Ok(utf_str) = std::str::from_utf8(utf) {
779                            spans.push(Span::raw(utf_str.replace('\0', ".")));
780                        } else {
781                            spans.push(Span::raw("."));
782                        }
783                    }
784                }
785
786                spans.push(Span::raw("\n"));
787
788                Line::from(spans)
789            })
790            .collect();
791
792        let title = self.active_buffer.title(buf.len());
793        let block = Block::default().title(title).borders(Borders::ALL);
794        let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
795        f.render_widget(paragraph, area);
796    }
797}
798
799#[derive(Clone, Debug, PartialEq, Eq)]
800struct ScopeVariable {
801    kind: ScopeVariableKind,
802    name: String,
803    value: Option<String>,
804}
805
806#[derive(Clone, Copy, Debug, PartialEq, Eq)]
807enum ScopeVariableKind {
808    Parameter,
809    Return,
810    Local,
811}
812
813struct ActiveInternalCall<'a> {
814    trace_node_idx: usize,
815    entry_step: usize,
816    end_step: usize,
817    decoded: &'a DecodedInternalCall,
818}
819
820const PRECOMPILE_NOTICE_PREFIX: &str = "precompile:";
821
822impl ScopeVariableKind {
823    const fn label(self) -> &'static str {
824        match self {
825            Self::Parameter => "param",
826            Self::Return => "return",
827            Self::Local => "local",
828        }
829    }
830
831    const fn color(self) -> Color {
832        match self {
833            Self::Parameter => Color::Cyan,
834            Self::Return => Color::Green,
835            Self::Local => Color::White,
836        }
837    }
838}
839
840impl TUIContext<'_> {
841    fn scope_variables(&mut self) -> Vec<ScopeVariable> {
842        let (scope, start) = {
843            let Ok((source_element, source)) = self.src_map() else {
844                return Vec::new();
845            };
846            let start = source_element.offset() as usize;
847            let end = start.saturating_add(source_element.length() as usize);
848            let Some(scope) = source.find_debug_scope(start, end) else {
849                return Vec::new();
850            };
851            (scope.clone(), start)
852        };
853
854        let parameter_values = self.decode_parameter_values(&scope);
855        let return_values = self.decode_return_values(&scope);
856        let mut variables = Vec::new();
857
858        variables.extend(scope.parameters.iter().enumerate().map(|(i, variable)| ScopeVariable {
859            kind: ScopeVariableKind::Parameter,
860            name: variable_name(variable, i, "arg"),
861            value: parameter_values.as_ref().and_then(|values| values.get(i).cloned()),
862        }));
863
864        variables.extend(scope.returns.iter().enumerate().map(|(i, variable)| ScopeVariable {
865            kind: ScopeVariableKind::Return,
866            name: variable_name(variable, i, "ret"),
867            value: return_values.as_ref().and_then(|values| values.get(i).cloned()),
868        }));
869
870        variables.extend(scope.visible_locals(start).enumerate().map(|(i, variable)| {
871            ScopeVariable {
872                kind: ScopeVariableKind::Local,
873                name: variable_name(variable, i, "local"),
874                value: None,
875            }
876        }));
877
878        variables
879    }
880
881    fn decode_parameter_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
882        let scope_signature = scope_function_signature(scope);
883        self.decode_internal_parameter_values(scope)
884            .or_else(|| decode_external_parameter_values(scope, &self.debug_call().calldata))
885            .or_else(|| {
886                self.debug_call().decoded.as_ref().and_then(|decoded| {
887                    let call_data = decoded.call_data.as_ref()?;
888                    scope_signature
889                        .as_deref()
890                        .is_some_and(|signature| signature == call_data.signature)
891                        .then(|| call_data.args.clone())
892                })
893            })
894    }
895
896    fn decode_return_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
897        let current_step = self.absolute_current_step();
898        if let Some(values) = self
899            .active_internal_call()
900            .and_then(|active| {
901                (current_step >= active.end_step
902                    && decoded_internal_name_matches(&active.decoded.func_name, scope))
903                .then(|| active.decoded.return_data.clone())
904            })
905            .flatten()
906        {
907            return Some(values);
908        }
909
910        if self.current_step + 1 < self.debug_steps().len()
911            || !matches!(
912                self.current_step().status,
913                Some(InstructionResult::Return | InstructionResult::Stop)
914            )
915        {
916            return None;
917        }
918
919        decode_external_return_values(
920            scope,
921            &self.debug_call().calldata,
922            &self.debug_call().returndata,
923        )
924    }
925
926    fn decode_internal_parameter_values(
927        &mut self,
928        scope: &DebugSourceScope,
929    ) -> Option<Vec<String>> {
930        let (args, trace_node_idx, entry_step) = {
931            let active = self.active_internal_call()?;
932            if !decoded_internal_name_matches(&active.decoded.func_name, scope) {
933                return None;
934            }
935
936            (active.decoded.args.clone(), active.trace_node_idx, active.entry_step)
937        };
938
939        if let Some(args) = args {
940            return Some(args);
941        }
942
943        let parameters = Parameters::parse(&scope.parameters_src).ok()?;
944        let node = self.debug_arena().iter().find(|node| {
945            node.trace_node_idx == trace_node_idx
946                && entry_step >= node.step_offset
947                && entry_step < node.step_offset.saturating_add(node.steps.len())
948        })?;
949        let step = node.steps.get(entry_step.checked_sub(node.step_offset)?)?;
950        decode_step_parameters(&parameters, step, Some(node.calldata.as_ref()))
951    }
952
953    fn active_internal_call(&mut self) -> Option<ActiveInternalCall<'_>> {
954        let current_node_idx = self.draw_memory.inner_call_index;
955        let trace_node_idx = self.debug_call().trace_node_idx;
956        let current_step = self.absolute_current_step();
957        let location = if let Some(cache) = self.draw_memory.active_internal_call
958            && cache.matches(current_node_idx, trace_node_idx, current_step)
959        {
960            cache.location
961        } else {
962            let location =
963                self.find_active_internal_call(current_node_idx, trace_node_idx, current_step);
964            self.draw_memory.active_internal_call = Some(ActiveInternalCallCache {
965                current_node_idx,
966                trace_node_idx,
967                absolute_step: current_step,
968                location,
969            });
970            location
971        }?;
972
973        self.active_internal_call_at(location)
974    }
975
976    fn find_active_internal_call(
977        &self,
978        current_node_idx: usize,
979        trace_node_idx: usize,
980        current_step: usize,
981    ) -> Option<ActiveInternalCallLocation> {
982        let mut active = None;
983
984        for (node_idx, node) in
985            self.debug_arena().iter().enumerate().take(current_node_idx.saturating_add(1))
986        {
987            if node.trace_node_idx != trace_node_idx {
988                continue;
989            }
990
991            for (step_idx, step) in node.steps.iter().enumerate() {
992                let marker_step = node.step_offset.saturating_add(step_idx);
993                if marker_step > current_step {
994                    break;
995                }
996
997                let Some(decoded) = step.decoded.as_deref() else { continue };
998                let DecodedTraceStep::InternalCall(_, end_step) = decoded else { continue };
999                if current_step <= *end_step {
1000                    active = Some(ActiveInternalCallLocation {
1001                        trace_node_idx,
1002                        marker_node_idx: node_idx,
1003                        marker_step_idx: step_idx,
1004                        entry_step: marker_step.saturating_add(1),
1005                        end_step: *end_step,
1006                    });
1007                }
1008            }
1009        }
1010
1011        active
1012    }
1013
1014    fn active_internal_call_at(
1015        &self,
1016        location: ActiveInternalCallLocation,
1017    ) -> Option<ActiveInternalCall<'_>> {
1018        let step = self
1019            .debug_arena()
1020            .get(location.marker_node_idx)?
1021            .steps
1022            .get(location.marker_step_idx)?;
1023        let DecodedTraceStep::InternalCall(decoded, end_step) = step.decoded.as_deref()? else {
1024            return None;
1025        };
1026        (*end_step == location.end_step).then_some(ActiveInternalCall {
1027            trace_node_idx: location.trace_node_idx,
1028            entry_step: location.entry_step,
1029            end_step: location.end_step,
1030            decoded,
1031        })
1032    }
1033
1034    fn absolute_current_step(&self) -> usize {
1035        self.debug_call().step_offset.saturating_add(self.current_step)
1036    }
1037}
1038
1039fn source_pane_title(
1040    call_kind_text: &str,
1041    source_name: Option<&str>,
1042    step_notice: Option<&str>,
1043) -> String {
1044    let mut title = call_kind_text.to_string();
1045    if let Some(step_notice) = step_notice {
1046        write!(title, " | {step_notice}").unwrap();
1047    }
1048    if let Some(source_name) = source_name {
1049        write!(title, " | {source_name}").unwrap();
1050    }
1051    title.push(' ');
1052    title
1053}
1054
1055fn variables_title(
1056    total_variables: usize,
1057    known_variables: usize,
1058    has_storage_access: bool,
1059    has_step_notice: bool,
1060) -> String {
1061    if total_variables == 0 && !has_storage_access && !has_step_notice {
1062        return "Variables".to_string();
1063    }
1064
1065    let mut title = format!("Variables: {known_variables}/{total_variables}");
1066    if has_step_notice {
1067        title.push_str(" | Trace");
1068    }
1069    if has_storage_access {
1070        title.push_str(" | Storage");
1071    }
1072    title
1073}
1074
1075fn shortcut_lines() -> Vec<Line<'static>> {
1076    let dimmed = Style::new().add_modifier(Modifier::DIM);
1077    vec![
1078        Line::from(Span::styled(
1079            "[q] quit | [j/k] op | [a/s] jump | [c/C] call | [g/G] start/end | [p] PC | [o] offset",
1080            dimmed,
1081        )),
1082        Line::from(Span::styled(
1083            "[/] search | [:] command | [n/N] repeat | [l] layout | [b] buffer",
1084            dimmed,
1085        )),
1086        Line::from(Span::styled(
1087            "[t] labels | [m] decode | [h] help | [J/K] stack scroll | [ctrl+j/k] data scroll | ['<char>] breakpoint",
1088            dimmed,
1089        )),
1090    ]
1091}
1092
1093const COMMAND_PROMPT_HINT: &str = "  Enter: run | Esc: cancel | help: command list";
1094
1095fn command_prompt_line(input: &str, width: u16) -> Line<'static> {
1096    let width = width as usize;
1097    let fixed_width = 2;
1098    let hint_width = text_width(COMMAND_PROMPT_HINT);
1099    let input_width = text_width(input);
1100    let include_hint = fixed_width + input_width + hint_width <= width;
1101    let input_width = if include_hint {
1102        width.saturating_sub(fixed_width + hint_width)
1103    } else {
1104        width.saturating_sub(fixed_width)
1105    };
1106
1107    let mut spans = vec![
1108        Span::styled(":", Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
1109        Span::raw(input_tail(input, input_width)),
1110        Span::styled("█", Style::new().fg(Color::Cyan)),
1111    ];
1112    if include_hint {
1113        spans.push(Span::styled(COMMAND_PROMPT_HINT, Style::new().add_modifier(Modifier::DIM)));
1114    }
1115    Line::from(spans)
1116}
1117
1118/// Returns the rendered display width of `text` in terminal cells.
1119fn text_width(text: &str) -> usize {
1120    Span::raw(text).width()
1121}
1122
1123fn input_tail(input: &str, max_width: usize) -> String {
1124    if text_width(input) <= max_width {
1125        return input.to_string();
1126    }
1127    if max_width == 0 {
1128        return String::new();
1129    }
1130    if max_width == 1 {
1131        return "<".to_string();
1132    }
1133
1134    // Reserve one cell for the leading `<` truncation indicator, then keep the widest
1135    // suffix that fits in the remaining width.
1136    let tail_width = max_width - 1;
1137    let mut start = input.len();
1138    for (idx, _) in input.char_indices().rev() {
1139        if text_width(&input[idx..]) > tail_width {
1140            break;
1141        }
1142        start = idx;
1143    }
1144    format!("<{}", &input[start..])
1145}
1146
1147fn step_notice_title(line: &str) -> &'static str {
1148    if line.starts_with(PRECOMPILE_NOTICE_PREFIX) { "precompile call" } else { "decoded step" }
1149}
1150
1151fn step_notice_line(line: &str) -> Line<'static> {
1152    Line::from(Span::styled(line.to_string(), Style::new().fg(Color::Magenta)))
1153}
1154
1155fn scope_variable_line(variable: ScopeVariable) -> Line<'static> {
1156    let color = variable.kind.color();
1157    let mut spans = Vec::with_capacity(6);
1158    spans.push(Span::styled(variable.kind.label(), Style::new().fg(Color::Gray)));
1159    spans.push(Span::raw(" "));
1160    spans.push(Span::styled(variable.name, Style::new().fg(color).add_modifier(Modifier::BOLD)));
1161    spans.push(Span::raw(" = "));
1162    if let Some(value) = variable.value {
1163        spans.push(Span::styled(value, Style::new().fg(color)));
1164    } else {
1165        spans.push(Span::styled("<unavailable>", Style::new().fg(Color::Gray)));
1166    }
1167    Line::from(spans)
1168}
1169
1170fn storage_access_line(access: StorageAccess) -> Line<'static> {
1171    Line::from(Span::styled(access.describe(), Style::new().fg(Color::Yellow)))
1172}
1173
1174fn storage_slot_lines(
1175    index: usize,
1176    index_width: usize,
1177    access: StorageAccess,
1178    current: bool,
1179) -> [Line<'static>; 2] {
1180    let value_style = if current {
1181        Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
1182    } else {
1183        Style::new().fg(Color::White)
1184    };
1185    let prefix_width = index_width + 2;
1186    [
1187        Line::from(vec![
1188            Span::styled(format!("{index:0index_width$}| "), Style::new().fg(Color::Gray)),
1189            Span::styled(access.op(), value_style),
1190            Span::raw(" slot "),
1191            Span::styled(hex_u256(access.slot()), value_style),
1192        ]),
1193        Line::from(vec![
1194            Span::raw(" ".repeat(prefix_width)),
1195            Span::raw("value "),
1196            Span::styled(hex_u256(access.value()), value_style),
1197        ]),
1198    ]
1199}
1200
1201fn variable_name(variable: &DebugVariable, index: usize, fallback_prefix: &str) -> String {
1202    variable
1203        .name
1204        .as_deref()
1205        .filter(|name| !name.is_empty())
1206        .map(ToOwned::to_owned)
1207        .unwrap_or_else(|| format!("{fallback_prefix}{index}"))
1208}
1209
1210fn decoded_internal_name_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1211    if let Some((contract_name, function_name)) = decoded_name.rsplit_once("::") {
1212        return contract_name == scope.contract_name
1213            && decoded_function_matches(function_name, scope);
1214    }
1215    decoded_function_matches(decoded_name, scope)
1216}
1217
1218fn decoded_function_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1219    if decoded_name == scope.function_name {
1220        return true;
1221    }
1222    scope_function_signature(scope).as_deref().is_some_and(|signature| decoded_name == signature)
1223}
1224
1225fn decode_external_parameter_values(
1226    scope: &DebugSourceScope,
1227    calldata: &[u8],
1228) -> Option<Vec<String>> {
1229    let types = external_scope_parameter_types(scope, calldata)?;
1230
1231    decode_abi_sequence(&types, &calldata[4..])
1232}
1233
1234fn decode_external_return_values(
1235    scope: &DebugSourceScope,
1236    calldata: &[u8],
1237    returndata: &[u8],
1238) -> Option<Vec<String>> {
1239    external_scope_parameter_types(scope, calldata)?;
1240    let returns_src = scope.returns_src.as_deref()?;
1241    let returns = Parameters::parse(returns_src).ok()?;
1242    let types = resolved_types(&returns)?;
1243    decode_abi_sequence(&types, returndata)
1244}
1245
1246fn external_scope_parameter_types(
1247    scope: &DebugSourceScope,
1248    calldata: &[u8],
1249) -> Option<Vec<DynSolType>> {
1250    if calldata.len() < 4 {
1251        return None;
1252    }
1253
1254    let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1255    let types = resolved_types(&parameters)?;
1256    let selector = function_selector(&scope.function_name, &types);
1257    if calldata.get(..4)? != selector.as_slice() {
1258        return None;
1259    }
1260
1261    Some(types)
1262}
1263
1264fn resolved_types(parameters: &Parameters<'_>) -> Option<Vec<DynSolType>> {
1265    parameters.params.iter().map(|param| param.resolve().ok()).collect()
1266}
1267
1268fn scope_function_signature(scope: &DebugSourceScope) -> Option<String> {
1269    let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1270    let types = resolved_types(&parameters)?;
1271    Some(function_signature(&scope.function_name, &types))
1272}
1273
1274fn function_selector(function_name: &str, types: &[DynSolType]) -> [u8; 4] {
1275    let signature = function_signature(function_name, types);
1276    keccak256(signature.as_bytes())[..4].try_into().unwrap()
1277}
1278
1279fn decode_abi_sequence(types: &[DynSolType], data: &[u8]) -> Option<Vec<String>> {
1280    if types.is_empty() {
1281        return Some(Vec::new());
1282    }
1283
1284    let value = DynSolType::Tuple(types.to_vec()).abi_decode_sequence(data).ok()?;
1285    let values = value.as_fixed_seq()?;
1286    Some(values.iter().map(format_token).collect())
1287}
1288
1289fn op_list_title(
1290    address: &Address,
1291    pc: usize,
1292    gas_remaining: u64,
1293    call_gas_used: u64,
1294    gas_refund_counter: u64,
1295    stats: Option<DebuggerStats>,
1296) -> String {
1297    let address = full_checksum_address(address);
1298    let mut title = format!(
1299        "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
1300         callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
1301    );
1302
1303    if let Some(stats) = stats {
1304        write!(
1305            title,
1306            " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
1307            stats.session_trace_gas_used, stats.session_subcalls
1308        )
1309        .unwrap();
1310    }
1311
1312    title
1313}
1314
1315fn full_checksum_address(address: &Address) -> String {
1316    address.to_string()
1317}
1318
1319fn stack_item_line(
1320    i: usize,
1321    min_len: usize,
1322    stack_item: &U256,
1323    param: Option<&OpcodeParam>,
1324    stack_labels: bool,
1325) -> Line<'static> {
1326    let value_style =
1327        if param.is_some() { Style::new().fg(Color::Cyan) } else { Style::new().fg(Color::White) };
1328    let mut spans = Vec::with_capacity(1 + 32 * 2 + 5);
1329
1330    // Stack index.
1331    spans.push(Span::styled(format!("{i:0min_len$}| "), Style::new().fg(Color::White)));
1332
1333    // Item hex bytes.
1334    hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| value_style);
1335
1336    spans.push(Span::raw(" | "));
1337    spans.push(Span::styled(stack_item.to_string(), value_style));
1338
1339    if stack_labels && let Some(param) = param {
1340        spans.push(Span::raw(" | "));
1341        spans.push(Span::raw(param.name));
1342    }
1343
1344    spans.push(Span::raw("\n"));
1345
1346    Line::from(spans)
1347}
1348
1349/// Wrapper around a list of [`Line`]s that prepends the line number on each new line.
1350struct SourceLines<'a> {
1351    lines: Vec<Line<'a>>,
1352    start_line: usize,
1353    max_line_num: usize,
1354}
1355
1356impl<'a> SourceLines<'a> {
1357    fn new(start_line: usize, end_line: usize) -> Self {
1358        Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
1359    }
1360
1361    fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
1362        self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
1363    }
1364
1365    fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
1366        let mut line_spans = Vec::with_capacity(4);
1367
1368        let line_number = format!(
1369            "{number: >width$} ",
1370            number = self.start_line + self.lines.len() + 1,
1371            width = self.max_line_num
1372        );
1373        line_spans.push(Span::styled(line_number, line_number_style));
1374
1375        // Space between line number and line text.
1376        line_spans.push(Span::raw("  "));
1377
1378        line_spans.extend_from_slice(spans);
1379
1380        self.lines.push(Line::from(line_spans));
1381    }
1382}
1383
1384fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
1385    for (i, &byte) in bytes.iter().enumerate() {
1386        if i > 0 {
1387            spans.push(Span::raw(" "));
1388        }
1389        spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
1390    }
1391}
1392
1393/// Returns the number of decimal digits in the given number.
1394///
1395/// This is the same as `n.to_string().len()`.
1396fn decimal_digits(n: usize) -> usize {
1397    n.checked_ilog10().unwrap_or(0) as usize + 1
1398}
1399
1400/// Returns the number of hexadecimal digits in the given number.
1401///
1402/// This is the same as `format!("{n:x}").len()`.
1403fn hex_digits(n: usize) -> usize {
1404    n.checked_ilog(16).unwrap_or(0) as usize + 1
1405}
1406
1407#[cfg(test)]
1408mod tests {
1409    use super::TUIContext;
1410    use crate::{
1411        DebugNode, DebuggerLayout,
1412        debugger::{DebuggerContext, DebuggerStats},
1413        op::OpcodeParam,
1414    };
1415    use alloy_dyn_abi::parser::Parameters;
1416    use alloy_primitives::{Address, Bytes, U256, address};
1417    use foundry_evm_core::Breakpoints;
1418    use foundry_evm_traces::debug::{ContractSources, DebugSourceScope, DebugVariable};
1419    use ratatui::{
1420        Terminal,
1421        backend::TestBackend,
1422        layout::Rect,
1423        style::{Color, Style},
1424        text::Line,
1425    };
1426    use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
1427    use revm_inspectors::tracing::types::{
1428        CallKind, CallTraceStep, DecodedCallData, DecodedCallTrace, DecodedInternalCall,
1429        DecodedTraceStep, StorageChange, StorageChangeReason,
1430    };
1431
1432    fn line_text(line: &Line<'_>) -> String {
1433        line.spans.iter().map(|span| span.content.as_ref()).collect()
1434    }
1435
1436    fn scope(function_name: &str, parameters_src: &str) -> DebugSourceScope {
1437        DebugSourceScope {
1438            contract_name: "DebugMe".to_string(),
1439            function_name: function_name.to_string(),
1440            range: 0..100,
1441            body_range: 10..90,
1442            parameters_src: parameters_src.to_string(),
1443            returns_src: None,
1444            parameters: Vec::new(),
1445            returns: Vec::new(),
1446            locals: Vec::new(),
1447        }
1448    }
1449
1450    fn scope_with_returns(
1451        function_name: &str,
1452        parameters_src: &str,
1453        returns_src: &str,
1454    ) -> DebugSourceScope {
1455        DebugSourceScope {
1456            returns_src: Some(returns_src.to_string()),
1457            ..scope(function_name, parameters_src)
1458        }
1459    }
1460
1461    fn trace_step(stack: Vec<U256>) -> CallTraceStep {
1462        CallTraceStep {
1463            pc: 0,
1464            op: OpCode::STOP,
1465            stack: Some(stack.into_boxed_slice()),
1466            push_stack: None,
1467            memory: None,
1468            returndata: Bytes::new(),
1469            gas_remaining: 0,
1470            gas_refund_counter: 0,
1471            gas_used: 0,
1472            gas_cost: 0,
1473            storage_change: None,
1474            status: Some(InstructionResult::Stop),
1475            immediate_bytes: None,
1476            decoded: None,
1477        }
1478    }
1479
1480    fn internal_call_step(end_step: usize, return_data: Vec<String>) -> CallTraceStep {
1481        internal_call_step_named("DebugMe::foo", end_step, Some(Vec::new()), Some(return_data))
1482    }
1483
1484    fn internal_call_step_named(
1485        func_name: &str,
1486        end_step: usize,
1487        args: Option<Vec<String>>,
1488        return_data: Option<Vec<String>>,
1489    ) -> CallTraceStep {
1490        let mut step = trace_step(Vec::new());
1491        step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1492            DecodedInternalCall { func_name: func_name.to_string(), args, return_data },
1493            end_step,
1494        )));
1495        step
1496    }
1497
1498    fn internal_call_step_without_args(end_step: usize) -> CallTraceStep {
1499        let mut step = trace_step(Vec::new());
1500        step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1501            DecodedInternalCall {
1502                func_name: "DebugMe::foo".to_string(),
1503                args: None,
1504                return_data: None,
1505            },
1506            end_step,
1507        )));
1508        step
1509    }
1510
1511    fn debug_node(
1512        trace_node_idx: usize,
1513        step_offset: usize,
1514        steps: Vec<CallTraceStep>,
1515    ) -> DebugNode {
1516        let mut node = DebugNode::new(Address::ZERO, CallKind::Call, steps, Bytes::new(), 0, None);
1517        node.trace_node_idx = trace_node_idx;
1518        node.step_offset = step_offset;
1519        node
1520    }
1521
1522    fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1523        DebuggerContext {
1524            debug_arena: arena,
1525            stats: None,
1526            identified_contracts: Default::default(),
1527            contracts_sources: ContractSources::default(),
1528            breakpoints: Breakpoints::default(),
1529            layout: Default::default(),
1530        }
1531    }
1532
1533    fn abi_word(value: U256) -> [u8; 32] {
1534        value.to_be_bytes::<32>()
1535    }
1536
1537    #[test]
1538    fn draw_buffer_handles_missing_memory_snapshot() {
1539        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1540        let tui = TUIContext::new(&mut context);
1541        let backend = TestBackend::new(80, 4);
1542        let mut terminal = Terminal::new(backend).unwrap();
1543
1544        terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 4))).unwrap();
1545
1546        let screen = terminal
1547            .backend()
1548            .buffer()
1549            .content()
1550            .iter()
1551            .map(|cell| cell.symbol())
1552            .collect::<String>();
1553        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1554    }
1555
1556    #[test]
1557    fn storage_explorer_draws_accessed_slots() {
1558        let mut first = trace_step(Vec::new());
1559        first.storage_change = Some(Box::new(StorageChange {
1560            key: U256::ZERO,
1561            value: U256::from(42),
1562            had_value: None,
1563            reason: StorageChangeReason::SSTORE,
1564        }));
1565        let mut second = trace_step(Vec::new());
1566        second.storage_change = Some(Box::new(StorageChange {
1567            key: U256::from(1),
1568            value: U256::from(0xbeef),
1569            had_value: None,
1570            reason: StorageChangeReason::SSTORE,
1571        }));
1572        let mut latest = trace_step(Vec::new());
1573        latest.storage_change = Some(Box::new(StorageChange {
1574            key: U256::ZERO,
1575            value: U256::from(43),
1576            had_value: Some(U256::from(42)),
1577            reason: StorageChangeReason::SSTORE,
1578        }));
1579        let mut context = context_with_arena(vec![debug_node(0, 0, vec![first, second, latest])]);
1580        let mut tui = TUIContext::new(&mut context);
1581        tui.current_step = 2;
1582        let backend = TestBackend::new(100, 6);
1583        let mut terminal = Terminal::new(backend).unwrap();
1584
1585        terminal
1586            .draw(|f| tui.draw_storage(f, Rect::new(0, 0, 100, 6), super::StorageSpace::Persistent))
1587            .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("Storage: 2 accessed slots"));
1597        assert!(screen.contains("SSTORE slot 0x0"));
1598        assert!(screen.contains("value 0x2b"));
1599        assert!(screen.contains("SSTORE slot 0x1"));
1600        assert!(screen.contains("value 0xbeef"));
1601    }
1602
1603    #[test]
1604    fn hidden_source_pane_omits_source_panel() {
1605        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1606        context.layout = DebuggerLayout::Horizontal;
1607        let mut tui = TUIContext::new(&mut context);
1608        tui.init();
1609        tui.show_source = false;
1610        let backend = TestBackend::new(220, 30);
1611        let mut terminal = Terminal::new(backend).unwrap();
1612
1613        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1614
1615        let screen = terminal
1616            .backend()
1617            .buffer()
1618            .content()
1619            .iter()
1620            .map(|cell| cell.symbol())
1621            .collect::<String>();
1622        assert!(!screen.contains("Contract call"));
1623        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1624    }
1625
1626    #[test]
1627    fn hidden_opcodes_pane_omits_opcode_panel() {
1628        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1629        context.layout = DebuggerLayout::Horizontal;
1630        let mut tui = TUIContext::new(&mut context);
1631        tui.init();
1632        tui.show_opcodes = false;
1633        let backend = TestBackend::new(220, 30);
1634        let mut terminal = Terminal::new(backend).unwrap();
1635
1636        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1637
1638        let screen = terminal
1639            .backend()
1640            .buffer()
1641            .content()
1642            .iter()
1643            .map(|cell| cell.symbol())
1644            .collect::<String>();
1645        assert!(!screen.contains("address:"));
1646        assert!(screen.contains("Contract call"));
1647        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1648    }
1649
1650    #[test]
1651    fn hidden_variables_pane_omits_variables_panel() {
1652        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1653        context.layout = DebuggerLayout::Horizontal;
1654        let mut tui = TUIContext::new(&mut context);
1655        tui.init();
1656        tui.show_variables = false;
1657        let backend = TestBackend::new(220, 30);
1658        let mut terminal = Terminal::new(backend).unwrap();
1659
1660        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1661
1662        let screen = terminal
1663            .backend()
1664            .buffer()
1665            .content()
1666            .iter()
1667            .map(|cell| cell.symbol())
1668            .collect::<String>();
1669        assert!(!screen.contains("Variables"));
1670        assert!(screen.contains("Stack"));
1671        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1672    }
1673
1674    #[test]
1675    fn hidden_stack_pane_omits_stack_panel() {
1676        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1677        context.layout = DebuggerLayout::Horizontal;
1678        let mut tui = TUIContext::new(&mut context);
1679        tui.init();
1680        tui.show_stack = false;
1681        let backend = TestBackend::new(220, 30);
1682        let mut terminal = Terminal::new(backend).unwrap();
1683
1684        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1685
1686        let screen = terminal
1687            .backend()
1688            .buffer()
1689            .content()
1690            .iter()
1691            .map(|cell| cell.symbol())
1692            .collect::<String>();
1693        assert!(!screen.contains("Stack:"));
1694        assert!(screen.contains("Variables"));
1695        assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1696    }
1697
1698    #[test]
1699    fn hidden_data_pane_omits_data_panel() {
1700        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1701        context.layout = DebuggerLayout::Horizontal;
1702        let mut tui = TUIContext::new(&mut context);
1703        tui.init();
1704        tui.show_opcodes = false;
1705        tui.show_variables = false;
1706        tui.show_stack = false;
1707        tui.show_data = false;
1708        let backend = TestBackend::new(80, 30);
1709        let mut terminal = Terminal::new(backend).unwrap();
1710
1711        terminal.draw(|f| tui.draw_layout(f)).unwrap();
1712
1713        let screen = terminal
1714            .backend()
1715            .buffer()
1716            .content()
1717            .iter()
1718            .map(|cell| cell.symbol())
1719            .collect::<String>();
1720        assert!(!screen.contains("Memory (max expansion: 0 bytes)"));
1721        assert!(screen.contains("Contract call"));
1722    }
1723
1724    #[test]
1725    fn command_prompt_draws_in_bordered_block() {
1726        let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1727        let mut tui = TUIContext::new(&mut context);
1728        tui.command_input = Some("pc 0".to_string());
1729        let backend = TestBackend::new(120, 6);
1730        let mut terminal = Terminal::new(backend).unwrap();
1731
1732        terminal.draw(|f| tui.draw_footer(f, Rect::new(0, 0, 120, 6))).unwrap();
1733
1734        let screen = terminal
1735            .backend()
1736            .buffer()
1737            .content()
1738            .iter()
1739            .map(|cell| cell.symbol())
1740            .collect::<String>();
1741        assert!(screen.contains("Command"));
1742        assert!(screen.contains(":pc 0"));
1743        assert!(screen.contains("Enter: run"));
1744        assert!(screen.contains("[:] command"));
1745    }
1746
1747    #[test]
1748    fn command_prompt_line_clips_long_input_to_tail() {
1749        let input = "continue 0123456789abcdef";
1750        let line = super::command_prompt_line(input, 12);
1751        let text = line_text(&line);
1752
1753        assert!(line.width() <= 12);
1754        assert_eq!(text, ":<789abcdef█");
1755        assert!(!text.contains("Enter: run"));
1756    }
1757
1758    #[test]
1759    fn command_prompt_line_clips_wide_unicode_to_width() {
1760        // Full-width characters render two cells each, so clipping must respect display
1761        // width rather than character count.
1762        let input = "界界界界界界界界";
1763        let line = super::command_prompt_line(input, 8);
1764
1765        assert!(line.width() <= 8);
1766    }
1767
1768    #[test]
1769    fn decode_external_parameter_values_decodes_named_params() {
1770        let scope = scope("foo", "(uint256 amount, bool ok)");
1771        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1772        let types = super::resolved_types(&parameters).unwrap();
1773        let mut calldata = Vec::new();
1774        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1775        calldata.extend_from_slice(&abi_word(U256::from(42)));
1776        calldata.extend_from_slice(&abi_word(U256::from(1)));
1777
1778        let values = super::decode_external_parameter_values(&scope, &calldata).unwrap();
1779
1780        assert_eq!(values, ["42", "true"]);
1781    }
1782
1783    #[test]
1784    fn decode_external_parameter_values_rejects_selector_mismatch() {
1785        let scope = scope("foo", "(uint256 amount)");
1786        let parameters = Parameters::parse("(uint256 amount)").unwrap();
1787        let types = super::resolved_types(&parameters).unwrap();
1788        let mut calldata = Vec::new();
1789        calldata.extend_from_slice(&super::function_selector("bar", &types));
1790        calldata.extend_from_slice(&abi_word(U256::from(42)));
1791
1792        assert_eq!(super::decode_external_parameter_values(&scope, &calldata), None);
1793    }
1794
1795    #[test]
1796    fn decode_external_return_values_decodes_named_returns() {
1797        let scope = scope_with_returns("foo", "(uint256 amount)", "(uint256 total, bool ok)");
1798        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1799        let types = super::resolved_types(&parameters).unwrap();
1800        let mut calldata = Vec::new();
1801        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1802        calldata.extend_from_slice(&abi_word(U256::from(42)));
1803        let mut returndata = Vec::new();
1804        returndata.extend_from_slice(&abi_word(U256::from(99)));
1805        returndata.extend_from_slice(&abi_word(U256::from(1)));
1806
1807        let values = super::decode_external_return_values(&scope, &calldata, &returndata).unwrap();
1808
1809        assert_eq!(values, ["99", "true"]);
1810    }
1811
1812    #[test]
1813    fn decode_return_values_uses_external_output_at_frame_end() {
1814        let scope = scope_with_returns("foo", "()", "(uint256 total)");
1815        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1816        let types = super::resolved_types(&parameters).unwrap();
1817        let mut calldata = Vec::new();
1818        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1819        let mut node = debug_node(
1820            0,
1821            0,
1822            vec![trace_step(Vec::new()), {
1823                let mut step = trace_step(Vec::new());
1824                step.op = OpCode::RETURN;
1825                step.status = Some(InstructionResult::Return);
1826                step
1827            }],
1828        );
1829        node.calldata = Bytes::from(calldata);
1830        node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1831        let mut context = context_with_arena(vec![node]);
1832        let mut tui = TUIContext::new(&mut context);
1833        tui.current_step = 1;
1834
1835        assert_eq!(tui.decode_return_values(&scope), Some(vec!["123".to_string()]));
1836    }
1837
1838    #[test]
1839    fn decode_return_values_waits_for_external_frame_end() {
1840        let scope = scope_with_returns("foo", "()", "(uint256 total)");
1841        let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1842        let types = super::resolved_types(&parameters).unwrap();
1843        let mut calldata = Vec::new();
1844        calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1845        let mut node = debug_node(0, 0, vec![trace_step(Vec::new()), trace_step(Vec::new())]);
1846        node.calldata = Bytes::from(calldata);
1847        node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1848        let mut context = context_with_arena(vec![node]);
1849        let mut tui = TUIContext::new(&mut context);
1850
1851        assert_eq!(tui.decode_return_values(&scope), None);
1852    }
1853
1854    #[test]
1855    fn scope_function_signature_includes_resolved_parameter_types() {
1856        let scope = scope("foo", "(uint256 amount, bool ok)");
1857
1858        assert_eq!(super::scope_function_signature(&scope).as_deref(), Some("foo(uint256,bool)"));
1859    }
1860
1861    #[test]
1862    fn decode_parameter_values_rejects_decoded_call_data_for_wrong_overload() {
1863        let mut node = debug_node(0, 0, vec![trace_step(Vec::new())]);
1864        node.decoded = Some(Box::new(DecodedCallTrace {
1865            call_data: Some(DecodedCallData {
1866                signature: "foo(address)".to_string(),
1867                args: vec!["0x000000000000000000000000000000000000002a".to_string()],
1868            }),
1869            ..Default::default()
1870        }));
1871        let mut context = context_with_arena(vec![node]);
1872        let mut tui = TUIContext::new(&mut context);
1873
1874        assert_eq!(tui.decode_parameter_values(&scope("foo", "(uint256 amount)")), None);
1875    }
1876
1877    #[test]
1878    fn decode_step_parameters_reads_static_values_from_stack() {
1879        let step = trace_step(vec![U256::from(42), U256::from(1)]);
1880        let parameters = Parameters::parse("(uint256 amount, bool ok)").unwrap();
1881        let values = super::decode_step_parameters(&parameters, &step, None).unwrap();
1882
1883        assert_eq!(values, ["42", "true"]);
1884    }
1885
1886    #[test]
1887    fn decode_internal_parameter_values_uses_absolute_entry_step() {
1888        let mut context = context_with_arena(vec![
1889            debug_node(0, 0, vec![internal_call_step_without_args(2)]),
1890            debug_node(1, 0, vec![trace_step(Vec::new())]),
1891            debug_node(0, 1, vec![trace_step(vec![U256::from(42)])]),
1892        ]);
1893        let mut tui = TUIContext::new(&mut context);
1894        tui.draw_memory.inner_call_index = 2;
1895
1896        assert_eq!(
1897            tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
1898            Some(vec!["42".to_string()])
1899        );
1900    }
1901
1902    #[test]
1903    fn decode_internal_parameter_values_passes_calldata_to_fallback_decoder() {
1904        let digest = U256::from(0x1234);
1905        let offset = 0x44;
1906        let mut calldata = vec![0; offset];
1907        calldata.extend_from_slice(&[0x11, 0x22, 0x33]);
1908
1909        let mut entry_node =
1910            debug_node(0, 1, vec![trace_step(vec![digest, U256::from(offset), U256::from(3)])]);
1911        entry_node.calldata = Bytes::from(calldata);
1912
1913        let mut context = context_with_arena(vec![
1914            debug_node(0, 0, vec![internal_call_step_without_args(2)]),
1915            debug_node(1, 0, vec![trace_step(Vec::new())]),
1916            entry_node,
1917        ]);
1918        let mut tui = TUIContext::new(&mut context);
1919        tui.draw_memory.inner_call_index = 2;
1920
1921        assert_eq!(
1922            tui.decode_internal_parameter_values(&scope(
1923                "foo",
1924                "(bytes32 digest, bytes calldata signature)"
1925            )),
1926            Some(vec![
1927                "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
1928                "0x112233".to_string(),
1929            ])
1930        );
1931    }
1932
1933    #[test]
1934    fn decode_internal_parameter_values_accepts_matching_overload_args() {
1935        let mut context = context_with_arena(vec![debug_node(
1936            0,
1937            0,
1938            vec![internal_call_step_named(
1939                "DebugMe::foo(uint256)",
1940                2,
1941                Some(vec!["42".to_string()]),
1942                None,
1943            )],
1944        )]);
1945        let mut tui = TUIContext::new(&mut context);
1946
1947        assert_eq!(
1948            tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
1949            Some(vec!["42".to_string()])
1950        );
1951    }
1952
1953    #[test]
1954    fn decode_internal_parameter_values_rejects_wrong_overload_args() {
1955        let mut context = context_with_arena(vec![debug_node(
1956            0,
1957            0,
1958            vec![internal_call_step_named(
1959                "DebugMe::foo(address)",
1960                2,
1961                Some(vec!["0x000000000000000000000000000000000000002a".to_string()]),
1962                None,
1963            )],
1964        )]);
1965        let mut tui = TUIContext::new(&mut context);
1966
1967        assert_eq!(tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), None);
1968    }
1969
1970    #[test]
1971    fn decode_return_values_uses_absolute_internal_call_end_step() {
1972        let mut context = context_with_arena(vec![debug_node(
1973            0,
1974            3,
1975            vec![internal_call_step(4, vec!["99".to_string()]), trace_step(Vec::new())],
1976        )]);
1977        let mut tui = TUIContext::new(&mut context);
1978        tui.current_step = 1;
1979
1980        assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["99".to_string()]));
1981    }
1982
1983    #[test]
1984    fn decode_return_values_finds_internal_call_split_by_child_node() {
1985        let mut context = context_with_arena(vec![
1986            debug_node(0, 0, vec![internal_call_step(2, vec!["7".to_string()])]),
1987            debug_node(1, 0, vec![trace_step(Vec::new())]),
1988            debug_node(0, 2, vec![trace_step(Vec::new())]),
1989        ]);
1990        let mut tui = TUIContext::new(&mut context);
1991        tui.draw_memory.inner_call_index = 2;
1992
1993        assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["7".to_string()]));
1994    }
1995
1996    #[test]
1997    fn decode_return_values_rejects_wrong_overload() {
1998        let mut context = context_with_arena(vec![debug_node(
1999            0,
2000            0,
2001            vec![
2002                internal_call_step_named(
2003                    "DebugMe::foo(address)",
2004                    1,
2005                    None,
2006                    Some(vec!["99".to_string()]),
2007                ),
2008                trace_step(Vec::new()),
2009            ],
2010        )]);
2011        let mut tui = TUIContext::new(&mut context);
2012        tui.current_step = 1;
2013
2014        assert_eq!(tui.decode_return_values(&scope("foo", "(uint256 amount)")), None);
2015    }
2016
2017    #[test]
2018    fn active_internal_call_caches_by_current_node_and_step() {
2019        let mut context = context_with_arena(vec![debug_node(
2020            0,
2021            0,
2022            vec![internal_call_step(2, vec!["1".to_string()]), trace_step(Vec::new())],
2023        )]);
2024        let mut tui = TUIContext::new(&mut context);
2025
2026        assert!(tui.active_internal_call().is_some());
2027        let cache = tui.draw_memory.active_internal_call;
2028        assert!(cache.and_then(|cache| cache.location).is_some());
2029
2030        assert!(tui.active_internal_call().is_some());
2031        assert_eq!(tui.draw_memory.active_internal_call, cache);
2032
2033        tui.current_step = 1;
2034        assert!(tui.active_internal_call().is_some());
2035        assert_ne!(tui.draw_memory.active_internal_call, cache);
2036    }
2037
2038    #[test]
2039    fn decoded_internal_name_matches_exact_contract_and_function() {
2040        let scope = scope("foo", "()");
2041
2042        assert!(super::decoded_internal_name_matches("DebugMe::foo", &scope));
2043        assert!(!super::decoded_internal_name_matches("DebugMe::barfoo", &scope));
2044        assert!(!super::decoded_internal_name_matches("Other::foo", &scope));
2045    }
2046
2047    #[test]
2048    fn decoded_internal_name_matches_canonical_signature_for_overloads() {
2049        let scope = scope("foo", "(uint256 amount)");
2050
2051        assert!(super::decoded_internal_name_matches("DebugMe::foo(uint256)", &scope));
2052        assert!(!super::decoded_internal_name_matches("DebugMe::foo(address)", &scope));
2053        assert!(!super::decoded_internal_name_matches("Other::foo(uint256)", &scope));
2054    }
2055
2056    #[test]
2057    fn scope_variable_line_marks_unavailable_locals() {
2058        let variable = super::ScopeVariable {
2059            kind: super::ScopeVariableKind::Local,
2060            name: "sum".to_string(),
2061            value: None,
2062        };
2063
2064        assert_eq!(line_text(&super::scope_variable_line(variable)), "local sum = <unavailable>");
2065    }
2066
2067    #[test]
2068    fn storage_access_line_formats_sload() {
2069        let mut step = trace_step(Vec::new());
2070        step.storage_change = Some(Box::new(StorageChange {
2071            key: U256::from(1),
2072            value: U256::from(42),
2073            had_value: None,
2074            reason: StorageChangeReason::SLOAD,
2075        }));
2076        let steps = [step];
2077        let access = super::storage_access_at(&steps, 0).unwrap();
2078
2079        assert_eq!(line_text(&super::storage_access_line(access)), "storage SLOAD slot 0x1 = 0x2a");
2080    }
2081
2082    #[test]
2083    fn storage_access_line_formats_sstore_with_previous_value() {
2084        let mut step = trace_step(Vec::new());
2085        step.storage_change = Some(Box::new(StorageChange {
2086            key: U256::from(1),
2087            value: U256::from(42),
2088            had_value: Some(U256::from(7)),
2089            reason: StorageChangeReason::SSTORE,
2090        }));
2091        let steps = [step];
2092        let access = super::storage_access_at(&steps, 0).unwrap();
2093
2094        assert_eq!(
2095            line_text(&super::storage_access_line(access)),
2096            "storage SSTORE slot 0x1: 0x7 -> 0x2a"
2097        );
2098    }
2099
2100    #[test]
2101    fn current_storage_access_line_uses_next_stack_snapshot_for_warm_sload() {
2102        let mut step = trace_step(vec![U256::from(1)]);
2103        step.op = OpCode::SLOAD;
2104        step.storage_change = None;
2105        let next_step = trace_step(vec![U256::from(42)]);
2106        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2107        let tui = TUIContext::new(&mut context);
2108
2109        assert_eq!(
2110            line_text(&tui.current_storage_access_line().unwrap()),
2111            "storage SLOAD slot 0x1 = 0x2a"
2112        );
2113    }
2114
2115    #[test]
2116    fn current_storage_access_line_uses_next_stack_snapshot_for_tload() {
2117        let mut step = trace_step(vec![U256::from(1)]);
2118        step.op = OpCode::TLOAD;
2119        let next_step = trace_step(vec![U256::from(42)]);
2120        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2121        let tui = TUIContext::new(&mut context);
2122
2123        assert_eq!(
2124            line_text(&tui.current_storage_access_line().unwrap()),
2125            "transient storage TLOAD slot 0x1 = 0x2a"
2126        );
2127    }
2128
2129    #[test]
2130    fn variable_name_falls_back_for_unnamed_values() {
2131        let variable = DebugVariable { name: None, declaration: 0..1, scope: 0..2 };
2132
2133        assert_eq!(super::variable_name(&variable, 2, "arg"), "arg2");
2134    }
2135
2136    #[test]
2137    fn current_step_notice_text_reads_decoded_line_steps() {
2138        let mut step = trace_step(Vec::new());
2139        step.decoded = Some(Box::new(DecodedTraceStep::Line(
2140            "precompile: PRECOMPILES::sha256(0x68656c6c6f)".to_string(),
2141        )));
2142        let mut context = context_with_arena(vec![debug_node(0, 0, vec![step])]);
2143        let tui = TUIContext::new(&mut context);
2144
2145        assert_eq!(
2146            tui.current_step_notice_text(),
2147            Some("precompile: PRECOMPILES::sha256(0x68656c6c6f)")
2148        );
2149    }
2150
2151    #[test]
2152    fn source_pane_title_includes_precompile_notice_before_source() {
2153        assert_eq!(
2154            super::source_pane_title(
2155                "Contract call",
2156                Some("test/Precompile.t.sol"),
2157                Some("precompile call")
2158            ),
2159            "Contract call | precompile call | test/Precompile.t.sol "
2160        );
2161    }
2162
2163    #[test]
2164    fn variables_title_tracks_decoded_step_notices() {
2165        assert_eq!(super::variables_title(0, 0, false, false), "Variables");
2166        assert_eq!(super::variables_title(0, 0, false, true), "Variables: 0/0 | Trace");
2167        assert_eq!(super::variables_title(2, 1, true, true), "Variables: 1/2 | Trace | Storage");
2168    }
2169
2170    #[test]
2171    fn step_notice_line_highlights_precompile_clue() {
2172        let line = super::step_notice_line("precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2173
2174        assert_eq!(line_text(&line), "precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2175        assert_eq!(line.spans[0].style, Style::new().fg(Color::Magenta));
2176        assert_eq!(super::step_notice_title(&line_text(&line)), "precompile call");
2177    }
2178
2179    #[test]
2180    fn op_list_title_includes_gas_and_subcall_stats() {
2181        let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
2182        let address = Address::from([0x42; 20]);
2183        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
2184
2185        assert!(title.contains("pc: 0x2a (42)"));
2186        assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
2187        assert!(title.contains("gasLeft: 123456"));
2188        assert!(title.contains("sessionTraceGasUsed: 789012"));
2189        assert!(title.contains("sessionSubcalls: 3"));
2190        assert!(title.contains("callGasUsed: 42"));
2191        assert!(title.contains("gasRefund: 7"));
2192    }
2193
2194    #[test]
2195    fn op_list_title_omits_aggregate_stats_when_unavailable() {
2196        let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
2197
2198        assert!(!title.contains("sessionTraceGasUsed"));
2199        assert!(!title.contains("sessionSubcalls"));
2200    }
2201
2202    #[test]
2203    fn op_list_title_uses_full_checksum_address() {
2204        let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
2205        let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
2206
2207        assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2208        assert!(!title.contains('…'));
2209    }
2210
2211    #[test]
2212    fn stack_item_line_includes_decimal_preview() {
2213        let line = super::stack_item_line(0, 2, &U256::from(42), None, false);
2214        let text = line_text(&line);
2215
2216        assert!(text.starts_with("00| "));
2217        assert!(text.ends_with("2a | 42\n"));
2218    }
2219
2220    #[test]
2221    fn stack_item_line_keeps_stack_labels_after_decimal_preview() {
2222        let param = OpcodeParam { name: "offset", index: 0 };
2223        let line = super::stack_item_line(0, 2, &U256::from(16), Some(&param), true);
2224
2225        assert!(line_text(&line).ends_with("10 | 16 | offset\n"));
2226    }
2227
2228    #[test]
2229    fn stack_item_line_highlights_decimal_preview_for_opcode_params() {
2230        let param = OpcodeParam { name: "offset", index: 0 };
2231        let line = super::stack_item_line(0, 2, &U256::from(16), Some(&param), false);
2232        let decimal = line.spans.iter().find(|span| span.content.as_ref() == "16").unwrap();
2233
2234        assert_eq!(decimal.style, Style::new().fg(Color::Cyan));
2235    }
2236
2237    #[test]
2238    fn decimal_digits() {
2239        assert_eq!(super::decimal_digits(0), 1);
2240        assert_eq!(super::decimal_digits(1), 1);
2241        assert_eq!(super::decimal_digits(2), 1);
2242        assert_eq!(super::decimal_digits(9), 1);
2243        assert_eq!(super::decimal_digits(10), 2);
2244        assert_eq!(super::decimal_digits(11), 2);
2245        assert_eq!(super::decimal_digits(50), 2);
2246        assert_eq!(super::decimal_digits(99), 2);
2247        assert_eq!(super::decimal_digits(100), 3);
2248        assert_eq!(super::decimal_digits(101), 3);
2249        assert_eq!(super::decimal_digits(201), 3);
2250        assert_eq!(super::decimal_digits(999), 3);
2251        assert_eq!(super::decimal_digits(1000), 4);
2252        assert_eq!(super::decimal_digits(1001), 4);
2253    }
2254
2255    #[test]
2256    fn hex_digits() {
2257        assert_eq!(super::hex_digits(0), 1);
2258        assert_eq!(super::hex_digits(1), 1);
2259        assert_eq!(super::hex_digits(2), 1);
2260        assert_eq!(super::hex_digits(9), 1);
2261        assert_eq!(super::hex_digits(10), 1);
2262        assert_eq!(super::hex_digits(11), 1);
2263        assert_eq!(super::hex_digits(15), 1);
2264        assert_eq!(super::hex_digits(16), 2);
2265        assert_eq!(super::hex_digits(17), 2);
2266        assert_eq!(super::hex_digits(0xff), 2);
2267        assert_eq!(super::hex_digits(0x100), 3);
2268        assert_eq!(super::hex_digits(0x101), 3);
2269    }
2270}