use std::convert::TryInto;
use std::fmt;
use std::fmt::Write;
use std::io::Result;
#[cfg(feature = "colors")]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Color {
Red,
Green,
Blue,
Magenta,
Yellow,
Cyan,
}
#[cfg(feature = "colors")]
impl termion::color::Color for Color {
fn write_fg(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Red => termion::color::LightRed.write_fg(f),
Self::Green => termion::color::LightGreen.write_fg(f),
Self::Blue => termion::color::LightBlue.write_fg(f),
Self::Magenta => termion::color::LightMagenta.write_fg(f),
Self::Yellow => termion::color::LightYellow.write_fg(f),
Self::Cyan => termion::color::LightCyan.write_fg(f),
}
}
fn write_bg(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Red => termion::color::LightRed.write_bg(f),
Self::Green => termion::color::LightGreen.write_bg(f),
Self::Blue => termion::color::LightBlue.write_bg(f),
Self::Magenta => termion::color::LightMagenta.write_bg(f),
Self::Yellow => termion::color::LightYellow.write_bg(f),
Self::Cyan => termion::color::LightCyan.write_bg(f),
}
}
}
#[cfg(not(feature = "colors"))]
pub type Color = ();
use crate::Span;
#[derive(Clone, Copy)]
pub enum Style {
Error,
Warning,
Note,
Custom(char, char, Color),
}
impl Style {
#[must_use]
#[cfg(not(feature = "colors"))]
pub const fn new(line: char, marker: char) -> Self { Self::Custom(line, marker, ()) }
#[must_use]
#[cfg(feature = "colors")]
pub const fn new(line: char, marker: char, color: Color) -> Self {
Self::Custom(line, marker, color)
}
#[must_use]
pub fn line(&self) -> char {
match self {
Self::Error | Self::Warning => '^',
Self::Note => '_',
Self::Custom(line, _, _) => *line,
}
}
#[must_use]
pub fn marker(&self) -> char {
match self {
Self::Error | Self::Warning | Self::Note => '^',
Self::Custom(_, marker, _) => *marker,
}
}
#[must_use]
pub fn color(&self) -> Color {
#[cfg(not(feature = "colors"))]
{
()
}
#[cfg(feature = "colors")]
{
match self {
Self::Error => Color::Red,
Self::Warning => Color::Yellow,
Self::Note => Color::Blue,
Self::Custom(_, _, color) => *color,
}
}
}
}
pub struct Highlight {
span: Span,
label: Option<String>,
style: Style,
}
pub struct Formatter {
highlights: Vec<Highlight>,
show_line_numbers: bool,
margin_color: Color,
}
struct MappedHighlight<'a> {
h: &'a Highlight,
column_offset: usize,
label_position: (usize, usize),
}
#[derive(Clone, Copy)]
pub enum Char {
Text(char),
Margin(char, Color),
Label(char, Color),
SpanUnderline(char, Color),
SpanMarker(char, Color),
SpanLine(Color),
SpanColumn(ColumnStyle, Color),
}
#[derive(Clone, Copy)]
pub enum ColumnStyle {
Normal,
Abbreviated,
}
impl Char {
const fn label(c: char, color: Color) -> Self { Self::Label(c, color) }
const fn space() -> Self { Self::Text(' ') }
fn unwrap(self) -> char {
match self {
Self::Text(c)
| Self::Margin(c, _)
| Self::Label(c, _)
| Self::SpanUnderline(c, _)
| Self::SpanMarker(c, _) => c,
Self::SpanLine(_) => '_',
Self::SpanColumn(ColumnStyle::Normal, _) => '|',
Self::SpanColumn(ColumnStyle::Abbreviated, _) => '/',
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn with(&self, c: char) -> Self {
match self {
Self::Text(_) => Self::Text(c),
Self::Margin(_, color) => Self::Margin(c, *color),
Self::Label(_, color) => Self::Label(c, *color),
Self::SpanUnderline(_, color) => Self::SpanUnderline(c, *color),
Self::SpanMarker(_, color) => Self::SpanMarker(c, *color),
Self::SpanLine(color) => Self::SpanLine(*color),
Self::SpanColumn(style, color) => Self::SpanColumn(*style, *color),
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn color(&self) -> Option<Color> {
match self {
Self::Text(_) => None,
Self::Margin(_, color)
| Self::Label(_, color)
| Self::SpanUnderline(_, color)
| Self::SpanMarker(_, color)
| Self::SpanLine(color)
| Self::SpanColumn(_, color) => Some(*color),
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_label(&self) -> bool {
if let Self::Label(_, _) = self {
true
} else {
false
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_free(&self) -> bool {
if let Self::Text(' ') = self {
true
} else {
false
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_span_line(&self) -> bool {
if let Self::SpanLine(_) = self {
true
} else {
false
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_inline_span_line(&self) -> bool {
match self {
Char::SpanUnderline(_, _) => true,
_ => false
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_span_column(&self) -> bool {
if let Self::SpanColumn(_, _) = self {
true
} else {
false
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_margin_column(&self) -> bool {
if let Self::Margin('|', _) = self {
true
} else {
false
}
}
}
impl From<char> for Char {
fn from(c: char) -> Self { Self::Text(c) }
}
struct Line {
data: Vec<Char>,
offset: usize,
margin_color: Color,
}
impl Line {
#[must_use]
fn new(margin: usize, margin_color: Color) -> Self {
let mut data = Vec::new();
data.resize(margin, Char::space());
Self {
data,
offset: margin,
margin_color,
}
}
#[must_use]
fn is_empty(&self) -> bool { self.data.len() == self.offset }
#[must_use]
fn get(&self, i: usize) -> Char {
if let Some(c) = self.data.get(i) {
*c
} else {
Char::space()
}
}
#[must_use]
fn is_free(&self, i: usize, j: usize) -> bool {
for k in i..j {
if !self.get(k).is_free() {
return false;
}
}
true
}
#[must_use]
fn is_first_char(&self, i: usize) -> bool {
for k in self.offset..i {
if let Char::Text(_) = self.get(k) {
return false;
}
}
true
}
fn push(&mut self, c: Char) {
if c.unwrap() == '\t' {
let len = self.data.len() - self.offset;
let tab_len = 8 - len % 8;
self.data.resize(self.offset + len + tab_len, c.with(' '));
} else {
self.data.push(c);
}
}
fn set(&mut self, i: usize, c: Char) {
if self.data.len() <= i {
self.data.resize(i + 1, Char::space());
}
if !self.data[i].is_label() {
self.data[i] = c;
}
}
fn draw_label(&mut self, label: &str, i: usize, style: Style) {
for (k, c) in label.chars().enumerate() {
self.set(i + k, Char::label(c, style.color()))
}
}
fn draw_line_number(&mut self, mut i: usize, margin: usize) {
let w = margin - 3;
self.set(margin - 2, Char::Margin('|', self.margin_color));
for k in 0..w {
if i > 0 {
let codepoint = 0x30 + i as u32 % 10;
i /= 10;
self.set(
w - k - 1,
Char::Margin(codepoint.try_into().unwrap(), self.margin_color),
);
}
}
}
}
impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
#[cfg(feature = "colors")]
let mut current_color = None;
for c in &self.data {
#[cfg(feature = "colors")]
{
if c.color() != current_color && !c.is_free() {
current_color = c.color();
if let Some(color) = current_color {
write!(f, "{}{}", termion::style::Bold, termion::color::Fg(color))?;
} else {
write!(f, "{}", termion::style::Reset)?;
}
}
}
c.unwrap().fmt(f)?;
}
'\n'.fmt(f)
}
}
struct LineBuffer {
index: usize,
margin: usize,
lines: Vec<Line>,
margin_color: Color,
}
impl LineBuffer {
#[must_use]
fn new(index: usize, line: Line, margin: usize, margin_color: Color) -> Self {
Self {
index,
margin,
lines: vec![line],
margin_color,
}
}
fn draw(&mut self, mh: &MappedHighlight) -> Option<usize> {
if mh.h.span.line_count() > 1 {
let column = self.margin - mh.column_offset - 1;
if mh.h.span.start().line == self.index {
self.draw_boundary(
column + 1,
self.margin + mh.h.span.start().column,
mh.h.style,
true,
);
None
} else if mh.h.span.end().line == self.index {
let j = self.margin + mh.h.span.last().column;
self.draw_boundary(column + 1, j, mh.h.style, false);
self.draw_column(column, mh.h.style);
Some(j)
} else if mh.h.span.start().line < self.index && mh.h.span.end().line > self.index {
self.draw_column(column, mh.h.style);
None
} else {
None
}
} else if mh.h.span.start().line == self.index {
let i = self.margin + mh.h.span.start().column;
let j = self.margin + mh.h.span.last().column;
self.draw_inline_span(i, j, mh.h.style);
Some(j)
} else {
None
}
}
fn draw_inline_span(&mut self, i: usize, j: usize, style: Style) {
let index = self.find_free_line(i, j + 1, false);
if index == 1 {
let line = &mut self.lines[index];
for k in i..=j {
line.set(k, Char::SpanUnderline(style.line(), style.color()));
}
} else {
for l in 1..=index {
let line = &mut self.lines[l];
if l == 1 {
line.set(i, Char::SpanMarker(style.marker(), style.color()));
line.set(j, Char::SpanMarker(style.marker(), style.color()));
} else if l == index {
line.set(i, Char::SpanColumn(ColumnStyle::Normal, style.color()));
line.set(j, Char::SpanColumn(ColumnStyle::Normal, style.color()));
for k in (i + 1)..j {
line.set(k, Char::SpanLine(style.color()));
}
} else {
line.set(i, Char::SpanColumn(ColumnStyle::Normal, style.color()));
line.set(j, Char::SpanColumn(ColumnStyle::Normal, style.color()));
}
}
}
}
fn draw_column(&mut self, i: usize, style: Style) {
for line in &mut self.lines {
line.set(i, Char::SpanColumn(ColumnStyle::Normal, style.color()));
}
}
fn draw_boundary(&mut self, i: usize, j: usize, style: Style, abbreviate: bool) {
if abbreviate && self.lines[0].is_first_char(j) {
self.draw_column(i - 1, style);
let first_line = &mut self.lines[0];
first_line.set(
i - 1,
Char::SpanColumn(ColumnStyle::Abbreviated, style.color()),
);
} else {
let index = self.find_free_line(i, j + 1, false);
for l in 1..=index {
let line = &mut self.lines[l];
if l == 1 {
line.set(j, Char::SpanMarker(style.marker(), style.color()));
} else {
line.set(j, Char::SpanColumn(ColumnStyle::Normal, style.color()));
}
if l == index {
for k in i..j {
line.set(k, Char::SpanLine(style.color()));
}
}
}
}
}
fn draw_label(&mut self, label: &str, i: usize, style: Style) {
let j = i + label.len() + 1;
if self.lines[1].is_free(i + 1, j + 2) {
self.lines[1].draw_label(label, i + 2, style);
} else {
let index = self.find_free_line(i, j, true);
for l in 2..=index {
let line = &mut self.lines[l];
if l == index {
line.draw_label(label, i, style);
} else {
line.set(i, Char::SpanColumn(ColumnStyle::Normal, style.color()));
}
}
}
}
fn find_free_line(&mut self, i: usize, j: usize, label: bool) -> usize {
let mut index = 1;
'next_line: loop {
if index >= self.lines.len() {
self.extend();
}
if index > 100 {
return index;
}
if self.lines[index].is_free(i, j) {
if label && index > 1 {
let last_line = &self.lines[index - 1];
for k in i..=j {
let top = last_line.get(k);
if top.is_span_line() || top.is_inline_span_line() || top.is_span_column() || top.is_label() {
index += 1;
continue 'next_line;
}
}
}
return index;
}
index += 1;
}
}
fn extend(&mut self) {
let mut new_line = Line::new(self.margin, self.margin_color);
{
let last_line = self.lines.last().unwrap();
for i in 0..self.margin {
let top = last_line.get(i);
let top_right = last_line.get(i + 1);
if top_right.is_span_line() && top.is_free() {
new_line.set(
i,
Char::SpanColumn(ColumnStyle::Normal, top_right.color().unwrap()),
);
}
if (!top_right.is_span_line() && top.is_span_column()) || top.is_margin_column() {
new_line.set(i, top);
}
}
}
self.lines.push(new_line);
}
}
impl fmt::Display for LineBuffer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for line in &self.lines {
line.fmt(f)?;
}
Ok(())
}
}
impl Highlight {
#[must_use]
fn map(&self, others: &[MappedHighlight]) -> MappedHighlight {
let mut column_offset = {
if self.span.line_count() > 1 {
1
} else {
0
}
};
for h in others.iter() {
if h.h.span.overlaps(&self.span) && column_offset > 0 && h.h.span.line_count() > 1 {
column_offset = std::cmp::max(column_offset, h.column_offset + 2);
}
}
MappedHighlight {
h: self,
column_offset,
label_position: (0, 0),
}
}
}
impl Formatter {
#[must_use]
pub fn new() -> Self { Self::default() }
#[must_use]
pub const fn with_color(margin_color: Color) -> Self {
Self {
highlights: Vec::new(),
show_line_numbers: true,
margin_color,
}
}
pub fn show_line_numbers(&mut self) { self.show_line_numbers = true; }
pub fn hide_line_numbers(&mut self) { self.show_line_numbers = false; }
pub fn add(&mut self, span: Span, label: Option<String>, style: Style) {
self.highlights.push(Highlight { span, label, style });
self.highlights.sort_by(|a, b| a.span.cmp(&b.span));
}
pub fn get<I: Iterator<Item = Result<char>>>(
&self,
input: I,
span: Span,
) -> std::io::Result<String> {
let line_number_margin = if self.show_line_numbers {
(((span.last().line + 1) as f32).log10() as usize) + 4
} else {
0
};
let mut highlights = Vec::new();
let mut highlights_margin = 0;
for h in &self.highlights {
let mapped_h = h.map(&highlights);
highlights_margin = std::cmp::max(highlights_margin, mapped_h.column_offset);
highlights.push(mapped_h);
}
if highlights_margin > 0 {
highlights_margin += 1;
}
let margin = line_number_margin + highlights_margin;
let mut lines = Vec::new();
let mut line_buffer = Line::new(margin, self.margin_color);
let mut line_span: Span = span.start().into();
if self.show_line_numbers {
line_buffer.draw_line_number(line_span.start().line + 1, line_number_margin);
}
for c in input {
let c = c?;
if line_span.end() >= span.end() {
break;
}
line_span.push(c);
if c == '\n' {
let mut new_line_buffer = Line::new(margin, self.margin_color);
std::mem::swap(&mut new_line_buffer, &mut line_buffer);
lines.push(LineBuffer::new(
line_span.start().line,
new_line_buffer,
margin,
self.margin_color,
));
line_span.clear();
if self.show_line_numbers {
line_buffer.draw_line_number(line_span.start().line + 1, line_number_margin);
}
} else {
line_buffer.push(Char::from(c));
}
if line_span.end() >= span.last() {
break;
}
}
if !line_buffer.is_empty() {
lines.push(LineBuffer::new(
line_span.start().line,
line_buffer,
margin,
self.margin_color,
));
}
for (i, line) in lines.iter_mut().enumerate() {
for mh in &mut highlights {
if let Some(pos) = line.draw(mh) {
mh.label_position = (i, pos);
}
}
}
for mh in &highlights {
if let Some(label) = &mh.h.label {
let line = &mut lines[mh.label_position.0];
line.draw_label(label, mh.label_position.1, mh.h.style);
}
}
let mut buffer = String::new();
for line in &lines {
write!(buffer, "{}", line).unwrap();
}
Ok(buffer)
}
}
impl Default for Formatter {
fn default() -> Self {
Self {
highlights: Vec::new(),
show_line_numbers: true,
margin_color: Color::Blue,
}
}
}