#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
use alloc::vec::Vec;
use core::fmt::{self, Display, Formatter};
use core::ops::Range;
pub struct LineIndex<'a>(Vec<(usize, &'a str)>);
impl<'a> LineIndex<'a> {
#[must_use]
pub fn new(s: &'a str) -> Self {
let newlines: Vec<_> = s
.char_indices()
.filter_map(|(i, c)| (c == '\n').then_some(i))
.collect();
let starts = core::iter::once(0).chain(newlines.iter().map(|i| *i + 1));
let ends = newlines.iter().copied().chain(core::iter::once(s.len()));
let lines = starts.zip(ends).map(|(start, end)| (start, &s[start..end]));
Self(lines.collect())
}
fn get(&self, offset: usize) -> Option<IndexEntry<'_>> {
use core::cmp::Ordering;
let line_no = self.0.binary_search_by(|(line_start, line)| {
if *line_start > offset {
Ordering::Greater
} else if line_start + line.len() < offset {
Ordering::Less
} else {
Ordering::Equal
}
});
let line_no = line_no.ok()?;
let (line_start, line) = self.0[line_no];
Some(IndexEntry {
line_no,
line,
bytes: offset - line_start,
})
}
}
struct IndexEntry<'a> {
line: &'a str,
line_no: usize,
bytes: usize,
}
struct Fns {
snake: Option<fn(&mut Formatter, usize) -> fmt::Result>,
text: fn(&mut Formatter, usize, usize) -> fmt::Result,
}
impl<T> LabelKind<T> {
fn has_snake(&self) -> bool {
match self {
Self::None => false,
Self::Snake | Self::Text(_) => true,
}
}
fn text(self) -> Option<T> {
match self {
Self::None | Self::Snake => None,
Self::Text(t) => Some(t),
}
}
}
pub struct Label<C, T, S = ()> {
code: C,
kind: LabelKind<T>,
style: S,
}
impl<T, S: Default> Label<Range<usize>, T, S> {
#[must_use]
pub fn new(code: Range<usize>) -> Self {
Self {
code,
kind: LabelKind::None,
style: S::default(),
}
}
}
impl<C, T, S> Label<C, T, S> {
#[must_use]
pub fn with_text(self, text: T) -> Self {
let kind = LabelKind::Text(text);
Self { kind, ..self }
}
#[must_use]
pub fn with_snake(self) -> Self {
let kind = LabelKind::Snake;
Self { kind, ..self }
}
#[must_use]
pub fn with_style(self, style: S) -> Self {
Self { style, ..self }
}
}
pub struct CodeWidth<C> {
code: C,
width: usize,
}
impl<C> CodeWidth<C> {
pub fn new(code: C, width: usize) -> Self {
CodeWidth { code, width }
}
fn left_right(&self) -> (usize, usize) {
let left = self.width / 2;
let right = self.width.saturating_sub(left + 1);
(left, right)
}
}
impl<C: Display> Display for CodeWidth<C> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.code.fmt(f)
}
}
type Paint<S> = fn(&mut Formatter, &S, &dyn Display) -> fmt::Result;
fn styled<'a, S>(paint: Paint<S>, style: &'a S, x: &'a impl Display) -> impl Display + 'a {
from_fn(move |f| paint(f, style, x))
}
struct FromFn<F>(F);
fn from_fn<F: Fn(&mut Formatter) -> fmt::Result>(f: F) -> FromFn<F> {
FromFn(f)
}
impl<F: Fn(&mut Formatter) -> fmt::Result> Display for FromFn<F> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
(self.0)(f)
}
}
enum LabelKind<T> {
None,
Snake,
Text(T),
}
pub struct Block<C, T, S> {
lines: Vec<Option<Line<C, T, S>>>,
paint: Paint<S>,
}
struct Line<C, T, S> {
no: usize,
parts: LineParts<C, T, S>,
}
impl<C, T, S> Line<C, T, S> {
fn map_code<C1>(self, f: impl FnMut(C) -> C1) -> Line<C1, T, S> {
Line {
no: self.no,
parts: self.parts.map_code(f),
}
}
}
struct LineParts<C, T, S> {
incoming: Option<(C, Option<T>, S)>,
inside: Vec<(C, LabelKind<T>, S)>,
outgoing: Option<(C, S)>,
}
impl<C, T, S> LineParts<C, T, S> {
fn arrows_below(&self) -> bool {
let inside = |(_code, label, _style): &_| LabelKind::has_snake(label);
self.incoming.is_some() || self.outgoing.is_some() || self.inside.iter().any(inside)
}
}
impl<C, T, S> Default for LineParts<C, T, S> {
fn default() -> Self {
Self {
incoming: None,
inside: Vec::new(),
outgoing: None,
}
}
}
impl<'a, T, S: Default + Clone> Block<&'a str, T, S> {
pub fn new<I>(idx: &'a LineIndex, labels: I) -> Option<Self>
where
I: IntoIterator<Item = Label<Range<usize>, T, S>>,
{
let mut prev_range: Option<Range<_>> = None;
let mut lines = Vec::new();
for Label { kind, code, style } in labels {
if code.start > code.end {
return None;
}
if let Some(prev) = prev_range.replace(code.clone()) {
if code.start <= prev.start || code.start < prev.end {
return None;
}
}
let start = idx.get(code.start)?;
let end = idx.get(code.end)?;
debug_assert!(start.line_no <= end.line_no);
let mut parts = match lines.pop() {
Some(Some((line_no, _line, parts))) if line_no == start.line_no => parts,
Some(line) => {
let non_consecutive = line
.as_ref()
.filter(|(no, ..)| start.line_no > no + 1)
.is_some();
lines.push(line);
if non_consecutive {
lines.push(None);
}
LineParts::default()
}
None => LineParts::default(),
};
if start.line_no == end.line_no {
parts.inside.push((start.bytes..end.bytes, kind, style));
lines.push(Some((start.line_no, start.line, parts)));
} else {
let range = start.bytes..start.line.len();
if kind.has_snake() {
parts.outgoing = Some((range, style.clone()));
} else {
parts.inside.push((range, LabelKind::None, style.clone()));
}
lines.push(Some((start.line_no, start.line, parts)));
for line_no in start.line_no + 1..end.line_no {
let line = idx.0[line_no].1;
let parts = LineParts {
inside: Vec::from([(0..line.len(), LabelKind::None, style.clone())]),
..Default::default()
};
lines.push(Some((line_no, line, parts)));
}
let mut parts = LineParts::default();
let range = 0..end.bytes;
if kind.has_snake() {
parts.incoming = Some((range, kind.text(), style.clone()));
} else {
parts.inside.push((range, LabelKind::None, style.clone()));
}
lines.push(Some((end.line_no, end.line, parts)));
}
}
let lines = lines.into_iter().map(|line| {
line.map(|(no, line, parts)| Line {
no,
parts: parts.segment(line),
})
});
Some(Block {
lines: lines.collect(),
paint: |f, _, s| write!(f, "{s}"),
})
}
}
impl<C, T, S> Block<C, T, S> {
#[must_use]
pub fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> Block<C1, T, S> {
let f = |line: Option<Line<C, T, S>>| line.map(|line| line.map_code(&mut f));
Block {
lines: self.lines.into_iter().map(f).collect(),
paint: self.paint,
}
}
fn some_incoming(&self) -> bool {
let mut lines = self.lines.iter().flatten();
lines.any(|line| line.parts.incoming.is_some())
}
fn line_no_width(&self) -> usize {
let max = self.lines.iter().flatten().next_back().unwrap().no + 1;
core::iter::successors(Some(max), |&n| (n >= 10).then_some(n / 10)).count()
}
#[must_use]
pub fn prologue(&self) -> impl Display {
let space = space(self.line_no_width());
from_fn(move |f| write!(f, "{space} {}{}", Snake::UpRight, Snake::Horizontal))
}
#[must_use]
pub fn space_vert(&self) -> impl Display {
let space = space(self.line_no_width());
from_fn(move |f| write!(f, "{space} {}", Snake::Vertical))
}
#[must_use]
pub fn epilogue(&self) -> impl Display {
Snake::line_up(self.line_no_width() + 1)
}
}
impl<C, T, S> Block<C, T, S> {
#[must_use]
pub fn with_paint(self, paint: Paint<S>) -> Self {
Self { paint, ..self }
}
}
fn space(width: usize) -> impl Display {
from_fn(move |f| write!(f, "{:width$}", ""))
}
macro_rules! width {
($slice:expr) => {
$slice.iter().map(|(code, ..)| code.width).sum::<usize>()
};
}
impl<C: Display, T: Display, S> Display for Block<CodeWidth<C>, T, S> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let paint = self.paint;
let mut incoming_style: Option<&S> = None;
let line_no_width = self.line_no_width();
let line_no_space = space(line_no_width);
let dots = from_fn(move |f| write!(f, "{line_no_space} {}", Snake::VerticalDots));
let incoming_space = if self.some_incoming() { " " } else { "" };
for line in &self.lines {
let Line { no: line_no, parts } = if let Some(line) = line {
line
} else {
writeln!(f, "{dots}")?;
continue;
};
write!(f, "{:>line_no_width$} │", line_no + 1)?;
if let Some(style) = incoming_style {
write!(f, " {}", styled(paint, style, &Snake::Vertical))?;
} else {
incoming_space.fmt(f)?;
}
write!(f, " ")?;
parts.code_parts().try_for_each(|(c, s)| paint(f, s, c))?;
writeln!(f)?;
if parts.arrows_below() {
write!(f, "{dots} ")?;
if let Some(style) = incoming_style {
styled(paint, style, &Snake::Vertical).fmt(f)?;
parts.incoming(paint, Snake::ArrowUp).fmt(f)?;
} else {
incoming_space.fmt(f)?;
}
writeln!(f, "{}", parts.arrows(paint))?;
}
if parts.incoming.is_some() {
assert!(incoming_style.take().is_some());
parts.incoming_text(&dots, paint).fmt(f)?;
}
let incoming_width = width!(parts.incoming);
let prefix = from_fn(|f| write!(f, "{dots}{incoming_space} {}", space(incoming_width)));
parts.inside_text(&prefix, paint).fmt(f)?;
if let Some((_, style)) = &parts.outgoing {
let snake = Snake::up_line_up(incoming_width + width!(parts.inside) + 1);
writeln!(f, "{dots} {}", styled(paint, style, &snake))?;
incoming_style = Some(style);
}
}
Ok(())
}
}
impl<C: Display, T, S> LineParts<C, T, S> {
fn code_parts(&self) -> impl Iterator<Item = (&C, &S)> {
let inside = self.inside.iter().map(|(code, _label, styl)| (code, styl));
let incoming = self.incoming.iter().map(|(code, _text, styl)| (code, styl));
let outgoing = self.outgoing.iter().map(|(code, styl)| (code, styl));
incoming.chain(inside).chain(outgoing)
}
}
impl<T, S: Default> LineParts<Range<usize>, T, S> {
fn segment(self, line: &str) -> LineParts<&str, T, S> {
let len = line.len();
let start = self.incoming.as_ref().map_or(0, |(code, ..)| code.end);
let end = self.outgoing.as_ref().map_or(len, |(code, _)| code.start);
let last = self.inside.last().map_or(start, |(code, ..)| code.end);
let mut pos = start;
let unlabelled =
|start, end| (start < end).then(|| (&line[start..end], LabelKind::None, S::default()));
let inside = self.inside.into_iter().flat_map(|(code, label, style)| {
let unlabelled = unlabelled(pos, code.start);
let labelled = (&line[code.start..code.end], label, style);
pos = code.end;
unlabelled.into_iter().chain([labelled])
});
LineParts {
incoming: self
.incoming
.map(|(code, text, sty)| (&line[..code.end], text, sty)),
inside: inside.chain(unlabelled(last, end)).collect(),
outgoing: self.outgoing.map(|(code, sty)| (&line[code.start..], sty)),
}
}
}
impl<C, T, S> LineParts<C, T, S> {
#[must_use]
fn map_code<C1>(self, mut f: impl FnMut(C) -> C1) -> LineParts<C1, T, S> {
let inside = self.inside.into_iter();
LineParts {
incoming: self.incoming.map(|(code, txt, sty)| (f(code), txt, sty)),
inside: inside.map(|(code, lbl, sty)| (f(code), lbl, sty)).collect(),
outgoing: self.outgoing.map(|(code, style)| (f(code), style)),
}
}
}
impl<C, T, S> LineParts<CodeWidth<C>, T, S> {
fn width(&self) -> usize {
width!(self.inside) + width!(self.incoming) + width!(self.outgoing)
}
fn incoming<'a>(&'a self, paint: Paint<S>, snake: Snake) -> impl Display + 'a {
from_fn(move |f| {
if let Some((code, _text, style)) = &self.incoming {
space(code.width).fmt(f)?;
paint(f, style, &snake)?;
}
Ok(())
})
}
fn outgoing<'a>(&'a self, paint: Paint<S>, snake: Snake) -> impl Display + 'a {
from_fn(move |f| {
if let Some((_code, style)) = &self.outgoing {
paint(f, style, &snake)?;
}
Ok(())
})
}
fn inside<'a>(&'a self, paint: Paint<S>, from: usize, fns: &'a Fns) -> impl Display + 'a {
from_fn(move |f| {
space(width!(self.inside[..from])).fmt(f)?;
for (code, label, style) in &self.inside[from..] {
match label {
LabelKind::Text(_text) => {
let (left, right) = code.left_right();
paint(f, style, &from_fn(|f| (fns.text)(f, left, right)))
}
LabelKind::Snake => match fns.snake {
Some(snake) => paint(f, style, &from_fn(|f| (snake)(f, code.width))),
None => space(code.width).fmt(f),
},
LabelKind::None => space(code.width).fmt(f),
}?;
}
Ok(())
})
}
fn arrows(&self, paint: Paint<S>) -> impl Display + '_ {
let fns = Fns {
snake: Some(|f, w| Snake::line(w).fmt(f)),
text: |f, l, r| Snake::line_down_line(l, r).fmt(f),
};
let outgoing = self.outgoing(paint, Snake::ArrowUp);
from_fn(move |f| write!(f, "{}{outgoing}", self.inside(paint, 0, &fns)))
}
fn inside_vert(&self, paint: Paint<S>, from: usize) -> impl Display + '_ {
let fns = Fns {
snake: None,
text: |f, l, r| write!(f, "{}{}{}", space(l), Snake::Vertical, space(r)),
};
let outgoing = self.outgoing(paint, Snake::Vertical);
from_fn(move |f| write!(f, "{}{outgoing}", self.inside(paint, from, &fns)))
}
}
impl<C, T: Display, S> LineParts<CodeWidth<C>, T, S> {
fn inside_text<'a>(&'a self, prefix: &'a impl Display, paint: Paint<S>) -> impl Display + 'a {
from_fn(move |f| {
let mut before = 0;
for (i, (code, label, style)) in self.inside.iter().enumerate() {
if let LabelKind::Text(text) = label {
writeln!(f, "{prefix}{}", self.inside_vert(paint, i))?;
let (left, right) = code.left_right();
let after = width!(self.inside) - before - code.width + width!(self.outgoing);
let space = space(before + left);
let snake = Snake::down_line(right + after + 1);
writeln!(f, "{prefix}{space}{} {text}", styled(paint, style, &snake))?;
}
before += code.width;
}
Ok(())
})
}
fn incoming_text<'a>(&'a self, dots: &'a impl Display, paint: Paint<S>) -> impl Display + 'a {
from_fn(move |f| {
if let Some((code, text, style)) = &self.incoming {
let snake = styled(paint, style, &Snake::Vertical);
let incoming = self.incoming(paint, Snake::Vertical);
writeln!(f, "{dots} {snake}{incoming}{}", self.inside_vert(paint, 0))?;
if let Some(text) = text {
let snake = Snake::down_line_up_line(code.width, self.width() + 1 - code.width);
let snake = styled(paint, style, &snake);
writeln!(f, "{dots} {snake} {text}")
} else {
let snake = Snake::down_line_up(code.width);
let snake = styled(paint, style, &snake);
writeln!(f, "{dots} {snake}{}", self.inside_vert(paint, 0))
}?
}
Ok(())
})
}
}
#[derive(Copy, Clone)]
enum Snake {
Horizontal,
Vertical,
VerticalDots,
UpRight,
RightUp,
DownRight,
ArrowUp,
HorizontalUp,
HorizontalDown,
}
impl Snake {
fn line(len: usize) -> impl Display {
from_fn(move |f| write!(f, "{:─>len$}", ""))
}
fn down_line(len: usize) -> impl Display {
from_fn(move |f| write!(f, "{}{}", Snake::DownRight, Snake::line(len)))
}
fn down_line_up_line(l: usize, r: usize) -> impl Display {
let l = Self::line(l);
let r = Self::line(r);
from_fn(move |f| write!(f, "{}{l}{}{r}", Self::DownRight, Self::HorizontalUp,))
}
fn down_line_up(len: usize) -> impl Display {
from_fn(move |f| write!(f, "{}{}{}", Self::DownRight, Self::line(len), Self::RightUp))
}
fn up_line_up(len: usize) -> impl Display {
from_fn(move |f| write!(f, "{}{}{}", Self::UpRight, Self::line(len), Self::RightUp))
}
fn line_up(len: usize) -> impl Display {
from_fn(move |f| write!(f, "{}{}", Self::line(len), Self::RightUp))
}
fn line_down_line(l: usize, r: usize) -> impl Display {
let l = Self::line(l);
let r = Self::line(r);
from_fn(move |f| write!(f, "{l}{}{r}", Self::HorizontalDown,))
}
fn as_str(self) -> &'static str {
match self {
Self::Horizontal => "─",
Self::Vertical => "│",
Self::VerticalDots => "┆",
Self::UpRight => "╭",
Self::RightUp => "╯",
Self::DownRight => "╰",
Self::ArrowUp => "▲",
Self::HorizontalUp => "┴",
Self::HorizontalDown => "┬",
}
}
}
impl Display for Snake {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.as_str().fmt(f)
}
}