Skip to main content

foundry_debugger/tui/
context.rs

1//! Debugger context and event handler implementation.
2
3use crate::{DebugNode, ExitReason, debugger::DebuggerContext};
4use alloy_primitives::{Address, hex};
5use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
6use foundry_evm_core::buffer::BufferKind;
7use foundry_tui::TuiApp;
8use ratatui::Frame;
9use revm::bytecode::opcode::OpCode;
10use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
11use std::ops::ControlFlow;
12
13/// This is currently used to remember last scroll position so screen doesn't wiggle as much.
14#[derive(Default)]
15pub(crate) struct DrawMemory {
16    pub(crate) inner_call_index: usize,
17    pub(crate) current_buf_startline: usize,
18    pub(crate) current_stack_startline: usize,
19}
20
21pub(crate) struct TUIContext<'a> {
22    pub(crate) debugger_context: &'a mut DebuggerContext,
23
24    /// Buffer for keys prior to execution, i.e. '10' + 'k' => move up 10 operations.
25    pub(crate) key_buffer: String,
26    /// Current step in the debug steps.
27    pub(crate) current_step: usize,
28    pub(crate) draw_memory: DrawMemory,
29    pub(crate) opcode_list: Vec<String>,
30    pub(crate) last_index: usize,
31
32    pub(crate) stack_labels: bool,
33    /// Whether to decode active buffer as utf8 or not.
34    pub(crate) buf_utf: bool,
35    pub(crate) show_shortcuts: bool,
36    /// The currently active buffer (memory, calldata, returndata) to be drawn.
37    pub(crate) active_buffer: BufferKind,
38}
39
40impl<'a> TUIContext<'a> {
41    pub(crate) fn new(debugger_context: &'a mut DebuggerContext) -> Self {
42        TUIContext {
43            debugger_context,
44
45            key_buffer: String::with_capacity(64),
46            current_step: 0,
47            draw_memory: DrawMemory::default(),
48            opcode_list: Vec::new(),
49            last_index: 0,
50
51            stack_labels: false,
52            buf_utf: false,
53            show_shortcuts: true,
54            active_buffer: BufferKind::Memory,
55        }
56    }
57
58    pub(crate) fn init(&mut self) {
59        self.gen_opcode_list();
60    }
61
62    pub(crate) fn debug_arena(&self) -> &[DebugNode] {
63        &self.debugger_context.debug_arena
64    }
65
66    pub(crate) fn debug_call(&self) -> &DebugNode {
67        &self.debug_arena()[self.draw_memory.inner_call_index]
68    }
69
70    /// Returns the current call address.
71    pub(crate) fn address(&self) -> &Address {
72        &self.debug_call().address
73    }
74
75    /// Returns the current call kind.
76    pub(crate) fn call_kind(&self) -> CallKind {
77        self.debug_call().kind
78    }
79
80    /// Returns the current debug steps.
81    pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
82        &self.debug_call().steps
83    }
84
85    /// Returns the current debug step.
86    pub(crate) fn current_step(&self) -> &CallTraceStep {
87        &self.debug_steps()[self.current_step]
88    }
89
90    fn gen_opcode_list(&mut self) {
91        self.opcode_list.clear();
92        let debug_steps =
93            &self.debugger_context.debug_arena[self.draw_memory.inner_call_index].steps;
94        for step in debug_steps {
95            self.opcode_list.push(pretty_opcode(step));
96        }
97    }
98
99    fn gen_opcode_list_if_necessary(&mut self) {
100        if self.last_index != self.draw_memory.inner_call_index {
101            self.gen_opcode_list();
102            self.last_index = self.draw_memory.inner_call_index;
103        }
104    }
105
106    fn active_buffer(&self) -> &[u8] {
107        match self.active_buffer {
108            BufferKind::Memory => self.current_step().memory.as_ref().unwrap().as_bytes(),
109            BufferKind::Calldata => &self.debug_call().calldata,
110            BufferKind::Returndata => &self.current_step().returndata,
111        }
112    }
113}
114
115impl TUIContext<'_> {
116    pub(crate) fn handle_event(&mut self, event: Event) -> ControlFlow<ExitReason> {
117        let ret = match event {
118            Event::Key(event) => self.handle_key_event(event),
119            Event::Mouse(event) => self.handle_mouse_event(event),
120            _ => ControlFlow::Continue(()),
121        };
122        // Generate the list after the event has been handled.
123        self.gen_opcode_list_if_necessary();
124        ret
125    }
126
127    fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
128        // Breakpoints
129        if let KeyCode::Char(c) = event.code
130            && c.is_alphabetic()
131            && self.key_buffer.starts_with('\'')
132        {
133            self.handle_breakpoint(c);
134            return ControlFlow::Continue(());
135        }
136
137        let control = event.modifiers.contains(KeyModifiers::CONTROL);
138
139        match event.code {
140            // Exit
141            KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
142
143            // Scroll up the memory buffer
144            KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
145                this.draw_memory.current_buf_startline =
146                    this.draw_memory.current_buf_startline.saturating_sub(1);
147            }),
148            // Scroll down the memory buffer
149            KeyCode::Char('j') | KeyCode::Down if control => self.repeat(|this| {
150                let max_buf = (this.active_buffer().len() / 32).saturating_sub(1);
151                if this.draw_memory.current_buf_startline < max_buf {
152                    this.draw_memory.current_buf_startline += 1;
153                }
154            }),
155
156            // Move up
157            KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
158            // Move down
159            KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
160
161            // Scroll up the stack
162            KeyCode::Char('K') => self.repeat(|this| {
163                this.draw_memory.current_stack_startline =
164                    this.draw_memory.current_stack_startline.saturating_sub(1);
165            }),
166            // Scroll down the stack
167            KeyCode::Char('J') => self.repeat(|this| {
168                let max_stack =
169                    this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
170                if this.draw_memory.current_stack_startline < max_stack {
171                    this.draw_memory.current_stack_startline += 1;
172                }
173            }),
174
175            // Cycle buffers
176            KeyCode::Char('b') => {
177                self.active_buffer = self.active_buffer.next();
178                self.draw_memory.current_buf_startline = 0;
179            }
180
181            // Go to top of file
182            KeyCode::Char('g') => {
183                self.draw_memory.inner_call_index = 0;
184                self.current_step = 0;
185            }
186
187            // Go to bottom of file
188            KeyCode::Char('G') => {
189                self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
190                self.current_step = self.n_steps() - 1;
191            }
192
193            // Go to previous call
194            KeyCode::Char('c') => {
195                self.draw_memory.inner_call_index =
196                    self.draw_memory.inner_call_index.saturating_sub(1);
197                self.current_step = self.n_steps() - 1;
198            }
199
200            // Go to next call
201            KeyCode::Char('C')
202                if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 =>
203            {
204                self.draw_memory.inner_call_index += 1;
205                self.current_step = 0;
206            }
207
208            // Step forward
209            KeyCode::Char('s') => self.repeat(|this| {
210                let remaining_steps = &this.debug_steps()[this.current_step..];
211                if let Some((i, _)) =
212                    remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
213                        let prev = &remaining_steps[*i - 1];
214                        is_jump(step, prev)
215                    })
216                {
217                    this.current_step += i
218                }
219            }),
220
221            // Step backwards
222            KeyCode::Char('a') => self.repeat(|this| {
223                let ops = &this.debug_steps()[..this.current_step];
224                this.current_step = ops
225                    .iter()
226                    .enumerate()
227                    .skip(1)
228                    .rev()
229                    .find(|&(i, op)| {
230                        let prev = &ops[i - 1];
231                        is_jump(op, prev)
232                    })
233                    .map(|(i, _)| i)
234                    .unwrap_or_default();
235            }),
236
237            // Toggle stack labels
238            KeyCode::Char('t') => self.stack_labels = !self.stack_labels,
239
240            // Toggle memory UTF-8 decoding
241            KeyCode::Char('m') => self.buf_utf = !self.buf_utf,
242
243            // Toggle help notice
244            KeyCode::Char('h') => self.show_shortcuts = !self.show_shortcuts,
245
246            // Numbers for repeating commands or breakpoints
247            KeyCode::Char(
248                other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
249            ) => {
250                // Early return to not clear the buffer.
251                self.key_buffer.push(other);
252                return ControlFlow::Continue(());
253            }
254
255            // Unknown/unhandled key code
256            _ => {}
257        };
258
259        self.key_buffer.clear();
260        ControlFlow::Continue(())
261    }
262
263    fn handle_breakpoint(&mut self, c: char) {
264        // Find the location of the called breakpoint in the whole debug arena (at this address with
265        // this pc)
266        if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) {
267            for (i, node) in self.debug_arena().iter().enumerate() {
268                if node.address == *caller
269                    && let Some(step) = node.steps.iter().position(|step| step.pc == *pc)
270                {
271                    self.draw_memory.inner_call_index = i;
272                    self.current_step = step;
273                    break;
274                }
275            }
276        }
277        self.key_buffer.clear();
278    }
279
280    fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
281        match event.kind {
282            MouseEventKind::ScrollUp => self.step_back(),
283            MouseEventKind::ScrollDown => self.step(),
284            _ => {}
285        }
286
287        ControlFlow::Continue(())
288    }
289
290    fn step_back(&mut self) {
291        if self.current_step > 0 {
292            self.current_step -= 1;
293        } else if self.draw_memory.inner_call_index > 0 {
294            self.draw_memory.inner_call_index -= 1;
295            self.current_step = self.n_steps() - 1;
296        }
297    }
298
299    fn step(&mut self) {
300        if self.current_step < self.n_steps() - 1 {
301            self.current_step += 1;
302        } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
303            self.draw_memory.inner_call_index += 1;
304            self.current_step = 0;
305        }
306    }
307
308    /// Calls a closure `f` the number of times specified in the key buffer, and at least once.
309    fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
310        for _ in 0..buffer_as_number(&self.key_buffer) {
311            f(self);
312        }
313    }
314
315    fn n_steps(&self) -> usize {
316        self.debug_steps().len()
317    }
318}
319
320impl TuiApp for TUIContext<'_> {
321    type Exit = ExitReason;
322
323    fn draw(&mut self, frame: &mut Frame<'_>) {
324        self.draw_layout(frame);
325    }
326
327    fn handle_event(&mut self, event: Event) -> ControlFlow<Self::Exit> {
328        TUIContext::handle_event(self, event)
329    }
330}
331
332/// Grab number from buffer. Used for something like '10k' to move up 10 operations
333fn buffer_as_number(s: &str) -> usize {
334    const MIN: usize = 1;
335    const MAX: usize = 100_000;
336    s.parse().unwrap_or(MIN).clamp(MIN, MAX)
337}
338
339fn pretty_opcode(step: &CallTraceStep) -> String {
340    if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
341        format!("{}(0x{})", step.op, hex::encode(immediate))
342    } else {
343        step.op.to_string()
344    }
345}
346
347fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
348    if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
349        return false;
350    }
351
352    let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
353
354    step.pc != prev.pc + 1 + immediate_len
355}