use super::display::UnicodeWidth as _;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SyntaxErrorLine {
pub(crate) line: String,
pub(crate) line_number: usize,
pub(crate) underline: Option<SyntaxErrorUnderline>,
pub(crate) truncated_start: bool,
pub(crate) truncated_end: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SyntaxErrorUnderline {
pub(crate) start_pos: usize,
pub(crate) len: usize,
pub(crate) message: Option<String>,
}
pub(super) struct ErrorFormatter<'a> {
input: &'a str,
line_data: Vec<LineData>,
}
#[derive(Debug, PartialEq, Eq)]
struct LineData {
start_idx: usize,
one_past_end_idx: usize,
char_data: Vec<CharData>,
}
#[derive(Debug, PartialEq, Eq)]
struct CharData {
byte_idx: usize,
acc_width: usize,
c: char,
}
impl<'a> ErrorFormatter<'a> {
pub(super) fn new(input: &'a str) -> Self {
if input.is_empty() {
return Self {
input,
line_data: vec![LineData::new(0, 0, vec![CharData::new(0, 1, ' ')])],
};
}
let mut acc_len = 0;
let line_data = input
.split_inclusive('\n')
.map(|line| {
let start_idx = acc_len;
let one_past_end_idx = acc_len + line.len();
let mut char_data = line
.char_indices()
.scan(0, |acc_width, (i, c)| {
*acc_width += c.width();
Some(CharData::new(i + start_idx, *acc_width, c))
})
.collect::<Vec<_>>();
if let Some(last) = char_data.last() {
char_data.push(CharData::new(last.byte_idx + 1, last.acc_width + 1, ' '));
} else {
char_data.push(CharData::new(start_idx + 1, 1, ' '));
}
acc_len += line.len();
LineData {
start_idx,
one_past_end_idx,
char_data,
}
})
.collect::<Vec<_>>();
Self { input, line_data }
}
pub(super) fn str(&self) -> &str {
self.input
}
pub(super) fn len(&self) -> usize {
self.input.len()
}
pub(super) fn is_multiline(&self) -> bool {
self.line_data.len() > 1
}
pub(super) fn build_error_lines(
&self,
error_byte_start: usize,
error_byte_end: usize,
min_context_width: usize,
soft_width_limit: usize,
underline_message: String,
) -> Vec<SyntaxErrorLine> {
let start_line_num = self.find_line_containing(error_byte_start);
let end_line_num = self.find_line_containing(error_byte_end);
let start_line = &self.line_data[start_line_num];
let end_line = &self.line_data[end_line_num];
let error_start_char_idx = start_line.find_char_at_idx(error_byte_start);
let error_end_char_idx = end_line.find_char_at_idx(error_byte_end);
let full_pre_context_width = if error_start_char_idx == 0 {
0
} else {
start_line.width_to_char(error_start_char_idx - 1)
};
let full_post_context_width = if error_end_char_idx == end_line.char_data.len() - 1 {
0
} else {
end_line.width_from_char(error_end_char_idx + 1)
};
if start_line_num == end_line_num {
let only_line = start_line; let line_error_width = only_line.width_of_char_span(error_start_char_idx, error_end_char_idx);
let total_width = only_line.total_width();
let (pre_width, post_width) = if total_width <= soft_width_limit {
(full_pre_context_width, full_post_context_width)
} else {
let allowed_total_context_width = soft_width_limit.saturating_sub(line_error_width);
let pre_allocation = allowed_total_context_width / 2;
let post_allocation = allowed_total_context_width - pre_allocation;
let pre_overallocation = pre_allocation.saturating_sub(full_pre_context_width);
let post_overallocation = post_allocation.saturating_sub(full_post_context_width);
let pre_width = (pre_allocation + post_overallocation)
.max(min_context_width)
.min(full_pre_context_width);
let post_width = (post_allocation + pre_overallocation)
.max(min_context_width)
.min(full_post_context_width);
(pre_width, post_width)
};
let pre_start_idx = only_line.find_start_of_pre_context(error_start_char_idx, pre_width);
let underline_offset = if error_start_char_idx == 0 {
0
} else {
only_line.width_of_char_span(pre_start_idx, error_start_char_idx - 1)
};
let post_end_idx = only_line.find_end_of_post_context(error_end_char_idx, post_width);
let display_line = self.slice_line(only_line, pre_start_idx, post_end_idx).to_string();
vec![SyntaxErrorLine {
truncated_start: full_pre_context_width != pre_width,
truncated_end: full_post_context_width != post_width,
line: display_line,
line_number: start_line_num,
underline: if line_error_width == 0 {
None
} else {
Some(SyntaxErrorUnderline {
len: line_error_width,
start_pos: underline_offset,
message: Some(underline_message),
})
},
}]
} else {
let first_line_error_width = start_line.width_from_char(error_start_char_idx);
let last_line_error_width = end_line.width_to_char(error_end_char_idx);
let pre_width = soft_width_limit
.saturating_sub(first_line_error_width)
.max(min_context_width)
.min(full_pre_context_width);
let post_width = soft_width_limit
.saturating_sub(last_line_error_width)
.max(min_context_width)
.min(full_post_context_width);
let pre_start_idx = start_line.find_start_of_pre_context(error_start_char_idx, pre_width);
let post_end_idx = end_line.find_end_of_post_context(error_end_char_idx, post_width);
let mut lines = Vec::with_capacity(end_line_num - start_line_num + 1);
let first_line_display =
self.input[start_line.char_data[pre_start_idx].byte_idx..start_line.one_past_end_idx].to_string();
lines.push(SyntaxErrorLine {
truncated_start: full_pre_context_width != pre_width,
truncated_end: false,
line: first_line_display,
line_number: start_line_num,
underline: Some(SyntaxErrorUnderline {
len: start_line.width_from_char(error_start_char_idx),
start_pos: pre_width,
message: None,
}),
});
for line_num in start_line_num + 1..end_line_num {
let line = &self.line_data[line_num];
let display_line = &self.input[line.start_idx..line.one_past_end_idx];
lines.push(SyntaxErrorLine {
truncated_start: false,
truncated_end: false,
line: display_line.to_string(),
line_number: line_num,
underline: Some(SyntaxErrorUnderline {
len: line.total_width(),
start_pos: 0,
message: None,
}),
});
}
let display_line = self.slice_line(end_line, 0, post_end_idx).to_string();
lines.push(SyntaxErrorLine {
truncated_start: false,
truncated_end: full_post_context_width != post_width,
line: display_line,
line_number: end_line_num,
underline: Some(SyntaxErrorUnderline {
len: end_line.width_to_char(error_end_char_idx),
start_pos: 0,
message: Some(underline_message),
}),
});
lines
}
}
fn find_line_containing(&self, idx: usize) -> usize {
self.line_data
.binary_search_by_key(&idx, |l| l.start_idx)
.unwrap_or_else(|idx| idx - 1)
}
fn slice_line(&self, line: &LineData, start_char_idx: usize, end_char_idx: usize) -> &str {
let start = line.char_data[start_char_idx].byte_idx;
let end = if end_char_idx == line.char_data.len() - 1 {
line.char_data[end_char_idx].byte_idx
} else {
line.char_data[end_char_idx + 1].byte_idx
};
&self.input[start..end]
}
}
impl CharData {
fn new(idx: usize, acc_width: usize, c: char) -> Self {
Self {
byte_idx: idx,
acc_width,
c,
}
}
}
impl LineData {
fn new(start_idx: usize, one_past_end_idx: usize, char_data: Vec<CharData>) -> Self {
Self {
start_idx,
one_past_end_idx,
char_data,
}
}
fn find_char_at_idx(&self, byte_idx: usize) -> usize {
self.char_data
.binary_search_by_key(&byte_idx, |c| c.byte_idx)
.unwrap_or_else(|idx| idx - 1)
}
fn find_start_of_pre_context(&self, error_start_char_idx: usize, pre_width: usize) -> usize {
let width_at_error_start = if error_start_char_idx == 0 {
0
} else {
self.width_to_char(error_start_char_idx - 1)
};
let target_width = width_at_error_start.saturating_sub(pre_width);
self.char_data
.binary_search_by_key(&target_width, |d| d.acc_width - d.c.width())
.unwrap_or_else(|idx| idx)
}
fn find_end_of_post_context(&self, error_end_char_idx: usize, post_width: usize) -> usize {
let width_at_error_end = self.width_to_char(error_end_char_idx);
let target_width = width_at_error_end + post_width;
self.char_data
.binary_search_by_key(&target_width, |c| c.acc_width)
.unwrap_or_else(|idx| idx - 1)
}
fn width_of_char_span(&self, start_char_idx: usize, end_char_idx: usize) -> usize {
self.char_data[end_char_idx].acc_width - self.char_data[start_char_idx].acc_width
+ self.char_data[start_char_idx].c.width()
}
fn width_from_char(&self, start_char_idx: usize) -> usize {
self.total_width() + self.char_data[start_char_idx].c.width() - self.char_data[start_char_idx].acc_width
}
fn width_to_char(&self, end_char_idx: usize) -> usize {
self.char_data[end_char_idx].acc_width
}
fn total_width(&self) -> usize {
self.char_data.last().map_or(0, |c| c.acc_width) - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn empty_input() {
let s = "";
let expected_lines = vec![SyntaxErrorLine {
line: String::new(),
truncated_start: false,
truncated_end: false,
line_number: 0,
underline: Some(SyntaxErrorUnderline {
start_pos: 0,
len: 1,
message: Some("message".to_string()),
}),
}];
let input = ErrorFormatter::new(s);
assert_eq!(input.str(), "");
assert_eq!(input.len(), 0);
assert!(!input.is_multiline());
let lines = input.build_error_lines(0, 0, 30, 80, "message".to_string());
assert_eq!(expected_lines, lines);
}
#[test]
fn simple_ascii_input() {
let s = "abc123...";
let expected_char_data = vec![
CharData::new(0, 1, 'a'),
CharData::new(1, 2, 'b'),
CharData::new(2, 3, 'c'),
CharData::new(3, 4, '1'),
CharData::new(4, 5, '2'),
CharData::new(5, 6, '3'),
CharData::new(6, 7, '.'),
CharData::new(7, 8, '.'),
CharData::new(8, 9, '.'),
CharData::new(9, 10, ' '),
];
let expected_line_data = vec![LineData::new(0, s.len(), expected_char_data)];
let expected_error_data_1 = vec![SyntaxErrorLine {
truncated_start: false,
truncated_end: false,
line: s.to_string(),
underline: Some(SyntaxErrorUnderline {
len: 9,
start_pos: 0,
message: Some("message".to_string()),
}),
line_number: 0,
}];
let expected_error_data_2 = vec![SyntaxErrorLine {
truncated_start: false,
truncated_end: false,
line: s.to_string(),
underline: Some(SyntaxErrorUnderline {
len: 2,
start_pos: 5,
message: Some("message".to_string()),
}),
line_number: 0,
}];
let input = ErrorFormatter::new(s);
assert_eq!(input.str(), s);
assert_eq!(input.len(), s.len());
assert!(!input.is_multiline());
assert_eq!(input.line_data, expected_line_data);
let data = input.build_error_lines(0, 8, 30, 80, "message".to_string());
assert_eq!(expected_error_data_1, data);
let data = input.build_error_lines(5, 6, 30, 80, "message".to_string());
assert_eq!(expected_error_data_2, data);
}
#[test]
fn variable_width_input() {
const WIDTH_TO_TEST: usize = 80;
let base_s = "🦀."; assert_eq!(base_s.len(), 5);
let s = base_s.repeat(100); let mut expected_char_data = vec![];
for i in 0..100 {
expected_char_data.push(CharData::new(5 * i, 3 * i + 2, '🦀'));
expected_char_data.push(CharData::new(5 * i + 4, 3 * i + 3, '.'));
}
expected_char_data.push(CharData::new(500, 301, ' '));
let expected_data = vec![LineData::new(0, s.len(), expected_char_data)];
let input = ErrorFormatter::new(&s);
assert_eq!(input.str(), s);
assert_eq!(input.len(), s.len());
assert!(!input.is_multiline());
assert_eq!(input.line_data, expected_data);
let iter = input.build_error_lines(50, 53, 5, WIDTH_TO_TEST, "message".to_string());
let expected_lines = vec![SyntaxErrorLine {
truncated_start: false,
truncated_end: true,
line_number: 0,
line: s.chars().take(53).collect::<String>(),
underline: Some(SyntaxErrorUnderline {
len: 2,
start_pos: 30,
message: Some("message".to_string()),
}),
}];
assert_eq!(expected_lines, iter);
let iter = input.build_error_lines(445, 448, 5, WIDTH_TO_TEST, "message".to_string());
let expected_lines = vec![SyntaxErrorLine {
truncated_start: true,
truncated_end: false,
line_number: 0,
line: s.chars().skip(147).collect::<String>(),
underline: Some(SyntaxErrorUnderline {
len: 2,
start_pos: 46,
message: Some("message".to_string()),
}),
}];
assert_eq!(expected_lines, iter);
let iter = input.build_error_lines(270, 273, 5, WIDTH_TO_TEST, "message".to_string());
let expected_lines = vec![SyntaxErrorLine {
truncated_start: true,
truncated_end: true,
line_number: 0,
line: s.chars().skip(56).take(27 + 26).collect::<String>(),
underline: Some(SyntaxErrorUnderline {
len: 2,
start_pos: 39,
message: Some("message".to_string()),
}),
}];
assert_eq!(expected_lines, iter);
}
}