Skip to content

Commit

Permalink
Implement option render links
Browse files Browse the repository at this point in the history
  • Loading branch information
nilehmann committed Jul 8, 2024
1 parent ce9ec6e commit 613b2d1
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 94 deletions.
11 changes: 10 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,20 @@ pub struct Links {
pub url: String,
}

impl Links {
pub fn render(&self, file: &str, line: usize, col: usize) -> String {
self.url
.replace("${LINE}", &format!("{line}"))
.replace("${COLUMN}", &format!("{col}"))
.replace("${FILE_PATH}", file)
}
}

impl Default for Links {
fn default() -> Self {
Self {
enabled: false,
url: "file://${FILE_PATH}".to_string(),
url: r"file://${FILE_PATH}".to_string(),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn main() -> anyhow::Result<()> {
}

for backtrace in parser.into_backtraces() {
backtrace.render(&mut Filters::new(&config))?;
backtrace.render(&config, &mut Filters::new(&config));
}

Ok(())
Expand Down
255 changes: 163 additions & 92 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,163 @@
use std::{
fmt,
fs::File,
io::{self, BufRead},
path::Path,
};

use crate::{Backtrace, Frame, FrameFilter, PanicInfo, SourceInfo};
use anstyle::{AnsiColor, Color, Reset, Style};

const GREEN: anstyle::Style =
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green)));
const CYAN: anstyle::Style =
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Cyan)));
const RED: anstyle::Style =
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red)));
const BOLD: anstyle::Style = anstyle::Style::new().bold();
const RESET: anstyle::Reset = anstyle::Reset;
use crate::{config::Config, Backtrace, Frame, FrameFilter, PanicInfo, SourceInfo};

const GREEN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
const CYAN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
const RED: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
const BOLD: Style = Style::new().bold();
const RESET: Reset = Reset;

impl Backtrace {
pub fn render(&self, filter: &mut impl FrameFilter) -> io::Result<()> {
if self.frames.is_empty() {
return Ok(());
pub fn render(&self, config: &Config, filter: &mut impl FrameFilter) {
let frameno_width = self.compute_frameno_width();
let lineno_width = self.compute_lineno_width();
let total_width = self.compute_width(frameno_width);
let cx = RenderCtxt {
config,
frameno_width,
lineno_width,
total_width,
};
cx.render_backtrace(self, filter)
}
}

struct RenderCtxt<'a> {
config: &'a Config,
frameno_width: usize,
lineno_width: usize,
total_width: usize,
}

impl<'a> RenderCtxt<'a> {
fn render_backtrace(&self, backtrace: &Backtrace, filter: &mut impl FrameFilter) {
if backtrace.frames.is_empty() {
return;
}
let framnow = self.compute_frameno_width();
let linenow = self.compute_lineno_width();
let width = self.compute_width(framnow);
anstream::eprintln!("\n{:━^width$}", " BACKTRACE ");
anstream::eprintln!("\n{:━^width$}", " BACKTRACE ", width = self.total_width);

let mut hidden = 0;
for frame in self.frames.iter().rev() {
for frame in backtrace.frames.iter().rev() {
if filter.should_hide(frame) {
hidden += 1;
} else {
print_hidden_frames_message(hidden, width)?;
frame.render(framnow, linenow)?;
self.print_hidden_frames_message(hidden);
self.render_frame(frame);
hidden = 0;
}
}
print_hidden_frames_message(hidden, width)?;
self.print_hidden_frames_message(hidden);

if let Some(panic_info) = &self.panic_info {
panic_info.render()?;
if let Some(panic_info) = &backtrace.panic_info {
self.render_panic_info(panic_info);
}

eprintln!();
}

fn print_hidden_frames_message(&self, hidden: u32) {
let msg = match hidden {
0 => return,
1 => format!(" ({hidden} frame hidden) "),
_ => format!(" ({hidden} frames hidden) "),
};
anstream::eprintln!("{CYAN}{msg:┄^width$}{RESET}", width = self.total_width);
}

fn render_frame(&self, frame: &Frame) {
anstream::eprintln!(
"{:>width$}: {GREEN}{}{RESET}",
frame.frameno,
frame.function,
width = self.frameno_width
);

if let Some(source_info) = &frame.source_info {
self.render_source_info(source_info);
let _ = self.render_code_snippet(source_info);
}
}

fn render_source_info(&self, source_info: &SourceInfo) {
let text = format!(
"{}:{}:{}",
source_info.file, source_info.lineno, source_info.colno
);
if self.config.links.enabled {
if let Some(encoded) = encode_file_path_for_url(&source_info.file) {
let url = self
.config
.links
.render(&encoded, source_info.lineno, source_info.colno);
anstream::eprintln!("{} at {}", self.frameno_padding(), Link::new(text, url));
return;
}
}
anstream::eprintln!("{} at {text}", self.frameno_padding())
}

fn render_code_snippet(&self, source_info: &SourceInfo) -> io::Result<()> {
let path = Path::new(&source_info.file);
if path.exists() {
let file = File::open(path)?;
let reader = io::BufReader::new(file);
for (i, line) in viewport(reader, source_info)? {
if i == source_info.lineno {
anstream::eprint!("{BOLD}");
}
anstream::eprintln!(
"{} {i:>width$} | {line}",
self.frameno_padding(),
width = self.lineno_width
);
if i == source_info.lineno {
anstream::eprint!("{RESET}");
}
}
}
Ok(())
}

fn frameno_padding(&self) -> Padding {
Padding(self.frameno_width)
}

fn render_panic_info(&self, panic_info: &PanicInfo) {
anstream::eprint!("{RED}");
anstream::eprintln!(
"thread '{}' panickd at {}",
panic_info.thread,
panic_info.at
);
for line in &panic_info.message {
anstream::eprintln!("{line}");
}
anstream::eprint!("{RESET}");
}
}

fn viewport(
reader: io::BufReader<File>,
source_info: &SourceInfo,
) -> io::Result<Vec<(usize, String)>> {
reader
.lines()
.enumerate()
.skip(source_info.lineno.saturating_sub(2))
.take(5)
.map(|(i, line)| Ok((i + 1, line?)))
.collect()
}

impl Backtrace {
fn compute_lineno_width(&self) -> usize {
// This is assuming we have 2 more lines in the file, if we don't, in the worst case we will
// print an unnecesary extra space for each line number.
Expand Down Expand Up @@ -73,21 +186,6 @@ impl Backtrace {
}

impl Frame {
fn render(&self, framenow: usize, linenow: usize) -> io::Result<()> {
anstream::eprintln!(
"{:>framenow$}: {GREEN}{}{RESET}",
self.frameno,
self.function
);

if let Some(source_info) = &self.source_info {
let padding = Padding(framenow);
anstream::eprintln!("{padding} at {source_info}");
source_info.render_code(padding, linenow)?;
}
Ok(())
}

fn width(&self, frameno_width: usize) -> usize {
usize::max(
frameno_width + 2 + self.function.len(),
Expand All @@ -99,63 +197,10 @@ impl Frame {
}
}

fn print_hidden_frames_message(hidden: u32, width: usize) -> io::Result<()> {
let msg = match hidden {
0 => return Ok(()),
1 => format!(" ({hidden} frame hidden) "),
_ => format!(" ({hidden} frames hidden) "),
};
anstream::eprintln!("{CYAN}{msg:┄^width$}{RESET}");
Ok(())
}

impl SourceInfo {
fn render_code(&self, padding: Padding, linenow: usize) -> io::Result<()> {
let path = Path::new(&self.file);
if path.exists() {
let lineno = self.lineno - 1;
let file = File::open(path)?;
let reader = io::BufReader::new(file);
let viewport: Vec<_> = reader
.lines()
.enumerate()
.skip(lineno.saturating_sub(2))
.take(5)
.collect();
for (i, line) in viewport {
if i == lineno {
anstream::eprint!("{BOLD}");
}
anstream::eprintln!("{padding} {:>linenow$} | {}", i + 1, line?);
if i == lineno {
anstream::eprint!("{RESET}");
}
}
}
Ok(())
}

/// Width without considering the source code snippet
fn width(&self, framenow: usize) -> usize {
framenow + self.file.len() + (self.lineno.ilog10() + self.colno.ilog10()) as usize + 9
}
}

impl PanicInfo {
fn render(&self) -> io::Result<()> {
anstream::eprint!("{RED}");
anstream::eprintln!("thread '{}' panickd at {}", self.thread, self.at);
for line in &self.message {
anstream::eprintln!("{line}");
}
anstream::eprint!("{RESET}");
Ok(())
}
}

impl std::fmt::Display for SourceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file, self.lineno, self.colno)
fn width(&self, frameno_width: usize) -> usize {
frameno_width + self.file.len() + (self.lineno.ilog10() + self.colno.ilog10()) as usize + 9
}
}

Expand All @@ -169,3 +214,29 @@ impl std::fmt::Display for Padding {
Ok(())
}
}

struct Link {
text: String,
url: String,
}

impl Link {
fn new(text: String, url: String) -> Self {
Self { text, url }
}
}

impl fmt::Display for Link {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"\u{1b}]8;;{}\u{1b}\\{}\u{1b}]8;;\u{1b}\\",
self.url, self.text
)
}
}

fn encode_file_path_for_url(path: &str) -> Option<String> {
let path = Path::new(path).canonicalize().ok()?;
Some(format!("{}", path.display()))
}

0 comments on commit 613b2d1

Please sign in to comment.