Support flexible pipe operator formatting

Rules are now as follows:

  - If a pipeline contains a newline, then the entire pipeline is formatted over multiple lines.
  - If it doesn't, then it's formatted as a single-line UNLESS it cannot fit; in which case, we fallback to multiline again.
This commit is contained in:
KtorZ 2023-03-14 16:38:57 +01:00 committed by Lucas
parent ae981403c6
commit 1311d9bd27
8 changed files with 176 additions and 35 deletions

View File

@ -456,6 +456,7 @@ pub enum UntypedExpr {
PipeLine { PipeLine {
expressions: Vec1<Self>, expressions: Vec1<Self>,
one_liner: bool,
}, },
Assignment { Assignment {

View File

@ -15,7 +15,9 @@ use crate::{
docvec, docvec,
expr::{UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR}, expr::{UntypedExpr, DEFAULT_ERROR_STR, DEFAULT_TODO_STR},
parser::extra::{Comment, ModuleExtra}, parser::extra::{Comment, ModuleExtra},
pretty::{break_, concat, flex_break, join, line, lines, nil, Document, Documentable}, pretty::{
break_, concat, flex_break, join, line, lines, nil, prebreak, Document, Documentable,
},
tipo::{self, Type}, tipo::{self, Type},
}; };
@ -683,7 +685,10 @@ impl<'comments> Formatter<'comments> {
.. ..
} => self.if_expr(branches, final_else), } => self.if_expr(branches, final_else),
UntypedExpr::PipeLine { expressions, .. } => self.pipeline(expressions), UntypedExpr::PipeLine {
expressions,
one_liner,
} => self.pipeline(expressions, *one_liner),
UntypedExpr::Int { value, .. } => value.to_doc(), UntypedExpr::Int { value, .. } => value.to_doc(),
@ -1013,7 +1018,11 @@ impl<'comments> Formatter<'comments> {
} }
} }
fn pipeline<'a>(&mut self, expressions: &'a Vec1<UntypedExpr>) -> Document<'a> { fn pipeline<'a>(
&mut self,
expressions: &'a Vec1<UntypedExpr>,
one_liner: bool,
) -> Document<'a> {
let mut docs = Vec::with_capacity(expressions.len() * 3); let mut docs = Vec::with_capacity(expressions.len() * 3);
let first = expressions.first(); let first = expressions.first();
let first_precedence = first.binop_precedence(); let first_precedence = first.binop_precedence();
@ -1031,13 +1040,30 @@ impl<'comments> Formatter<'comments> {
_ => self.wrap_expr(expr), _ => self.wrap_expr(expr),
}; };
docs.push(line());
docs.push(commented("|> ".to_doc(), comments)); let expr = self.operator_side(doc, 4, expr.binop_precedence());
docs.push(self.operator_side(doc, 4, expr.binop_precedence()));
match printed_comments(comments, true) {
None => {
let pipe = prebreak("|> ", " |> ").nest(2);
docs.push(pipe.append(expr));
}
Some(comments) => {
let pipe = prebreak("|> ", "|> ");
docs.push(
" ".to_doc()
.append(comments.nest(2).append(pipe.append(expr).group())),
);
}
}
} }
if one_liner {
docs.to_doc().group()
} else {
docs.to_doc().force_break() docs.to_doc().force_break()
} }
}
fn pipe_capture_right_hand_side<'a>(&mut self, fun: &'a UntypedExpr) -> Document<'a> { fn pipe_capture_right_hand_side<'a>(&mut self, fun: &'a UntypedExpr) -> Document<'a> {
let (fun, args) = match fun { let (fun, args) = match fun {

View File

@ -54,7 +54,6 @@ pub fn module(
extra.empty_lines.push(span.start); extra.empty_lines.push(span.start);
None None
} }
Token::NewLine => None,
Token::LeftParen => { Token::LeftParen => {
if previous_is_newline { if previous_is_newline {
Some((Token::NewLineLeftParen, *span)) Some((Token::NewLineLeftParen, *span))
@ -62,6 +61,14 @@ pub fn module(
Some((Token::LeftParen, *span)) Some((Token::LeftParen, *span))
} }
} }
Token::Pipe => {
if previous_is_newline {
Some((Token::NewLinePipe, *span))
} else {
Some((Token::Pipe, *span))
}
}
Token::NewLine => None,
_ => Some((token, *span)), _ => Some((token, *span)),
}; };
previous_is_newline = current_is_newline; previous_is_newline = current_is_newline;
@ -1309,17 +1316,30 @@ pub fn expr_parser(
// Pipeline // Pipeline
disjunction disjunction
.clone() .clone()
.then(just(Token::Pipe).ignore_then(disjunction).repeated()) .then(
.foldl(|l, r| { choice((just(Token::Pipe), just(Token::NewLinePipe)))
let expressions = if let expr::UntypedExpr::PipeLine { mut expressions } = l { .then(disjunction)
.repeated(),
)
.foldl(|l, (pipe, r)| {
if let expr::UntypedExpr::PipeLine {
mut expressions,
one_liner,
} = l
{
expressions.push(r); expressions.push(r);
expressions return expr::UntypedExpr::PipeLine {
} else { expressions,
one_liner,
};
}
let mut expressions = Vec1::new(l); let mut expressions = Vec1::new(l);
expressions.push(r); expressions.push(r);
expressions expr::UntypedExpr::PipeLine {
}; expressions,
expr::UntypedExpr::PipeLine { expressions } one_liner: pipe != Token::NewLinePipe,
}
}) })
}) })
} }

View File

@ -49,6 +49,7 @@ pub enum Token {
Vbar, // '|' Vbar, // '|'
VbarVbar, // '||' VbarVbar, // '||'
AmperAmper, // '&&' AmperAmper, // '&&'
NewLinePipe, // '↳|>'
Pipe, // '|>' Pipe, // '|>'
Dot, // '.' Dot, // '.'
RArrow, // '->' RArrow, // '->'
@ -134,6 +135,7 @@ impl fmt::Display for Token {
Token::Vbar => "|", Token::Vbar => "|",
Token::VbarVbar => "||", Token::VbarVbar => "||",
Token::AmperAmper => "&&", Token::AmperAmper => "&&",
Token::NewLinePipe => "↳|>",
Token::Pipe => "|>", Token::Pipe => "|>",
Token::Dot => ".", Token::Dot => ".",
Token::RArrow => "->", Token::RArrow => "->",

View File

@ -139,6 +139,7 @@ pub enum Document<'a> {
Break { Break {
broken: &'a str, broken: &'a str,
unbroken: &'a str, unbroken: &'a str,
break_first: bool,
kind: BreakKind, kind: BreakKind,
}, },
@ -207,6 +208,7 @@ fn fits(
Document::Group(doc) => docs.push_front((indent, Mode::Unbroken, doc)), Document::Group(doc) => docs.push_front((indent, Mode::Unbroken, doc)),
Document::Str(s) => limit -= s.len() as isize, Document::Str(s) => limit -= s.len() as isize,
Document::String(s) => limit -= s.len() as isize, Document::String(s) => limit -= s.len() as isize,
Document::Break { unbroken, .. } => match mode { Document::Break { unbroken, .. } => match mode {
@ -255,31 +257,39 @@ fn format(
Document::Break { Document::Break {
broken, broken,
unbroken, unbroken,
break_first,
kind: BreakKind::Flex, kind: BreakKind::Flex,
} => { } => {
let unbroken_width = width + unbroken.len() as isize; let unbroken_width = width + unbroken.len() as isize;
if fits(limit, unbroken_width, docs.clone()) { if fits(limit, unbroken_width, docs.clone()) {
writer.push_str(unbroken); writer.push_str(unbroken);
width = unbroken_width; width = unbroken_width;
} else { continue;
writer.push_str(broken); }
if *break_first {
writer.push('\n'); writer.push('\n');
for _ in 0..indent { for _ in 0..indent {
writer.push(' '); writer.push(' ');
} }
writer.push_str(broken);
} else {
writer.push_str(broken);
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
}
width = indent; width = indent;
} }
}
// Strict breaks are conditional to the mode // Strict breaks are conditional to the mode
Document::Break { Document::Break {
broken, broken,
unbroken, unbroken,
break_first,
kind: BreakKind::Strict, kind: BreakKind::Strict,
} => { } => {
width = match mode { width = match mode {
@ -289,6 +299,18 @@ fn format(
width + unbroken.len() as isize width + unbroken.len() as isize
} }
Mode::Broken | Mode::ForcedBroken if *break_first => {
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
writer.push_str(broken);
indent
}
Mode::Broken | Mode::ForcedBroken => { Mode::Broken | Mode::ForcedBroken => {
writer.push_str(broken); writer.push_str(broken);
@ -361,6 +383,16 @@ pub fn break_<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
broken, broken,
unbroken, unbroken,
kind: BreakKind::Strict, kind: BreakKind::Strict,
break_first: false,
}
}
pub fn prebreak<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
Document::Break {
broken,
unbroken,
kind: BreakKind::Strict,
break_first: true,
} }
} }
@ -369,6 +401,16 @@ pub fn flex_break<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
broken, broken,
unbroken, unbroken,
kind: BreakKind::Flex, kind: BreakKind::Flex,
break_first: false,
}
}
pub fn flex_prebreak<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
Document::Break {
broken,
unbroken,
kind: BreakKind::Flex,
break_first: true,
} }
} }

View File

@ -560,3 +560,50 @@ fn test_unicode() {
assert_fmt(src, src); assert_fmt(src, src);
} }
#[test]
fn test_preserve_pipe() {
let src = indoc! { r#"
fn foo() {
a |> b |> c |> d
}
fn foo() {
a
// Foo
|> b// Some comments
|> c
|> d
}
fn baz() {
// Commented
however |> it_automatically_breaks |> into_multiple_lines |> anytime_when |> it_is_too_long // What?
}
"#};
let expected = indoc! { r#"
fn foo() {
a |> b |> c |> d
}
fn foo() {
a // Foo
|> b // Some comments
|> c
|> d
}
fn baz() {
// Commented
however
|> it_automatically_breaks
|> into_multiple_lines
|> anytime_when
|> it_is_too_long
// What?
}
"#};
assert_fmt(src, expected);
}

View File

@ -545,6 +545,7 @@ fn pipeline() {
tipo: (), tipo: (),
}], }],
body: expr::UntypedExpr::PipeLine { body: expr::UntypedExpr::PipeLine {
one_liner: false,
expressions: vec1::vec1![ expressions: vec1::vec1![
expr::UntypedExpr::BinOp { expr::UntypedExpr::BinOp {
location: Span::new((), 31..36), location: Span::new((), 31..36),
@ -717,6 +718,7 @@ fn let_bindings() {
expr::UntypedExpr::Assignment { expr::UntypedExpr::Assignment {
location: Span::new((), 23..70), location: Span::new((), 23..70),
value: Box::new(expr::UntypedExpr::PipeLine { value: Box::new(expr::UntypedExpr::PipeLine {
one_liner: false,
expressions: vec1::vec1![ expressions: vec1::vec1![
expr::UntypedExpr::BinOp { expr::UntypedExpr::BinOp {
location: Span::new((), 35..40), location: Span::new((), 35..40),
@ -1105,6 +1107,7 @@ fn anonymous_function() {
annotation: None, annotation: None,
}, },
expr::UntypedExpr::PipeLine { expr::UntypedExpr::PipeLine {
one_liner: true,
expressions: vec1::vec1![ expressions: vec1::vec1![
expr::UntypedExpr::Int { expr::UntypedExpr::Int {
location: Span::new((), 71..72), location: Span::new((), 71..72),

View File

@ -274,7 +274,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
location, value, .. location, value, ..
} => Ok(self.infer_string(value, location)), } => Ok(self.infer_string(value, location)),
UntypedExpr::PipeLine { expressions } => self.infer_pipeline(expressions), UntypedExpr::PipeLine { expressions, .. } => self.infer_pipeline(expressions),
UntypedExpr::Fn { UntypedExpr::Fn {
location, location,