aiken/crates/lang/src/pretty.rs

433 lines
11 KiB
Rust

//! This module implements the functionality described in
//! ["Strictly Pretty" (2000) by Christian Lindig][0], with a few
//! extensions.
//!
//! This module is heavily influenced by Elixir's Inspect.Algebra and
//! JavaScript's Prettier.
//!
//! [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
//!
//! ## Extensions
//!
//! - `ForcedBreak` from Elixir.
//! - `FlexBreak` from Elixir.
#![allow(clippy::wrong_self_convention)]
use std::collections::VecDeque;
use itertools::Itertools;
#[macro_export]
macro_rules! docvec {
() => {
Document::Vec(Vec::new())
};
($($x:expr),+ $(,)?) => {
Document::Vec(vec![$($x.to_doc()),+])
};
}
/// Coerce a value into a Document.
/// Note we do not implement this for String as a slight pressure to favour str
/// over String.
pub trait Documentable<'a> {
fn to_doc(self) -> Document<'a>;
}
impl<'a> Documentable<'a> for char {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for &'a str {
fn to_doc(self) -> Document<'a> {
Document::Str(self)
}
}
impl<'a> Documentable<'a> for isize {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for i64 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for usize {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for f64 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{:?}", self))
}
}
impl<'a> Documentable<'a> for u64 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{:?}", self))
}
}
impl<'a> Documentable<'a> for u32 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for u16 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for u8 {
fn to_doc(self) -> Document<'a> {
Document::String(format!("{}", self))
}
}
impl<'a> Documentable<'a> for Document<'a> {
fn to_doc(self) -> Document<'a> {
self
}
}
impl<'a> Documentable<'a> for Vec<Document<'a>> {
fn to_doc(self) -> Document<'a> {
Document::Vec(self)
}
}
impl<'a, D: Documentable<'a>> Documentable<'a> for Option<D> {
fn to_doc(self) -> Document<'a> {
self.map(Documentable::to_doc).unwrap_or_else(nil)
}
}
pub fn concat<'a>(docs: impl IntoIterator<Item = Document<'a>>) -> Document<'a> {
Document::Vec(docs.into_iter().collect())
}
pub fn join<'a>(
docs: impl IntoIterator<Item = Document<'a>>,
separator: Document<'a>,
) -> Document<'a> {
concat(Itertools::intersperse(docs.into_iter(), separator))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Document<'a> {
/// A mandatory linebreak
Line(usize),
/// Forces contained groups to break
ForceBroken(Box<Self>),
/// May break contained document based on best fit, thus flex break
FlexBreak(Box<Self>),
/// Renders `broken` if group is broken, `unbroken` otherwise
Break {
broken: &'a str,
unbroken: &'a str,
kind: BreakKind,
},
/// Join multiple documents together
Vec(Vec<Self>),
/// Nests the given document by the given indent
Nest(isize, Box<Self>),
/// Nests the given document to the current cursor position
Group(Box<Self>),
/// A string to render
String(String),
/// A str to render
Str(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Broken,
Unbroken,
//
// These are used for the Fits variant, taken from Elixir's
// Inspect.Algebra's `fits` extension.
//
/// Broken and forced to remain broken
ForcedBroken,
// ForcedUnbroken, // Used for next_break_fits. Not yet implemented.
}
impl Mode {
fn is_forced(&self) -> bool {
matches!(self, Mode::ForcedBroken)
}
}
fn fits(
mut limit: isize,
mut current_width: isize,
mut docs: VecDeque<(isize, Mode, &Document<'_>)>,
) -> bool {
loop {
if current_width > limit {
return false;
};
let (indent, mode, document) = match docs.pop_front() {
Some(x) => x,
None => return true,
};
match document {
Document::ForceBroken(_) => {
return false;
}
Document::Line(_) => return true,
Document::Nest(i, doc) => docs.push_front((i + indent, mode, doc)),
Document::Group(doc) if mode.is_forced() => docs.push_front((indent, mode, doc)),
Document::Group(doc) => docs.push_front((indent, Mode::Unbroken, doc)),
Document::Str(s) => limit -= s.len() as isize,
Document::String(s) => limit -= s.len() as isize,
Document::Break { unbroken, .. } => match mode {
Mode::Broken | Mode::ForcedBroken => return true,
Mode::Unbroken => current_width += unbroken.len() as isize,
},
Document::FlexBreak(doc) => docs.push_front((indent, mode, doc)),
Document::Vec(vec) => {
for doc in vec.iter().rev() {
docs.push_front((indent, mode, doc));
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakKind {
Flex,
Strict,
}
fn format(
writer: &mut String,
limit: isize,
mut width: isize,
mut docs: VecDeque<(isize, Mode, &Document<'_>)>,
) {
while let Some((indent, mode, document)) = docs.pop_front() {
match document {
Document::Line(i) => {
for _ in 0..*i {
writer.push('\n');
}
for _ in 0..indent {
writer.push(' ');
}
width = indent;
}
// Flex breaks are NOT conditional to the mode
Document::Break {
broken,
unbroken,
kind: BreakKind::Flex,
} => {
let unbroken_width = width + unbroken.len() as isize;
if fits(limit, unbroken_width, docs.clone()) {
writer.push_str(unbroken);
width = unbroken_width;
} else {
writer.push_str(broken);
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
width = indent;
}
}
// Strict breaks are conditional to the mode
Document::Break {
broken,
unbroken,
kind: BreakKind::Strict,
} => {
width = match mode {
Mode::Unbroken => {
writer.push_str(unbroken);
width + unbroken.len() as isize
}
Mode::Broken | Mode::ForcedBroken => {
writer.push_str(broken);
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
indent
}
};
}
Document::String(s) => {
width += s.len() as isize;
writer.push_str(s);
}
Document::Str(s) => {
width += s.len() as isize;
writer.push_str(s);
}
Document::Vec(vec) => {
for doc in vec.iter().rev() {
docs.push_front((indent, mode, doc));
}
}
Document::Nest(i, doc) => {
docs.push_front((indent + i, mode, doc));
}
Document::Group(doc) | Document::FlexBreak(doc) => {
let mut group_docs = VecDeque::new();
group_docs.push_front((indent, Mode::Unbroken, doc.as_ref()));
if fits(limit, width, group_docs) {
docs.push_front((indent, Mode::Unbroken, doc));
} else {
docs.push_front((indent, Mode::Broken, doc));
}
}
Document::ForceBroken(document) => {
docs.push_front((indent, Mode::ForcedBroken, document));
}
}
}
}
pub fn nil<'a>() -> Document<'a> {
Document::Vec(vec![])
}
pub fn line<'a>() -> Document<'a> {
Document::Line(1)
}
pub fn lines<'a>(i: usize) -> Document<'a> {
Document::Line(i)
}
pub fn break_<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
Document::Break {
broken,
unbroken,
kind: BreakKind::Strict,
}
}
pub fn flex_break<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
Document::Break {
broken,
unbroken,
kind: BreakKind::Flex,
}
}
impl<'a> Document<'a> {
pub fn group(self) -> Self {
Self::Group(Box::new(self))
}
pub fn nest(self, indent: isize) -> Self {
Self::Nest(indent, Box::new(self))
}
pub fn force_break(self) -> Self {
Self::ForceBroken(Box::new(self))
}
pub fn append(self, second: impl Documentable<'a>) -> Self {
match self {
Self::Vec(mut vec) => {
vec.push(second.to_doc());
Self::Vec(vec)
}
first => Self::Vec(vec![first, second.to_doc()]),
}
}
pub fn to_pretty_string(self, limit: isize) -> String {
let mut buffer = String::new();
self.pretty_print(limit, &mut buffer);
buffer
}
pub fn surround(self, open: impl Documentable<'a>, closed: impl Documentable<'a>) -> Self {
open.to_doc().append(self).append(closed)
}
pub fn pretty_print(&self, limit: isize, writer: &mut String) {
let mut docs = VecDeque::new();
docs.push_front((0, Mode::Unbroken, self));
format(writer, limit, 0, docs);
}
/// Returns true when the document contains no printable characters
/// (whitespace and newlines are considered printable characters).
pub fn is_empty(&self) -> bool {
use Document::*;
match self {
Line(n) => *n == 0,
String(s) => s.is_empty(),
Str(s) => s.is_empty(),
// assuming `broken` and `unbroken` are equivalent
Break { broken, .. } => broken.is_empty(),
ForceBroken(d) | FlexBreak(d) | Nest(_, d) | Group(d) => d.is_empty(),
Vec(docs) => docs.iter().all(|d| d.is_empty()),
}
}
}