1use std::fmt::Write as _;
9
10use crate::response::{
11 BreakpointEvent, Level, Output, StateInfo, Stop, StopReason, ValueStatus, VariableInfo,
12};
13
14#[derive(Debug, Clone, Copy, Default)]
16pub struct Renderer {
17 pub color: bool,
19}
20
21impl Renderer {
22 #[must_use]
23 pub const fn new(color: bool) -> Self {
24 Self { color }
25 }
26
27 #[must_use]
30 pub fn render(&self, output: &Output) -> String {
31 let mut text = String::new();
32 for line in self.lines(output) {
33 text.push_str(&line);
34 text.push('\n');
35 }
36 text
37 }
38
39 #[must_use]
41 pub fn lines(&self, output: &Output) -> Vec<String> {
42 match output {
43 Output::Loaded { steps, contracts } => {
44 let mut lines = vec![format!("Loaded trace with {} steps.", self.number(steps))];
45 if contracts.is_empty() {
46 lines.push(self.dim(
47 "No sources matched the executed contracts; stepping is by instruction.",
48 ));
49 } else {
50 let listed = contracts
51 .iter()
52 .map(|contract| match &contract.address {
53 Some(address) => {
54 format!("{} ({})", self.name(&contract.name), self.dim(address))
55 }
56 None => self.name(&contract.name),
57 })
58 .collect::<Vec<_>>();
59 lines.push(format!(
60 "{} {}",
61 self.info("Sources loaded for:"),
62 listed.join(", ")
63 ));
64 }
65 lines
66 }
67 Output::Stop(stop) => self.stop_lines(stop),
68 Output::Message { level, text, note } => {
69 let mut lines = vec![match level {
70 Level::Info => self.info_message(text),
71 Level::Warning => self.warning(text),
72 Level::Note => format!("{} {text}", self.warning("Note:")),
73 }];
74 if let Some(note) = note {
75 lines.push(format!("{} {note}", self.dim("note:")));
76 }
77 lines
78 }
79 Output::Breakpoint { event, id, label } => vec![match event {
80 BreakpointEvent::Set => format!(
81 "{} {label}",
82 self.success(format!("Breakpoint #{id} set at"))
83 ),
84 BreakpointEvent::Cleared => format!(
85 "{} {label}",
86 self.info(format!("Breakpoint #{id} cleared at"))
87 ),
88 }],
89 Output::Breakpoints { breakpoints } => {
90 if breakpoints.is_empty() {
91 vec![self.dim("No breakpoints set.")]
92 } else {
93 breakpoints
94 .iter()
95 .map(|breakpoint| {
96 format!("#{} {}", self.number(breakpoint.id), breakpoint.label)
97 })
98 .collect()
99 }
100 }
101 Output::Backtrace {
102 warning,
103 note,
104 frames,
105 } => {
106 let mut lines = Vec::new();
107 if let Some(warning) = warning {
108 lines.push(self.warning(format!("warning: {warning}")));
109 }
110 if let Some(note) = note {
111 lines.push(format!("{} {note}", self.dim("note:")));
112 }
113 for frame in frames {
114 let mut line = format!("#{:<2} {}", frame.index, self.name(&frame.name));
115 if !frame.arguments.is_empty() {
116 let arguments = frame
117 .arguments
118 .iter()
119 .map(|argument| format!("{} = {}", argument.name, argument.value))
120 .collect::<Vec<_>>()
121 .join(", ");
122 let _ = write!(line, "({arguments})");
123 }
124 if let Some(location) = &frame.location {
125 let _ = write!(line, " at {location}");
126 }
127 if let Some(address) = &frame.address {
128 let _ = write!(line, " ({})", self.address(address));
129 }
130 line.push_str(&self.dim(format!(" step {}, PC {}", frame.step, frame.pc)));
131 lines.push(line);
132 }
133 lines
134 }
135 Output::Listing {
136 path,
137 current_line,
138 lines,
139 } => {
140 let mut out = vec![format!("{}:{}", self.info(path), self.number(current_line))];
141 for line in lines {
142 if line.current {
143 out.push(format!(
144 "{} {}",
145 self.bold(format!("=> {:>5} |", line.line)),
146 self.bold(&line.text)
147 ));
148 } else {
149 out.push(format!(
150 "{} {}",
151 self.dim(format!(" {:>5} |", line.line)),
152 line.text
153 ));
154 }
155 }
156 out
157 }
158 Output::Stack { words, shown } => {
159 if words.is_empty() {
160 return vec![self.dim("Stack: empty")];
161 }
162 let mut lines = vec![format!(
163 "{} {} words, top first",
164 self.info("Stack:"),
165 self.number(words.len())
166 )];
167 for (index, word) in words.iter().enumerate().rev().take(*shown) {
168 lines.push(format!(" [{index}] {}", self.number(word)));
169 }
170 if *shown < words.len() {
171 lines.push(self.dim(format!(" ... {} more below", words.len() - shown)));
172 }
173 lines
174 }
175 Output::Memory(memory) => {
176 let range = if memory.start > 0 || memory.end < memory.total {
177 format!(", showing bytes {}..{}", memory.start, memory.end)
178 } else {
179 String::new()
180 };
181 let mut lines = vec![format!(
182 "{} {} bytes{range}",
183 self.info("Memory:"),
184 self.number(memory.total)
185 )];
186 for word in &memory.words {
187 lines.push(format!(
188 "{} {}",
189 self.dim(format!("0x{:04x}:", word.offset)),
190 word.hex
191 ));
192 }
193 lines
194 }
195 Output::Storage { address, slots } => {
196 if slots.is_empty() {
197 return vec![self.dim("Storage: no slots read or written yet.")];
198 }
199 let mut lines = vec![match address {
200 Some(address) => {
201 format!(
202 "{} {}",
203 self.info("Storage:"),
204 self.dim(format!("of {address}"))
205 )
206 }
207 None => self.info("Storage:"),
208 }];
209 for slot in slots {
210 let was = slot
211 .was
212 .as_ref()
213 .map(|was| self.dim(format!(" (was {was})")))
214 .unwrap_or_default();
215 lines.push(format!(" {} = {}{was}", slot.slot, slot.value));
216 }
217 lines
218 }
219 Output::Calldata { bytes, data } => vec![
220 format!("{} {} bytes", self.info("Calldata:"), self.number(bytes)),
221 data.clone(),
222 ],
223 Output::Variables {
224 warning,
225 pc,
226 locals,
227 unavailable,
228 state,
229 } => {
230 let mut lines = Vec::new();
231 if let Some(warning) = warning {
232 lines.push(self.warning(format!("warning: {warning}")));
233 }
234 if locals.is_empty() {
235 lines.push(match unavailable {
238 Some(reason) => format!(
239 "{} locals are unavailable here: {reason}",
240 self.dim("Variables:")
241 ),
242 None => format!(
243 "{} no variables in scope at PC {}",
244 self.dim("Variables:"),
245 self.number(pc)
246 ),
247 });
248 }
249 for variable in locals {
250 lines.push(self.variable(variable));
251 }
252 match state {
253 StateInfo::Variables { variables } => {
254 lines.push(self.dim("State:"));
255 for variable in variables {
256 lines.push(self.variable(variable));
257 }
258 }
259 StateInfo::None => {
260 lines.push(self.dim("State: this contract declares no state variables"));
261 }
262 StateInfo::NoLayout => lines.push(self.dim(
263 "State: no storage layout loaded; compile with `--storage-layout` to \
264 read state variables",
265 )),
266 StateInfo::NoStorage => {
267 lines.push(self.dim("State: no storage was recorded for this step"));
268 }
269 }
270 lines
271 }
272 Output::Variable { warning, variable } => {
273 let mut lines = Vec::new();
274 if let Some(warning) = warning {
275 lines.push(self.warning(format!("warning: {warning}")));
276 }
277 lines.push(self.variable(variable));
278 lines
279 }
280 Output::Resources { contracts, json } => {
281 if *json {
282 let document = serde_json::json!({ "contracts": contracts });
283 return serde_json::to_string_pretty(&document)
284 .unwrap_or_default()
285 .lines()
286 .map(str::to_owned)
287 .collect();
288 }
289 if contracts.is_empty() {
290 return vec![self.warning("No contract sources are loaded.")];
291 }
292 let mut lines = Vec::new();
293 for contract in contracts {
294 lines.push(format!(
295 "{} {} {}",
296 self.name(&contract.name),
297 self.dim(contract.address.as_deref().unwrap_or("<any address>")),
298 self.dim(format!("[{}]", contract.environment))
299 ));
300 for path in &contract.sources {
301 lines.push(format!(" {path}"));
302 }
303 }
304 lines
305 }
306 Output::Help { lines } => lines.clone(),
307 Output::Tui | Output::Quit => Vec::new(),
308 }
309 }
310
311 fn stop_lines(&self, stop: &Stop) -> Vec<String> {
314 let mut lines = Vec::new();
315 match &stop.reason {
316 StopReason::Initial | StopReason::Moved => {}
317 StopReason::Breakpoint { id, label } => lines.push(format!(
318 "{} {}, {label}",
319 self.success(format!("Breakpoint #{id} hit at step")),
320 self.number(stop.step)
321 )),
322 StopReason::End => lines.push(format!(
323 "{} {}",
324 self.info("End of trace at step"),
325 self.number(stop.step)
326 )),
327 StopReason::Start => lines.push(format!(
328 "{} {}",
329 self.info("Start of trace at step"),
330 self.number(stop.step)
331 )),
332 }
333 let status = format!(
334 "step {}/{}, pc {}, {}, gas {}",
335 self.number(stop.step),
336 self.number(stop.last_step),
337 self.number(stop.pc),
338 self.opcode(&stop.op),
339 self.number(stop.gas)
340 );
341 match &stop.location {
342 Some(location) => {
343 let function = location
344 .function
345 .as_deref()
346 .map(|name| format!(" in {}", self.name(name)))
347 .unwrap_or_default();
348 let generated = if location.generated {
349 self.dim(" (compiler-generated code for this line)")
350 } else {
351 String::new()
352 };
353 lines.push(format!(
354 "{}:{}{function}{generated} {}",
355 self.info(&location.path),
356 self.number(location.line),
357 self.dim(format!("({status})"))
358 ));
359 if stop.stack.is_none() {
360 if let Some(text) = &location.text {
361 lines.push(format!(
362 "{} {text}",
363 self.dim(format!("{:>5} |", location.line))
364 ));
365 }
366 }
367 }
368 None if stop.has_source => {
369 let address = stop
370 .address
371 .as_deref()
372 .map(|address| format!(" in {}", self.address(address)))
373 .unwrap_or_default();
374 lines.push(format!(
375 "{} {}",
376 self.dim(format!("no source for this step{address}")),
377 self.dim(format!("({status})"))
378 ));
379 }
380 None => lines.push(status),
381 }
382 if let Some(stack) = &stop.stack {
383 lines.push(format!(
384 "{} {}",
385 self.info("Stack:"),
386 self.stack_summary(stack)
387 ));
388 }
389 if let Some(note) = &stop.note {
390 lines.push(format!("{} {note}", self.warning("Note:")));
391 }
392 lines
393 }
394
395 fn stack_summary(&self, top_first: &[String]) -> String {
397 if top_first.is_empty() {
398 return self.dim("empty");
399 }
400 let total = top_first.len();
401 let mut parts = top_first
402 .iter()
403 .take(4)
404 .enumerate()
405 .map(|(offset, word)| {
406 format!(
407 "{} {}",
408 self.dim(format!("[{}]", total - 1 - offset)),
409 self.number(shorten_hex(word))
410 )
411 })
412 .collect::<Vec<_>>();
413 if total > 4 {
414 parts.push(self.dim(format!("... +{} more", total - 4)));
415 }
416 parts.join(" ")
417 }
418
419 fn variable(&self, variable: &VariableInfo) -> String {
420 let value = match variable.status {
421 ValueStatus::Unavailable => self.warning(&variable.value),
422 _ => self.success(&variable.value),
423 };
424 let place = variable
425 .place
426 .as_ref()
427 .map(|place| self.dim(format!(" [{place}]")))
428 .unwrap_or_default();
429 format!(
430 "{} {} = {value}{place}",
431 self.info(&variable.ty),
432 self.bold(&variable.name)
433 )
434 }
435
436 fn info_message(&self, text: &str) -> String {
437 match text.split_once(": ") {
439 Some((label, rest)) if !label.contains(' ') || label.starts_with("Mode") => {
440 format!("{} {rest}", self.info(format!("{label}:")))
441 }
442 _ => self.info(text),
443 }
444 }
445
446 fn paint(&self, text: impl std::fmt::Display, code: &str) -> String {
447 if self.color {
448 format!("\x1b[{code}m{text}\x1b[0m")
449 } else {
450 text.to_string()
451 }
452 }
453
454 fn bold(&self, text: impl std::fmt::Display) -> String {
455 self.paint(text, "1")
456 }
457
458 fn dim(&self, text: impl std::fmt::Display) -> String {
459 self.paint(text, "2")
460 }
461
462 fn info(&self, text: impl std::fmt::Display) -> String {
463 self.paint(text, "96")
464 }
465
466 fn success(&self, text: impl std::fmt::Display) -> String {
467 self.paint(text, "92")
468 }
469
470 fn warning(&self, text: impl std::fmt::Display) -> String {
471 self.paint(text, "93")
472 }
473
474 fn number(&self, text: impl std::fmt::Display) -> String {
475 self.paint(text, "93")
476 }
477
478 fn opcode(&self, text: impl std::fmt::Display) -> String {
479 self.paint(text, "94")
480 }
481
482 fn name(&self, text: impl std::fmt::Display) -> String {
483 self.paint(text, "95")
484 }
485
486 fn address(&self, text: impl std::fmt::Display) -> String {
487 self.paint(text, "95")
488 }
489}
490
491#[must_use]
493pub fn shorten_hex(value: &str) -> String {
494 let Some(digits) = value.strip_prefix("0x") else {
495 return value.to_owned();
496 };
497 if value.len() <= 10 {
498 return value.to_owned();
499 }
500 let head = digits.chars().take(4).collect::<String>();
501 format!("0x{head}...")
502}
503
504#[cfg(test)]
505mod tests {
506 use super::{shorten_hex, Renderer};
507 use crate::response::{Level, Output, Stop, StopLocation, StopReason};
508
509 #[test]
510 fn a_stop_is_one_location_line_and_the_source_line() {
511 let renderer = Renderer::new(false);
512 let stop = Stop {
513 reason: StopReason::Breakpoint {
514 id: 1,
515 label: "Counter.sol:7".to_owned(),
516 },
517 step: 12,
518 last_step: 99,
519 pc: 34,
520 op: "PUSH1".to_owned(),
521 gas: 5000,
522 location: Some(StopLocation {
523 path: "Counter.sol".to_owned(),
524 line: 7,
525 function: Some("increment".to_owned()),
526 generated: false,
527 text: Some(" count += 1;".to_owned()),
528 }),
529 address: None,
530 has_source: true,
531 stack: None,
532 note: Some("something".to_owned()),
533 };
534 assert_eq!(
535 renderer.render(&Output::Stop(stop)),
536 "Breakpoint #1 hit at step 12, Counter.sol:7\n\
537 Counter.sol:7 in increment (step 12/99, pc 34, PUSH1, gas 5000)\n \
538 7 | count += 1;\n\
539 Note: something\n"
540 );
541 let bare = Stop {
542 reason: StopReason::Moved,
543 step: 0,
544 last_step: 3,
545 pc: 0,
546 op: "PUSH1".to_owned(),
547 gas: 1,
548 location: None,
549 address: Some("0xabc".to_owned()),
550 has_source: false,
551 stack: Some(vec!["0x2".to_owned(), "0x1".to_owned()]),
552 note: None,
553 };
554 assert_eq!(
555 renderer.render(&Output::Stop(bare)),
556 "step 0/3, pc 0, PUSH1, gas 1\nStack: [1] 0x2 [0] 0x1\n"
557 );
558 }
559
560 #[test]
561 fn messages_and_hex_are_plain_without_color() {
562 let renderer = Renderer::new(false);
563 assert_eq!(
564 renderer.render(&Output::Message {
565 level: Level::Warning,
566 text: "Unknown command: x".to_owned(),
567 note: Some("try help".to_owned()),
568 }),
569 "Unknown command: x\nnote: try help\n"
570 );
571 assert_eq!(shorten_hex("0x1234"), "0x1234");
572 assert_eq!(shorten_hex("0x123456789abcdef0"), "0x1234...");
573 assert_eq!(shorten_hex("plain"), "plain");
574 assert!(Renderer::new(true)
575 .render(&Output::Help {
576 lines: vec!["x".to_owned()]
577 })
578 .starts_with('x'));
579 }
580}