use unicode_segmentation::UnicodeSegmentation;
use yansi::{Color, Paint};
use crate::{render::print_gap, FileId, Files, Label, Render, Style};
impl<Id: FileId> Render<Id> for Vec<Label<Id>> {
fn render<'a>(
&'a self,
ln_width: Option<usize>,
files: &impl Files<'a, FileId = Id>,
style: &Style,
) -> String {
if self.is_empty() {
return String::new();
}
let Some(ln_width) = ln_width else {
panic!("ln_width must be provided to render labels")
};
let mut groups: Vec<Vec<&Label<Id>>> = Vec::new();
for label in self {
let group = groups
.iter_mut()
.find(|group| group.first().expect("must have one to exist").file == label.file);
match group {
Some(group) => {
group.push(label);
}
None => {
groups.push(vec![label]);
}
}
}
let mut result = String::new();
for (i, group) in groups.iter().enumerate() {
result.push_str(&render_group(group, i == 0, ln_width, files, style));
if i != groups.len() - 1 {
result.push(' ');
result.push_str(&" ".repeat(ln_width));
result.push(' ');
result.push_str(
style
.characters
.vbar_gap
.to_string()
.dim()
.to_string()
.as_str(),
);
}
result.push('\n');
}
result
}
}
pub fn render_group<'a, Id: FileId>(
group: &[&'a Label<Id>],
first: bool,
ln_width: usize,
files: &impl Files<'a, FileId = Id>,
style: &Style,
) -> String {
if group.is_empty() {
return String::new();
}
let file = &group.first().expect("must have one to exist").file;
assert!(
group.iter().all(|label| label.file == *file),
"All labels in a group must have the same file."
);
assert!(
group.iter().all(|label| label.span.start < label.span.end),
"Negative length labels are not supported."
);
let mut result = String::new();
let file_name = files.name(file).expect("file must exist");
result.push_str(
format!(
" {} {}{}{}{}",
" ".repeat(ln_width),
if first {
style.characters.ltop.dim()
} else {
style.characters.lcross.dim()
},
style.characters.lbox.dim(),
file_name,
style.characters.rbox.dim()
)
.as_str(),
);
result.push('\n');
result.push(' ');
result.push_str(&" ".repeat(ln_width));
result.push(' ');
result.push_str(&style.characters.vbar.dim().to_string());
let mut groups: Vec<Vec<(usize, &Label<Id>)>> = Vec::new();
for label in group {
let start = files
.line_from_char(&label.file, label.span.start)
.expect("start must exist");
let end = files
.line_from_char(&label.file, label.span.end)
.expect("end must exist");
assert!((start == end), "Multi-line labels are not supported.");
let group = groups
.iter_mut()
.find(|group| group.first().expect("must have one to exist").0 == start);
match group {
Some(group) => {
group.push((start, label));
}
None => {
groups.push(vec![(start, label)]);
}
}
}
for (x, group) in groups.iter().enumerate() {
result.push('\n');
result.push_str(&render_line(ln_width, group, file, files, style));
if x != groups.len() - 1 {
print_gap(&mut result, ln_width, style);
}
}
result
}
#[allow(clippy::too_many_lines)] fn render_line<'a, Id: FileId>(
width: usize,
labels: &[(usize, &Label<Id>)],
file: &'a Id,
files: &impl Files<'a, FileId = Id>,
style: &Style,
) -> String {
let line_number = labels.first().expect("must have one to exist").0;
let mut result = String::new();
result.push_str(
&format!(
" {:width$} {}",
line_number,
style.characters.vbar,
width = width
)
.dim()
.to_string(),
);
result.push(' ');
let mut messages = Vec::new();
let line_start = files
.char_from_line(file, line_number)
.expect("line must exist");
for (_, label) in labels {
messages.push((
label.span.start - line_start,
label.span.end - line_start,
label,
));
}
messages.sort_by(|a, b| a.0.cmp(&b.0));
let source = files
.content(file)
.expect("content must exist")
.as_ref()
.lines()
.nth(line_number - 1)
.expect("line must exist")
.to_string();
let mut source_out = String::with_capacity(source.len());
let mut color_stack: Vec<(usize, Color)> = Vec::new();
let mut last_end = 0;
let mut spans = messages.clone().into_iter().peekable();
loop {
let (start, end, color) = match spans.next() {
Some((start, end, label)) => (start, end, label.color),
None => (source.len(), source.len(), Color::Primary),
};
if start != last_end {
if color_stack.is_empty() {
source_out.push_str(
&source
.graphemes(true)
.skip(last_end)
.take(start - last_end)
.collect::<String>()
.dim()
.to_string(),
);
} else {
let (stack_end, stack_color) =
*color_stack.first().expect("must have one to exist");
if stack_end < start {
source_out.push_str(
&Paint::new(
&source
.graphemes(true)
.skip(last_end)
.take(stack_end - last_end)
.collect::<String>(),
)
.fg(stack_color)
.to_string(),
);
color_stack.pop();
source_out.push_str(
&source
.graphemes(true)
.skip(stack_end)
.take(start - stack_end)
.collect::<String>()
.dim()
.to_string(),
);
} else {
source_out.push_str(
&Paint::new(
&source
.graphemes(true)
.skip(last_end)
.take(start - last_end)
.collect::<String>(),
)
.fg(stack_color)
.to_string(),
);
}
}
}
let next_start = spans.peek().map_or(source.len(), |(start, _, _)| *start);
if next_start > end {
source_out.push_str(
&Paint::new(
&source
.graphemes(true)
.skip(start)
.take(end - start)
.collect::<String>(),
)
.fg(color)
.to_string(),
);
last_end = end;
} else {
source_out.push_str(
&Paint::new(
&source
.graphemes(true)
.skip(start)
.take(next_start - start)
.collect::<String>(),
)
.fg(color)
.to_string(),
);
last_end = next_start;
color_stack.push((end, color));
}
if last_end == source.len() {
break;
}
}
result.push_str(&source_out);
result.push('\n');
print_gap(&mut result, width, style);
let mut last_end = 0;
for (start, end, label) in &messages {
if label.content.is_none() {
continue;
}
if last_end == 0 {
result.push(' ');
}
let Some(gap) = start.checked_sub(last_end) else {
continue;
};
result.push_str(&" ".repeat(gap));
let len = end - start;
result.push_str(
&match len {
1 => style.characters.underbar.to_string(),
2 => format!(
"{}{}",
style.characters.underline, style.characters.underbar
),
_ => format!(
"{}{}{}",
style.characters.underline,
style.characters.underbar,
style.characters.underline.to_string().repeat(len - 2)
),
}
.paint(label.color)
.to_string(),
);
last_end = *end;
}
result.push('\n');
if messages.iter().all(|(_, _, label)| label.content.is_none()) {
return result;
}
let printable = messages
.iter()
.filter(|(_, _, label)| label.content.is_some())
.collect::<Vec<_>>();
let mut printed = Vec::with_capacity(printable.len());
for i in 0..printable.len() {
if printed.contains(&i) {
continue;
}
let (start, end, label) = printable[i];
if let Some(content) = &label.content {
let mut line = String::new();
print_gap(&mut line, width, style);
line.push_str(&" ".repeat(start + (end - start).min(2)));
line.push_str(
&style
.characters
.lbot
.to_string()
.paint(label.color)
.to_string(),
);
line.push(' ');
line.push_str(content);
for (y, (next_start, _, next_label)) in printable.iter().enumerate().skip(i + 1) {
if printed.contains(&y) {
continue;
}
if let Some(gap) = next_start
.checked_sub(*end)
.and_then(|gap| gap.checked_sub(1))
{
line.push_str(&" ".repeat(gap));
line.push_str(
&style
.characters
.lbot
.to_string()
.paint(next_label.color)
.to_string(),
);
line.push(' ');
line.push_str(
next_label
.content
.as_ref()
.expect("content must exist to be printable"),
);
printed.push(y);
}
}
result.push_str(&line);
result.push('\n');
}
printed.push(i);
}
result
}