use std::fmt;
use annotate_snippets::{
AnnotationKind, Level, Renderer, Snippet as AnnotateSnippet, renderer::DecorStyle,
};
use crate::localizer::Localizer;
use crate::Location;
#[derive(Clone, Copy, Debug)]
pub(crate) struct SnippetSource<'a> {
pub(crate) text: &'a str,
pub(crate) path: &'a str,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum LineMapping {
Identity,
Offset { start_line: usize },
}
#[cold]
#[inline(never)]
pub(crate) fn crop_source_window(
text: &str,
location: &Location,
mapping: LineMapping,
crop_radius: usize,
) -> (String, usize) {
if text.is_empty() || location == &Location::UNKNOWN {
return (String::new(), 1);
}
let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
let absolute_row = location.line as usize;
let relative_row = match mapping {
LineMapping::Identity => absolute_row,
LineMapping::Offset { start_line } => {
if absolute_row < start_line {
return (String::new(), start_line);
}
absolute_row.saturating_sub(start_line).saturating_add(1)
}
};
let starts = line_starts(text);
if starts.is_empty() {
return (String::new(), 1);
}
if relative_row == 0 || relative_row > starts.len() {
let start_line = match mapping {
LineMapping::Identity => 1,
LineMapping::Offset { start_line } => start_line,
};
return (String::new(), start_line);
}
let total_lines = starts.len();
let window_start_row = relative_row.saturating_sub(2).max(1);
let window_end_row = relative_row.saturating_add(2).min(total_lines);
let window_start_row = window_start_row.min(window_end_row);
let window_start = starts[window_start_row - 1];
let window_end = if window_end_row < total_lines {
starts[window_end_row]
} else {
text.len()
};
let window_text = &text[window_start..window_end];
let cropped = if crop_radius == 0 {
window_text.to_owned()
} else {
let needs_storage_crop = window_text.len() > 16 * 1024
|| window_text
.lines()
.any(|l| l.strip_suffix('\r').unwrap_or(l).len() > 4 * 1024);
if !needs_storage_crop {
return (
window_text.to_owned(),
match mapping {
LineMapping::Identity => window_start_row,
LineMapping::Offset { start_line } => start_line
.saturating_add(window_start_row)
.saturating_sub(1),
},
);
}
let error_row = relative_row;
let error_col = location.column as usize;
let left_col = error_col.saturating_sub(crop_radius).max(1);
let right_col = error_col.saturating_add(crop_radius);
let mut out = String::with_capacity(window_text.len().min(4096));
let mut old_pos = 0usize;
let mut row = window_start_row;
while old_pos < window_text.len() {
let next_nl = window_text[old_pos..].find('\n').map(|i| old_pos + i);
let (line_raw, had_nl, consumed) = match next_nl {
Some(nl) => (&window_text[old_pos..nl], true, (nl - old_pos) + 1),
None => (
&window_text[old_pos..],
false,
window_text.len().saturating_sub(old_pos),
),
};
let line = line_raw.strip_suffix('\r').unwrap_or(line_raw);
if row == error_row {
let end_col_excl = right_col.saturating_add(1);
let end_byte = col_to_byte_offset_in_line(line, end_col_excl).unwrap_or(line.len());
let right_clipped = end_byte < line.len();
out.push_str(&line[..end_byte]);
if right_clipped {
out.push('…');
}
} else {
let (rendered, _crop) = crop_line_by_cols(line, left_col, right_col);
out.push_str(&rendered);
}
if had_nl {
out.push('\n');
}
old_pos = old_pos.saturating_add(consumed);
row = row.saturating_add(1);
}
sanitize_terminal_snippet_preserve_len(out)
};
let start_line = match mapping {
LineMapping::Identity => window_start_row,
LineMapping::Offset { start_line } => start_line
.saturating_add(window_start_row)
.saturating_sub(1),
};
(cropped, start_line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crop_source_window_truncates_huge_error_line_on_the_right_only() {
let text = format!("key: {}\n", "A".repeat(10_000));
let loc = Location::new(1, 6);
let (cropped, start_line) = crop_source_window(&text, &loc, LineMapping::Identity, 20);
assert_eq!(start_line, 1);
assert!(cropped.len() < 512, "cropped len was {}", cropped.len());
let line = cropped.lines().next().unwrap();
assert!(line.starts_with("key: "));
assert!(line.ends_with('…'), "expected ellipsis at end, got: {line:?}");
let off = col_to_byte_offset_in_line(line, 6).expect("col 6 offset");
assert_eq!(&line[off..off + 1], "A");
}
#[test]
fn crop_source_window_crops_context_lines_on_both_sides() {
let text = format!(
"{}\nkey: {}\n{}\n",
"X".repeat(10_000),
"A".repeat(10_000),
"Y".repeat(10_000)
);
let loc = Location::new(2, 6);
let (cropped, _start_line) = crop_source_window(&text, &loc, LineMapping::Identity, 8);
let mut lines = cropped.lines();
let before = lines.next().unwrap();
let error = lines.next().unwrap();
let after = lines.next().unwrap();
assert!(before.ends_with('…'), "before line not right-truncated: {before:?}");
assert!(after.ends_with('…'), "after line not right-truncated: {after:?}");
assert!(before.len() < 256, "before line too large: {}", before.len());
assert!(after.len() < 256, "after line too large: {}", after.len());
assert!(error.starts_with("key: "));
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Snippet<'a> {
pub(crate) source: SnippetSource<'a>,
pub(crate) mapping: LineMapping,
pub(crate) crop_radius: usize,
}
impl<'a> Snippet<'a> {
#[inline]
pub(crate) fn new(text: &'a str, path: &'a str, crop_radius: usize) -> Self {
Self {
source: SnippetSource { text, path },
mapping: LineMapping::Identity,
crop_radius,
}
}
#[inline]
pub(crate) fn with_offset(self, start_line: usize) -> Self {
debug_assert!(start_line >= 1);
Self {
mapping: LineMapping::Offset { start_line },
..self
}
}
pub(crate) fn fmt_or_fallback(
self,
f: &mut fmt::Formatter<'_>,
level: Level,
l10n: &dyn Localizer,
msg: &str,
location: &Location,
) -> fmt::Result {
if location == &Location::UNKNOWN {
return write!(f, "{msg}");
}
let absolute_row = location.line as usize;
let col = location.column as usize;
let (relative_row, _window_title_row) = match self.mapping {
LineMapping::Identity => (absolute_row, absolute_row),
LineMapping::Offset { start_line } => {
if absolute_row < start_line {
return fmt_with_location(f, l10n, msg, location);
}
let relative = absolute_row
.saturating_sub(start_line)
.saturating_add(1);
(relative, absolute_row)
}
};
let line_starts = line_starts(self.source.text);
if line_starts.is_empty() {
return fmt_with_location(f, l10n, msg, location);
}
if relative_row == 0 || relative_row > line_starts.len() {
return fmt_with_location(f, l10n, msg, location);
}
let Some(start) =
line_col_to_byte_offset_with_starts(self.source.text, &line_starts, relative_row, col)
else {
return fmt_with_location(f, l10n, msg, location);
};
let end = match self.source.text.as_bytes().get(start) {
Some(b'\n') | Some(b'\r') => start,
_ => next_char_boundary(self.source.text, start).unwrap_or(start),
};
let total_lines = line_starts.len();
let window_start_row = relative_row.saturating_sub(2).max(1);
let window_end_row = relative_row.saturating_add(2).min(total_lines);
let window_start_row = window_start_row.min(window_end_row);
let window_start = line_starts[window_start_row - 1];
let window_end = if window_end_row < total_lines {
line_starts[window_end_row]
} else {
self.source.text.len()
};
let window_text = &self.source.text[window_start..window_end];
let local_start = start.saturating_sub(window_start).min(window_text.len());
let local_end = end.saturating_sub(window_start).min(window_text.len());
let (window_text, local_start, local_end) = crop_window_text(
window_text,
window_start_row,
relative_row,
col,
self.crop_radius,
local_start,
local_end,
);
let window_start_absolute_row = match self.mapping {
LineMapping::Identity => window_start_row,
LineMapping::Offset { start_line } => start_line
.saturating_add(window_start_row)
.saturating_sub(1),
};
let loc_prefix = l10n.snippet_location_prefix(*location);
let report = &[level
.primary_title(format!("{}: {msg}", loc_prefix))
.element(
AnnotateSnippet::source(&window_text)
.line_start(window_start_absolute_row)
.path(self.source.path)
.fold(false)
.annotation(
AnnotationKind::Primary
.span(local_start..local_end)
.label(msg),
),
)];
let renderer = Renderer::plain().decor_style(DecorStyle::Ascii);
write!(f, "{}", renderer.render(report))
}
}
pub(crate) fn fmt_snippet_window_offset_or_fallback(
f: &mut fmt::Formatter<'_>,
l10n: &dyn Localizer,
location: &Location,
text: &str,
start_line: usize,
msg: &str,
crop_radius: usize,
) -> fmt::Result {
fmt_snippet_window_with_mapping_or_fallback(
f,
l10n,
location,
text,
LineMapping::Offset { start_line },
msg,
crop_radius,
)
}
fn fmt_snippet_window_with_mapping_or_fallback(
f: &mut fmt::Formatter<'_>,
_l10n: &dyn Localizer,
location: &Location,
text: &str,
mapping: LineMapping,
msg: &str,
crop_radius: usize,
) -> fmt::Result {
if location == &Location::UNKNOWN {
return Ok(());
}
let absolute_row = location.line as usize;
let col = location.column as usize;
let row = match mapping {
LineMapping::Identity => absolute_row,
LineMapping::Offset { start_line } => {
if absolute_row < start_line {
return Ok(());
}
absolute_row
.saturating_sub(start_line)
.saturating_add(1)
}
};
let line_starts = line_starts(text);
if line_starts.is_empty() {
return Ok(());
}
if row == 0 || row > line_starts.len() {
return Ok(());
}
let Some(start) = line_col_to_byte_offset_with_starts(text, &line_starts, row, col) else {
return Ok(());
};
let end = match text.as_bytes().get(start) {
Some(b'\n') | Some(b'\r') => start,
_ => next_char_boundary(text, start).unwrap_or(start),
};
let total_lines = line_starts.len();
let window_start_row = row.saturating_sub(2).max(1);
let window_end_row = row.saturating_add(2).min(total_lines);
let window_start_row = window_start_row.min(window_end_row);
let window_start = line_starts[window_start_row - 1];
let window_end = if window_end_row < total_lines {
line_starts[window_end_row]
} else {
text.len()
};
let window_text = &text[window_start..window_end];
let local_start = start.saturating_sub(window_start).min(window_text.len());
let local_end = end.saturating_sub(window_start).min(window_text.len());
let (window_text, local_start, _local_end) = crop_window_text(
window_text,
window_start_row,
row,
col,
crop_radius,
local_start,
local_end,
);
let window_start_absolute_row = match mapping {
LineMapping::Identity => window_start_row,
LineMapping::Offset { start_line } => start_line
.saturating_add(window_start_row)
.saturating_sub(1),
};
let max_display_row = match mapping {
LineMapping::Identity => window_end_row,
LineMapping::Offset { start_line } => start_line
.saturating_add(window_end_row)
.saturating_sub(1),
};
let gutter_width = max_display_row.to_string().len();
writeln!(f, " |")?;
let mut cur_row = window_start_row;
for line in window_text.split_inclusive('\n') {
let mut line = line;
if let Some(stripped) = line.strip_suffix('\n') {
line = stripped;
}
if let Some(stripped) = line.strip_suffix('\r') {
line = stripped;
}
let display_row = window_start_absolute_row
.saturating_add(cur_row)
.saturating_sub(window_start_row);
writeln!(f, "{display_row:>gutter_width$} | {line}")?;
if cur_row == row {
let line_byte_start = window_text[..local_start]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let caret_chars = window_text[line_byte_start..local_start].chars().count();
if msg.is_empty() {
writeln!(f, " | {space:>caret_chars$}^", space = "")?;
} else {
writeln!(f, " | {space:>caret_chars$}^ {msg}", space = "", msg = msg)?;
}
}
cur_row += 1;
if cur_row > window_end_row {
break;
}
}
if window_end_row == total_lines && window_text.ends_with('\n') && cur_row <= window_end_row {
let display_row = window_start_absolute_row
.saturating_add(cur_row)
.saturating_sub(window_start_row);
writeln!(f, "{display_row:>gutter_width$} |")?;
if cur_row == row {
let line_byte_start = window_text[..local_start]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let caret_chars = window_text[line_byte_start..local_start].chars().count();
if msg.is_empty() {
writeln!(f, " | {space:>caret_chars$}^", space = "")?;
} else {
writeln!(f, " | {space:>caret_chars$}^ {msg}", space = "", msg = msg)?;
}
}
}
writeln!(f, " |")
}
fn fmt_with_location(
f: &mut fmt::Formatter<'_>,
l10n: &dyn Localizer,
msg: &str,
location: &Location,
) -> fmt::Result {
let out = l10n.attach_location(std::borrow::Cow::Borrowed(msg), *location);
write!(f, "{out}")
}
pub(crate) fn is_terminal_snippet_clean(text: &str) -> bool {
let b = text.as_bytes();
for &x in b {
if (x < 0x20 && x != b'\n' && x != b'\t') || x == 0x7F {
return false;
}
}
let mut i = 0usize;
while i + 1 < b.len() {
if b[i] == 0xC2 && (0x80..=0x9F).contains(&b[i + 1]) {
return false;
}
i += 1;
}
true
}
pub(crate) fn sanitize_terminal_snippet_preserve_len(s: String) -> String {
let mut bytes = s.into_bytes();
for x in &mut bytes {
let b = *x;
if (b < 0x20 && b != b'\n' && b != b'\t') || b == 0x7F {
*x = b' ';
}
}
let mut i = 0usize;
while i + 1 < bytes.len() {
if bytes[i] == 0xC2 && (0x80..=0x9F).contains(&bytes[i + 1]) {
bytes[i + 1] = 0xA0;
i += 2;
continue;
}
i += 1;
}
match String::from_utf8(bytes) {
Ok(out) => out,
Err(e) => String::from_utf8_lossy(&e.into_bytes()).into_owned(),
}
}
fn crop_window_text(
window_text: &str,
window_start_row: usize,
error_row: usize,
error_col: usize,
crop_radius: usize,
local_start: usize,
local_end: usize,
) -> (String, usize, usize) {
if crop_radius == 0
&& !window_text.as_bytes().contains(&b'\r')
&& is_terminal_snippet_clean(window_text)
{
return (window_text.to_owned(), local_start, local_end);
}
let do_crop = crop_radius != 0;
let left_col = error_col.saturating_sub(crop_radius).max(1);
let right_col = error_col.saturating_add(crop_radius);
let mut out = String::with_capacity(window_text.len().min(4096));
let mut old_pos = 0usize;
let mut new_local_start = local_start;
let mut new_local_end = local_end;
let mut rebased = false;
let mut row = window_start_row;
while old_pos < window_text.len() {
let next_nl = window_text[old_pos..].find('\n').map(|i| old_pos + i);
let (line_raw, had_nl, consumed) = match next_nl {
Some(nl) => (&window_text[old_pos..nl], true, (nl - old_pos) + 1),
None => (&window_text[old_pos..], false, window_text.len() - old_pos),
};
let line = line_raw.strip_suffix('\r').unwrap_or(line_raw);
let line_start_old = old_pos;
let line_start_new = out.len();
let (rendered_line, crop) = if do_crop {
crop_line_by_cols(line, left_col, right_col)
} else {
(
line.to_owned(),
LineCrop {
start_byte: 0,
prefix_bytes: 0,
},
)
};
out.push_str(&rendered_line);
if had_nl {
out.push('\n');
}
if row == error_row {
let mut old_in_line_start = local_start.saturating_sub(line_start_old);
let mut old_in_line_end = local_end.saturating_sub(line_start_old);
old_in_line_start = old_in_line_start.min(line.len());
old_in_line_end = old_in_line_end.min(line.len());
old_in_line_start = old_in_line_start.saturating_sub(crop.start_byte);
old_in_line_end = old_in_line_end.saturating_sub(crop.start_byte);
new_local_start = line_start_new + crop.prefix_bytes + old_in_line_start;
new_local_end = line_start_new + crop.prefix_bytes + old_in_line_end;
let max = line_start_new + rendered_line.len();
new_local_start = new_local_start.min(max);
new_local_end = new_local_end.min(max);
if new_local_end < new_local_start {
new_local_end = new_local_start;
}
rebased = true;
}
old_pos += consumed;
row += 1;
if !had_nl {
break;
}
}
if !rebased && window_text.ends_with('\n') && row == error_row {
new_local_start = out.len();
new_local_end = out.len();
}
let max = out.len();
new_local_start = new_local_start.min(max);
new_local_end = new_local_end.min(max);
if new_local_end < new_local_start {
new_local_end = new_local_start;
}
let out = sanitize_terminal_snippet_preserve_len(out);
(out, new_local_start, new_local_end)
}
#[derive(Clone, Copy, Debug)]
struct LineCrop {
start_byte: usize,
prefix_bytes: usize,
}
fn crop_line_by_cols(line: &str, left_col_1: usize, right_col_1: usize) -> (String, LineCrop) {
let line_len_cols = line.chars().count();
if line_len_cols == 0 {
return (
String::new(),
LineCrop {
start_byte: 0,
prefix_bytes: 0,
},
);
}
if left_col_1 >= line_len_cols.saturating_add(1) {
return (
line.to_owned(),
LineCrop {
start_byte: 0,
prefix_bytes: 0,
},
);
}
if left_col_1 <= 1 && right_col_1 >= line_len_cols {
return (
line.to_owned(),
LineCrop {
start_byte: 0,
prefix_bytes: 0,
},
);
}
let start_col = left_col_1.min(line_len_cols + 1);
let end_col_excl = right_col_1.saturating_add(1).min(line_len_cols + 1);
let start_byte = col_to_byte_offset_in_line(line, start_col).unwrap_or(0);
let end_byte = col_to_byte_offset_in_line(line, end_col_excl).unwrap_or(line.len());
let left_clipped = start_col > 1 && start_byte > 0;
let right_clipped = end_col_excl <= line_len_cols && end_byte < line.len();
let mut out = String::new();
if left_clipped {
out.push('…');
}
out.push_str(&line[start_byte..end_byte]);
if right_clipped {
out.push('…');
}
let prefix_bytes = if left_clipped { '…'.len_utf8() } else { 0 };
(
out,
LineCrop {
start_byte,
prefix_bytes,
},
)
}
fn col_to_byte_offset_in_line(line: &str, col_1: usize) -> Option<usize> {
if col_1 == 0 {
return None;
}
let mut col = 1usize;
for (i, _ch) in line.char_indices() {
if col == col_1 {
return Some(i);
}
col += 1;
}
if col == col_1 {
return Some(line.len());
}
None
}
fn line_starts(source: &str) -> Vec<usize> {
if source.is_empty() {
return Vec::new();
}
let mut starts = vec![0usize];
for (i, b) in source.as_bytes().iter().enumerate() {
if *b == b'\n' {
starts.push(i + 1);
}
}
starts
}
fn line_col_to_byte_offset_with_starts(
source: &str,
starts: &[usize],
row_1: usize,
col_1: usize,
) -> Option<usize> {
if row_1 == 0 || col_1 == 0 {
return None;
}
if starts.is_empty() {
return None;
}
let row_idx = row_1 - 1;
if row_idx >= starts.len() {
return None;
}
let line_start = starts[row_idx];
let mut line_end = if row_idx + 1 < starts.len() {
starts[row_idx + 1].saturating_sub(1) } else {
source.len()
};
if line_end > line_start && source.as_bytes().get(line_end - 1) == Some(&b'\r') {
line_end -= 1;
}
let line = &source[line_start..line_end];
col_to_byte_offset_in_line(line, col_1).map(|off| line_start + off)
}
fn next_char_boundary(source: &str, start: usize) -> Option<usize> {
if start >= source.len() {
return None;
}
let s = &source[start..];
let mut it = s.char_indices();
let _ = it.next()?; match it.next() {
Some((i, _)) => Some(start + i),
None => Some(source.len()),
}
}