foundry_debugger/tui/
context.rs
1use 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 revm::bytecode::opcode::OpCode;
8use revm_inspectors::tracing::types::{CallKind, CallTraceStep};
9use std::ops::ControlFlow;
10
11#[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 pub(crate) key_buffer: String,
24 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 pub(crate) buf_utf: bool,
33 pub(crate) show_shortcuts: bool,
34 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 pub(crate) fn address(&self) -> &Address {
70 &self.debug_call().address
71 }
72
73 pub(crate) fn call_kind(&self) -> CallKind {
75 self.debug_call().kind
76 }
77
78 pub(crate) fn debug_steps(&self) -> &[CallTraceStep] {
80 &self.debug_call().steps
81 }
82
83 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 self.gen_opcode_list_if_necessary();
122 ret
123 }
124
125 fn handle_key_event(&mut self, event: KeyEvent) -> ControlFlow<ExitReason> {
126 if let KeyCode::Char(c) = event.code
128 && c.is_alphabetic()
129 && self.key_buffer.starts_with('\'')
130 {
131 self.handle_breakpoint(c);
132 return ControlFlow::Continue(());
133 }
134
135 let control = event.modifiers.contains(KeyModifiers::CONTROL);
136
137 match event.code {
138 KeyCode::Char('q') => return ControlFlow::Break(ExitReason::CharExit),
140
141 KeyCode::Char('k') | KeyCode::Up if control => self.repeat(|this| {
143 this.draw_memory.current_buf_startline =
144 this.draw_memory.current_buf_startline.saturating_sub(1);
145 }),
146 KeyCode::Char('j') | KeyCode::Down if control => self.repeat(|this| {
148 let max_buf = (this.active_buffer().len() / 32).saturating_sub(1);
149 if this.draw_memory.current_buf_startline < max_buf {
150 this.draw_memory.current_buf_startline += 1;
151 }
152 }),
153
154 KeyCode::Char('k') | KeyCode::Up => self.repeat(Self::step_back),
156 KeyCode::Char('j') | KeyCode::Down => self.repeat(Self::step),
158
159 KeyCode::Char('K') => self.repeat(|this| {
161 this.draw_memory.current_stack_startline =
162 this.draw_memory.current_stack_startline.saturating_sub(1);
163 }),
164 KeyCode::Char('J') => self.repeat(|this| {
166 let max_stack =
167 this.current_step().stack.as_ref().map_or(0, |s| s.len()).saturating_sub(1);
168 if this.draw_memory.current_stack_startline < max_stack {
169 this.draw_memory.current_stack_startline += 1;
170 }
171 }),
172
173 KeyCode::Char('b') => {
175 self.active_buffer = self.active_buffer.next();
176 self.draw_memory.current_buf_startline = 0;
177 }
178
179 KeyCode::Char('g') => {
181 self.draw_memory.inner_call_index = 0;
182 self.current_step = 0;
183 }
184
185 KeyCode::Char('G') => {
187 self.draw_memory.inner_call_index = self.debug_arena().len() - 1;
188 self.current_step = self.n_steps() - 1;
189 }
190
191 KeyCode::Char('c') => {
193 self.draw_memory.inner_call_index =
194 self.draw_memory.inner_call_index.saturating_sub(1);
195 self.current_step = self.n_steps() - 1;
196 }
197
198 KeyCode::Char('C') => {
200 if self.debug_arena().len() > self.draw_memory.inner_call_index + 1 {
201 self.draw_memory.inner_call_index += 1;
202 self.current_step = 0;
203 }
204 }
205
206 KeyCode::Char('s') => self.repeat(|this| {
208 let remaining_steps = &this.debug_steps()[this.current_step..];
209 if let Some((i, _)) =
210 remaining_steps.iter().enumerate().skip(1).find(|(i, step)| {
211 let prev = &remaining_steps[*i - 1];
212 is_jump(step, prev)
213 })
214 {
215 this.current_step += i
216 }
217 }),
218
219 KeyCode::Char('a') => self.repeat(|this| {
221 let ops = &this.debug_steps()[..this.current_step];
222 this.current_step = ops
223 .iter()
224 .enumerate()
225 .skip(1)
226 .rev()
227 .find(|&(i, op)| {
228 let prev = &ops[i - 1];
229 is_jump(op, prev)
230 })
231 .map(|(i, _)| i)
232 .unwrap_or_default();
233 }),
234
235 KeyCode::Char('t') => self.stack_labels = !self.stack_labels,
237
238 KeyCode::Char('m') => self.buf_utf = !self.buf_utf,
240
241 KeyCode::Char('h') => self.show_shortcuts = !self.show_shortcuts,
243
244 KeyCode::Char(
246 other @ ('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\''),
247 ) => {
248 self.key_buffer.push(other);
250 return ControlFlow::Continue(());
251 }
252
253 _ => {}
255 };
256
257 self.key_buffer.clear();
258 ControlFlow::Continue(())
259 }
260
261 fn handle_breakpoint(&mut self, c: char) {
262 if let Some((caller, pc)) = self.debugger_context.breakpoints.get(&c) {
265 for (i, node) in self.debug_arena().iter().enumerate() {
266 if node.address == *caller
267 && let Some(step) = node.steps.iter().position(|step| step.pc == *pc)
268 {
269 self.draw_memory.inner_call_index = i;
270 self.current_step = step;
271 break;
272 }
273 }
274 }
275 self.key_buffer.clear();
276 }
277
278 fn handle_mouse_event(&mut self, event: MouseEvent) -> ControlFlow<ExitReason> {
279 match event.kind {
280 MouseEventKind::ScrollUp => self.step_back(),
281 MouseEventKind::ScrollDown => self.step(),
282 _ => {}
283 }
284
285 ControlFlow::Continue(())
286 }
287
288 fn step_back(&mut self) {
289 if self.current_step > 0 {
290 self.current_step -= 1;
291 } else if self.draw_memory.inner_call_index > 0 {
292 self.draw_memory.inner_call_index -= 1;
293 self.current_step = self.n_steps() - 1;
294 }
295 }
296
297 fn step(&mut self) {
298 if self.current_step < self.n_steps() - 1 {
299 self.current_step += 1;
300 } else if self.draw_memory.inner_call_index < self.debug_arena().len() - 1 {
301 self.draw_memory.inner_call_index += 1;
302 self.current_step = 0;
303 }
304 }
305
306 fn repeat(&mut self, mut f: impl FnMut(&mut Self)) {
308 for _ in 0..buffer_as_number(&self.key_buffer) {
309 f(self);
310 }
311 }
312
313 fn n_steps(&self) -> usize {
314 self.debug_steps().len()
315 }
316}
317
318fn buffer_as_number(s: &str) -> usize {
320 const MIN: usize = 1;
321 const MAX: usize = 100_000;
322 s.parse().unwrap_or(MIN).clamp(MIN, MAX)
323}
324
325fn pretty_opcode(step: &CallTraceStep) -> String {
326 if let Some(immediate) = step.immediate_bytes.as_ref().filter(|b| !b.is_empty()) {
327 format!("{}(0x{})", step.op, hex::encode(immediate))
328 } else {
329 step.op.to_string()
330 }
331}
332
333fn is_jump(step: &CallTraceStep, prev: &CallTraceStep) -> bool {
334 if !matches!(prev.op, OpCode::JUMP | OpCode::JUMPI) {
335 return false;
336 }
337
338 let immediate_len = prev.immediate_bytes.as_ref().map_or(0, |b| b.len());
339
340 step.pc != prev.pc + 1 + immediate_len
341}