foundry_debugger/tui/
context.rs

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