use crate::{Metrics, Position, Span};
use std::fmt;
#[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 = ();
#[derive(Clone, Copy)]
pub enum Style {
Error,
Warning,
Note,
Help,
Custom(char, char, Color),
}
impl Style {
#[must_use]
#[cfg(not(feature = "colors"))]
pub const fn new(underline: char, marker: char) -> Self { Self::Custom(underline, 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::Help => '-',
Self::Custom(line, _, _) => *line,
}
}
#[must_use]
pub fn marker(&self) -> char {
match self {
Self::Error | Self::Warning => '^',
Self::Note | Self::Help => '-',
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::Help => Color::Green,
Self::Custom(_, _, color) => *color,
}
}
}
}
pub struct Highlight {
span: Span,
label: Option<String>,
style: Style,
}
impl Highlight {
fn margin_nest_level(&self, highlights: &[MappedHighlight]) -> usize {
if self.span.line_count() > 1 {
let mut level = 2;
for h in highlights {
if self.span.overlaps(h.span()) {
level = std::cmp::max(level, 2 + h.margin_nest_level)
}
}
level
} else {
0
}
}
fn start_nest_level(
&self,
highlights: &[MappedHighlight],
first_non_whitespace: Option<usize>,
) -> usize {
if self.span.last.line > self.span.start.line
&& first_non_whitespace.is_some()
&& first_non_whitespace.unwrap() >= self.span.start.column
{
0
} else {
let mut level = 1;
for h in highlights {
if (self.span.start.line == h.span().start.line
|| self.span.start.line == h.span().last.line)
&& (self.span.overlaps(h.span()) || self.span.line_count() > 1)
{
level = std::cmp::max(level, 1 + h.start_nest_level)
}
}
level
}
}
fn end_nest_level(&self, highlights: &[MappedHighlight]) -> usize {
let mut level = 1;
for h in highlights {
if (self.span.last.line == h.span().start.line
|| self.span.last.line == h.span().last.line)
&& self.span.overlaps(h.span())
{
level = std::cmp::max(level, 1 + h.end_nest_level)
}
}
level
}
}
pub struct Formatter {
highlights: Vec<Highlight>,
margin_color: Color,
show_line_numbers: bool,
use_line_begining_shortcut: bool,
viewbox: Option<usize>,
}
impl Formatter {
#[must_use]
pub fn new() -> Self { Self::default() }
#[must_use]
pub const fn with_margin_color(margin_color: Color) -> Self {
Self {
highlights: Vec::new(),
margin_color,
viewbox: Some(2),
show_line_numbers: true,
use_line_begining_shortcut: true,
}
}
pub fn set_line_numbers_visible(&mut self, visible: bool) { self.show_line_numbers = visible; }
pub fn show_line_numbers(&mut self) { self.show_line_numbers = false; }
pub fn hide_line_numbers(&mut self) { self.show_line_numbers = false; }
pub fn set_viewbox(&mut self, viewbox: Option<usize>) { self.viewbox = viewbox }
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));
}
#[must_use]
pub fn span(&self) -> Option<Span> {
let mut span: Option<Span> = None;
for h in &self.highlights {
span = Some(span.map(|s| s.union(h.span)).unwrap_or(h.span))
}
span
}
}
#[derive(Clone, Copy)]
struct MappedHighlight<'a> {
h: &'a Highlight,
margin_nest_level: usize,
start_nest_level: usize,
end_nest_level: usize,
}
impl<'a> MappedHighlight<'a> {
pub const fn span(&self) -> &Span { &self.h.span }
pub const fn style(&self) -> &Style { &self.h.style }
pub const fn label(&self) -> Option<&String> { self.h.label.as_ref() }
fn update_start_nest_level(
&mut self,
highlights: &[MappedHighlight],
first_non_whitespace: Option<usize>,
) {
self.start_nest_level = self.h.start_nest_level(highlights, first_non_whitespace)
}
fn update_end_nest_level(&mut self, highlights: &[MappedHighlight]) {
self.end_nest_level = self.h.end_nest_level(highlights)
}
}
#[derive(Clone, Copy)]
pub enum Char {
Empty,
Text(char),
Margin(char, Color),
Label(char, Color),
SpanMarker(char, Color),
SpanUnderline(char, Color),
SpanVertical(Color),
SpanHorizontal(Color),
SpanMargin(Color),
SpanMarginMarker(Color),
}
impl Char {
const fn unwrap(self) -> char {
match self {
Self::Empty => ' ',
Self::Text(c)
| Self::Margin(c, _)
| Self::Label(c, _)
| Self::SpanUnderline(c, _)
| Self::SpanMarker(c, _) => c,
Self::SpanVertical(_) => '|',
Self::SpanHorizontal(_) => '_',
Self::SpanMargin(_) => '|',
Self::SpanMarginMarker(_) => '/',
}
}
#[cfg(feature = "colors")]
const fn color(&self) -> Option<Color> {
match self {
Self::Empty | Self::Text(_) => None,
Self::Margin(_, color)
| Self::Label(_, color)
| Self::SpanUnderline(_, color)
| Self::SpanMarker(_, color)
| Self::SpanVertical(color)
| Self::SpanHorizontal(color)
| Self::SpanMargin(color) | Self::SpanMarginMarker(color) => Some(*color),
}
}
const fn is_free(&self) -> bool {
match self {
Self::Empty => true,
_ => false,
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_span_horizontal(&self) -> bool {
match self {
Self::SpanHorizontal(_) => true,
_ => false,
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_span_margin(&self) -> bool {
match self {
Self::SpanMargin(_) => true,
_ => false,
}
}
}
impl From<char> for Char {
fn from(c: char) -> Self { Self::Text(c) }
}
struct CharMap {
data: Vec<Char>,
width: usize,
height: usize,
}
impl CharMap {
fn new() -> CharMap {
CharMap {
data: vec![Char::Empty],
width: 1,
height: 1,
}
}
fn from_label<M: Metrics>(text: &str, color: Color, metrics: &M) -> CharMap {
let mut map = CharMap {
data: Vec::with_capacity(text.len()),
width: 0,
height: 0,
};
let mut pos = Position::new(0, 0);
for c in text.chars() {
match c {
'\n' | '\t' => (),
_ => map.set(pos.column, pos.line, Char::Label(c, color)),
}
pos.shift(c, metrics)
}
map
}
fn height(&self) -> usize { self.height }
fn align<I: Iterator<Item = usize>>(&mut self, width: usize, _height: usize, it: I) {
for i in it {
let x = i % width;
let y = i / width;
if x < self.width {
if y < self.height {
let j = x + y * self.width;
self.data[i] = self.data[j];
} else {
let my = self.height - 1;
self.data[i] = match (self.get(x, my), self.get(x + 1, my)) {
(Char::SpanMargin(_), Char::SpanHorizontal(_))
if x == 0 || !self.get(x - 1, my).is_span_horizontal() =>
{
Char::Empty
}
(Char::SpanMargin(c), _) => Char::SpanMargin(c),
(Char::SpanMarginMarker(c), _) => Char::SpanMargin(c),
(Char::Empty, Char::SpanHorizontal(c)) => Char::SpanMargin(c),
(Char::Margin('|', c), _) => Char::Margin('|', c),
_ => Char::Empty,
}
}
} else {
self.data[i] = Char::Empty
}
}
}
fn resize(&mut self, width: usize, height: usize) {
let len = width * height;
if len != self.data.len() {
if len > self.data.len() {
self.data.resize(len, Char::Empty);
}
if width < self.width {
self.align(width, height, 0..len);
} else {
self.align(width, height, (0..len).rev());
}
if len < self.data.len() {
self.data.resize(len, Char::Empty);
}
self.width = width;
self.height = height;
}
}
fn reserve(&mut self, width: usize, height: usize) {
self.resize(
std::cmp::max(width, self.width),
std::cmp::max(height, self.height),
)
}
fn get(&self, x: usize, y: usize) -> Char {
if x >= self.width || y >= self.height {
Char::Empty
} else {
self.data[x + y * self.width]
}
}
fn set(&mut self, x: usize, y: usize, c: Char) {
self.reserve(x + 1, y + 1);
self.data[x + y * self.width] = c;
}
fn draw_marker(&mut self, style: &Style, y: usize, x: usize) {
let mut head = false;
for j in 1..=y {
let previous_c = self.get(x, j);
if previous_c.is_free() || previous_c.is_span_horizontal() {
let c = if head {
Char::SpanVertical(style.color())
} else {
head = true;
Char::SpanMarker(style.marker(), style.color())
};
self.set(x, j, c);
}
}
}
fn draw_open_line(&mut self, style: &Style, y: usize, start: usize, end: usize) {
self.reserve(end + 1, y + 1);
for x in start..=end {
if x == end {
self.draw_marker(style, y, x)
} else {
if !self.get(x, y).is_span_margin() {
self.set(x, y, Char::SpanHorizontal(style.color()))
}
}
}
}
fn draw_closed_line(&mut self, style: &Style, y: usize, start: usize, end: usize) {
self.reserve(end + 1, y + 1);
for x in start..=end {
if x == start || x == end {
self.draw_marker(style, y, x)
} else {
let c = if y == 1 {
Char::SpanUnderline(style.line(), style.color())
} else {
Char::SpanHorizontal(style.color())
};
self.set(x, y, c)
}
}
}
fn is_rect_free(&self, offset_x: usize, offset_y: usize, width: usize, height: usize) -> bool {
for y in offset_y..(offset_y + height) {
for x in offset_x..(offset_x + width) {
if !self.get(x, y).is_free() {
return false;
}
}
}
true
}
fn draw_charmap(&mut self, offset_x: usize, offset_y: usize, map: &CharMap) {
self.reserve(offset_x + map.width, offset_y + map.height);
for y in 0..map.height {
for x in 0..map.width {
self.set(offset_x + x, offset_y + y, map.get(x, y))
}
}
}
fn draw_charmap_if_free(&mut self, offset_x: usize, offset_y: usize, map: &CharMap) -> bool {
let mut dx = 0;
let mut dy = 0;
if offset_x > 0 {
dx = 1;
}
if offset_y > 1 {
dy = 1;
}
if self.is_rect_free(
offset_x - dx,
offset_y - dy,
map.width + dx + 1,
map.height + dy + 1,
) {
self.draw_charmap(offset_x, offset_y, map);
true
} else {
false
}
}
}
impl fmt::Display for CharMap {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
#[cfg(feature = "colors")]
let mut current_color = None;
for y in 0..self.height {
for x in 0..self.width {
let i = x + y * self.width;
let c = self.data[i];
#[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)?;
}
write!(f, "\n")?;
}
#[cfg(feature = "colors")]
write!(f, "{}", termion::style::Reset)?;
Ok(())
}
}
pub struct Formatted(Vec<CharMap>);
impl fmt::Display for Formatted {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for map in &self.0 {
map.fmt(f)?;
}
Ok(())
}
}
pub enum ImportantLines {
All,
Lines(Vec<usize>, usize),
}
impl ImportantLines {
fn includes(&self, line: usize) -> bool {
match self {
ImportantLines::All => true,
ImportantLines::Lines(important_lines, viewbox) => important_lines
.binary_search_by(|candidate| {
use std::cmp::Ordering;
if line <= candidate + viewbox
&& line >= candidate - std::cmp::min(candidate, viewbox)
{
Ordering::Equal
} else if line <= candidate + viewbox {
Ordering::Greater
} else {
Ordering::Less
}
})
.is_ok(),
}
}
}
impl Formatter {
fn important_lines(&self) -> ImportantLines {
if let Some(viewbox) = self.viewbox {
let mut important_lines = Vec::new();
for h in &self.highlights {
important_lines.push(h.span.start.line);
if h.span.start.line != h.span.last.line {
important_lines.push(h.span.last.line)
}
}
important_lines.sort_unstable();
ImportantLines::Lines(important_lines, viewbox)
} else {
ImportantLines::All
}
}
#[must_use]
pub fn margin_len(&self, span: &Span) -> usize {
if self.show_line_numbers {
let last_line = match self.viewbox {
Some(viewbox) => {
if let Some(last_highlight) = self.highlights.last() {
last_highlight.span.last().line + viewbox
} else {
return 0
}
},
None => span.last().line
};
(((last_line + 1) as f32).log10() as usize) + 4
} else {
0
}
}
pub fn render<E, I: Iterator<Item = Result<char, E>>, M: Metrics>(
&self,
input: I,
span: Span,
metrics: &M,
) -> Result<Formatted, E> {
let mut mapped_highlights = Vec::with_capacity(self.highlights.len());
let mut nest_margin = 0;
for h in &self.highlights {
let margin_nest_level = h.margin_nest_level(&mapped_highlights);
if margin_nest_level > nest_margin {
nest_margin = margin_nest_level;
}
mapped_highlights.push(MappedHighlight {
h,
margin_nest_level,
start_nest_level: 0,
end_nest_level: 0,
});
}
let margin_len = self.margin_len(&span);
let margin = margin_len + nest_margin;
let mut pos = span.start();
let mut lines = vec![CharMap::new()];
let important_lines = self.important_lines();
let mut is_important_line = important_lines.includes(pos.line);
if is_important_line {
lines.push(CharMap::new())
}
let mut first_non_whitespace = None;
for c in input {
if pos > span.last() {
break;
}
let c = c?;
let x = margin + pos.column;
match c {
'\n' => {
if is_important_line {
let line_charmap = lines.last_mut().unwrap();
self.draw_line_number(Some(pos.line), line_charmap, margin_len);
self.draw_line_highlights(
pos.line,
line_charmap,
margin,
&mut mapped_highlights,
metrics,
first_non_whitespace,
);
}
first_non_whitespace = None;
if important_lines.includes(pos.line + 1) {
if !is_important_line && !lines.is_empty() {
let mut viewbox_charmap = CharMap::new();
self.draw_line_number(None, &mut viewbox_charmap, margin_len);
self.draw_line_highlights(
pos.line,
&mut viewbox_charmap,
margin,
&mut mapped_highlights,
metrics,
None,
);
lines.push(viewbox_charmap)
}
is_important_line = true
} else {
is_important_line = false
}
if is_important_line {
lines.push(CharMap::new())
}
}
'\t' => (),
_ => {
if is_important_line {
if self.use_line_begining_shortcut
&& first_non_whitespace.is_none()
&& !c.is_whitespace() && !c.is_control()
{
first_non_whitespace = Some(pos.column)
}
lines.last_mut().unwrap().set(x, 0, Char::Text(c))
}
}
}
pos.shift(c, metrics)
}
if is_important_line {
let line_charmap = lines.last_mut().unwrap();
self.draw_line_number(Some(pos.line), line_charmap, margin_len);
self.draw_line_highlights(
pos.line,
line_charmap,
margin,
&mut mapped_highlights,
metrics,
first_non_whitespace,
);
}
Ok(Formatted(lines))
}
fn draw_line_number(
&self,
line: Option<usize>,
charmap: &mut CharMap,
margin_len: usize,
) {
if margin_len > 0 {
charmap.set(
margin_len - 2,
0,
Char::Margin('|', self.margin_color),
);
match line {
Some(mut line) => {
let mut x = margin_len - 3;
line += 1;
while line > 0 {
x -= 1;
let d = line % 10;
charmap.set(
x,
0,
Char::Margin(
std::char::from_digit(d as u32, 10).unwrap(),
self.margin_color,
),
);
line /= 10;
}
}
None => {
for x in 0..(margin_len - 3) {
charmap.set(x, 0, Char::Margin('.', self.margin_color))
}
}
}
}
}
fn draw_line_highlights<M: Metrics>(
&self,
line: usize,
charmap: &mut CharMap,
margin: usize,
highlights: &mut [MappedHighlight],
metrics: &M,
first_non_whitespace: Option<usize>,
) {
for i in 0..highlights.len() {
let mut h = highlights[i];
let mut shortcut = false;
if h.span().start.line == line {
h.update_start_nest_level(&highlights[0..i], first_non_whitespace);
if h.span().last.line == line {
charmap.draw_closed_line(
h.style(),
h.start_nest_level,
margin + h.span().start.column,
margin + h.span().last.column,
)
} else {
if first_non_whitespace.is_some()
&& h.span().start.column <= first_non_whitespace.unwrap()
{
shortcut = true;
charmap.set(
margin - h.margin_nest_level,
0,
Char::SpanMarginMarker(h.style().color()),
)
} else {
charmap.draw_open_line(
h.style(),
h.start_nest_level,
margin - h.margin_nest_level + 1,
margin + h.span().start.column,
)
}
}
} else if h.span().last.line == line {
h.update_end_nest_level(&highlights[0..i]);
charmap.draw_open_line(
h.style(),
h.end_nest_level,
margin - h.margin_nest_level + 1,
margin + h.span().last.column,
);
}
if shortcut || (h.span().start.line < line && h.span().last.line >= line) {
let end = if h.span().last.line == line {
h.end_nest_level
} else {
charmap.height() - 1
};
let x = margin - h.margin_nest_level;
let offset_y = if shortcut { 1 } else { 0 };
for y in offset_y..=end {
charmap.set(x, y, Char::SpanMargin(h.style().color()))
}
}
highlights[i] = h;
}
for h in highlights.iter().rev() {
if h.span().last.line == line {
if let Some(label) = h.label() {
let label_charmap = CharMap::from_label(&label, h.style().color(), metrics);
let x = margin + h.span().last.column;
let mut y = 1;
if !charmap.draw_charmap_if_free(x + 2, y, &label_charmap) {
y += 2;
while !charmap.draw_charmap_if_free(x, y, &label_charmap) {
y += 1;
}
}
for vy in 2..y {
charmap.set(x, vy, Char::SpanVertical(h.style().color()));
}
}
}
}
}
}
impl Default for Formatter {
fn default() -> Formatter {
Formatter {
highlights: Vec::new(),
#[cfg(not(feature = "colors"))]
margin_color: (),
#[cfg(feature = "colors")]
margin_color: Color::Blue,
viewbox: Some(2),
show_line_numbers: true,
use_line_begining_shortcut: true,
}
}
}