///|
/// Where a type is: this decides its parentheses.
priv enum TypeAt {
AnyType // nothing needs parentheses
ArrowLeft // left of `->`: a function type needs them
TypeArg // argument of a type application: also an application with arguments
}
///|
fn Ctx::type_doc(
self : Ctx,
path : @syntax.NodePath,
level : Int,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> @pretty.Doc raise PrintError {
let (lead, d, trail) = self.type_parts(path, level, t, at)
lead + d + trail
}
///|
/// The leading comments, the type and the trailing comments, taken in that
/// order.
fn Ctx::type_parts(
self : Ctx,
path : @syntax.NodePath,
level : Int,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> (@pretty.Doc, @pretty.Doc, @pretty.Doc) raise PrintError {
check_level(path, level)
let lead = self.leading(t.range)
let d = match self.type_paren_comments(t) {
Some((pre, post)) => {
// elm-format Parse/Type.hs tuple: parentheses that hold comments are
// `TypeParens`, printed as `parens (formatCommented ...)`.
let (inner, flat) = commented(
pre,
self.type_body(path, level, t, AnyType),
post,
)
@pretty.group(
@pretty.align(
@pretty.text("(") +
@pretty.nest(1, inner) +
(if flat { @pretty.softline() } else { @pretty.hardline() }) +
@pretty.text(")"),
),
)
}
None => self.type_body(path, level, t, at)
}
(lead, d, self.trailing(t.range))
}
///|
/// The source parentheses around type `t`: the starts of the first and the
/// last token in them. elm-syntax keeps no node for these parentheses; the
/// range of `t` then starts at the `(` and ends after the `)`. `None` when
/// there is no source or there are no parentheses.
fn Ctx::type_parens(
self : Ctx,
t : @ast.Node[@ast.TypeAnnotation],
) -> (@ast.Location, @ast.Location)? {
guard self.source is Some(facts) else { return None }
// A type that starts with its first child has no parentheses of its own.
let first_child = match t.value {
Unit | Tupled(_) => return None
Typed(name, _) => Some(name.range.start)
FunctionTypeAnnotation(left, _) => Some(left.range.start)
GenericType(_) | Record(_) | GenericRecord(_, _) => None
}
guard facts.token_at(t.range.start) is Some(open) &&
facts.lexemes[open] == "(" &&
!(first_child is Some(s) && compare_location(s, t.range.start) == 0) else {
return None
}
let close = facts.first_at_or_after(t.range.end) - 1
guard close > open + 1 && facts.lexemes[close] == ")" else { return None }
Some((facts.starts[open + 1], facts.starts[close - 1]))
}
///|
/// The comments in the source parentheses around type `t` (see
/// `Ctx::type_parens`) before and after the type in them; marks them
/// printed. elm-format keeps such parentheses. `None` when there are no
/// such comments.
fn Ctx::type_paren_comments(
self : Ctx,
t : @ast.Node[@ast.TypeAnnotation],
) -> (Array[@ast.Node[String]], Array[@ast.Node[String]])? {
guard self.type_parens(t) is Some((first, last)) else { return None }
let before = (c : @ast.Node[String]) => at_or_before(c.range.end, first)
let after = (c : @ast.Node[String]) => {
compare_location(last, c.range.start) < 0
}
// The comments before the name of a type application lead its first
// argument, and the comments after its last part trail that part.
let (first_part, last_part) = match t.value {
Typed(_, args) if !args.is_empty() =>
(Some(args[0].range), Some(args[args.length() - 1].range))
FunctionTypeAnnotation(left, right) => {
let mut r = right
while r.value is FunctionTypeAnnotation(_, next) {
r = next
}
(Some(left.range), Some(r.range))
}
_ => (None, None)
}
let pre = match first_part {
Some(r) => self.take_if(Leading, r, before)
None => []
}
pre.append(self.take_if(Inner, t.range, before))
let post = self.take_if(Inner, t.range, after)
match last_part {
Some(r) => post.append(self.take_if(Trailing, r, after))
None => ()
}
post.sort_by((a, b) => compare_location(a.range.start, b.range.start))
pre.sort_by((a, b) => compare_location(a.range.start, b.range.start))
if pre.is_empty() && post.is_empty() {
None
} else {
Some((pre, post))
}
}
///|
/// The type without its own comments.
fn Ctx::type_body(
self : Ctx,
path : @syntax.NodePath,
level : Int,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> @pretty.Doc raise PrintError {
match t.value {
GenericType(name) => self.lower_name(path, name)
Unit => @pretty.text("(") + self.inner_alone(t.range) + @pretty.text(")")
Typed(name, args) => {
let (module_, base_name) = name.value
let head = qualified_upper(path, module_, base_name)
guard !args.is_empty() else { return head }
let docs = []
for i, a in args {
docs.push(self.type_doc(path.child("args", i), level + 1, a, TypeArg))
}
// elm-format Parse/Type.hs app (`checkMultiline`): a line break
// anywhere in the application puts each argument on its own line
// (`FASplitFirst`); else `FAJoinFirst JoinAll`.
let split = self.split_within({
start: name.range.start,
end: args[args.length() - 1].range.end,
})
let d = application(split, self.join(), head, docs)
if at is TypeArg {
parens(self.join(), d)
} else {
d
}
}
Tupled(items) => {
guard items.length() >= 2 else {
raise PrintError(path~, problem=ShortTuple)
}
let docs = []
for i, x in items {
if i > 0 {
// elm-format Box.hs TupleType (`formatC2Eol`): the comments before
// a `,` go under the item before it.
let comma = self.token_before(x.range.start)
let cs = self.take_if(Leading, x.range, c => ends_before(c, comma))
if !cs.is_empty() {
docs[i - 1] = docs[i - 1] + @pretty.hardline() + comment_box(cs)
}
}
docs.push(self.type_doc(path.child("values", i), level + 1, x, AnyType))
}
// elm-format Parse/Type.hs tuple (`checkMultiline`).
sequence(
self.split_within(t.range),
"(",
")",
docs,
inner=@pretty.nest(2, self.inner_close(t.range)),
)
}
Record(fields) => {
let (docs, leads) = self.record_fields(path, "value", level, fields)
let inner = match fields.last() {
Some(f) => self.inner(t.range, last=Some(f.range))
None => self.inner_alone(t.range, brace=true)
}
// elm-format Parse/Type.hs record (`checkMultiline`).
sequence(self.split_within(t.range), "{", "}", docs, leads~, inner~)
}
GenericRecord(name, fields) => {
guard !fields.value.is_empty() else {
raise PrintError(path~, problem=NoFields)
}
// elm-format Box.hs formatRecordLike: the comments before the `|`
// follow the base (`formatCommented`), in its column.
let bar = self.token_before(fields.value[0].range.start)
let base_post = self.take_if(Leading, fields.value[0].range, c => {
ends_before(c, bar)
})
let base = @pretty.align(
self.with_comments(
name.range,
self.lower_name(path.child("name", 0), name.value),
) +
(if base_post.iter().all(one_line) {
comments_after(base_post)
} else {
@pretty.hardline() + comment_block(base_post)
}),
)
let (docs, leads) = self.record_fields(
path,
"values",
level,
fields.value,
)
// elm-format Parse/Type.hs record (`checkMultiline`).
extension(
self.split_within(t.range),
base,
docs,
leads~,
inner=self.inner(t.range, last=fields.value.last().map(f => f.range)),
)
}
FunctionTypeAnnotation(_, _) => {
let d = @pretty.group(self.arrow_chain(path, level, t))
if at is AnyType {
d
} else {
parens(self.join(), d)
}
}
}
}
///|
/// `a : A` for each field, at `path.child(field_name, i)`, and the comments
/// before the `,` of each (see `Ctx::before_item`).
fn Ctx::record_fields(
self : Ctx,
path : @syntax.NodePath,
field_name : String,
level : Int,
fields : @ast.RecordDefinition,
) -> (Array[@pretty.Doc], Array[@pretty.Doc]) raise PrintError {
let docs = []
let leads = []
for i, f in fields {
let p = path.child(field_name, i)
let name = f.value.name
let previous = if i > 0 { Some(fields[i - 1].range) } else { None }
leads.push(
match previous {
Some(r) => self.before_item(Some(r), f.range)
None => @pretty.empty()
},
)
// elm-format Box.hs formatPair: the comments between the label and
// the `:` follow the label (`formatTailCommented`). When one of them
// is not one line, the label, each comment, the `:` and the type go on
// lines of their own, the last two at the next tab stop.
let colon = self.token_after(name.range.end)
let label_post = self.take_if(Trailing, name.range, c => {
is_line_comment(c) && ends_before(c, colon)
})
label_post.append(
self.take_if(Leading, f.value.type_annotation.range, c => {
ends_before(c, colon)
}),
)
let lead = self.after_comma(previous, f.range)
let moved = self.after_token(name.range)
let label = self.with_comments(
name.range,
self.lower_name(p.child("name", 0), name.value),
)
// The comments after the type go after the field, outside its group.
let value_trail = self.take(Trailing, f.value.type_annotation.range)
let value = self.type_doc(
p.child("typeAnnotation", 0),
level + 1,
f.value.type_annotation,
AnyType,
)
// elm-format Parse/Common.hs pair (`checkMultiline`): a line break
// from the label to the end of the type.
let split = self.split_within({
start: name.range.start,
end: f.value.type_annotation.range.end,
})
let pair = if label_post.iter().all(one_line) {
field(
split,
label + comments_after(label_post),
self.separator(name.range, " :", moved),
value,
)
} else {
label +
@pretty.hardline() +
comment_block(label_post) +
@pretty.tab(
4,
@pretty.hardline() +
@pretty.text(":") +
comments_after(moved) +
@pretty.hardline() +
value,
)
}
docs.push(
lead +
pair +
comments_after(value_trail) +
self.trailing_before_comma(f.range, fields.get(i + 1).map(n => n.range)),
)
}
(docs, leads)
}
///|
/// `a -> b -> c`, flattened without recursion and without its own group (a
/// signature shares its group with `name :`). Not a function type: the type
/// itself. The comments before a flattened function type go before its
/// `->`.
///
/// In the `ElmFormat` layout (elm-format Box.hs `FunctionType`), a line
/// break anywhere in the type (Parse/Helpers.hs `separated`) puts each `->`
/// on its own line, and a multi-line type after a `->` goes on the next
/// line at the next tab stop (`prefixOrIndented`). The comments after a
/// `->` go after it.
fn Ctx::arrow_chain(
self : Ctx,
path : @syntax.NodePath,
level : Int,
t : @ast.Node[@ast.TypeAnnotation],
eol? : Array[@ast.Node[String]] = [],
) -> @pretty.Doc raise PrintError {
let split = self.split_within(t.range)
let mut node = t
let mut p = path
let mut d = @pretty.empty()
let mut first = true
let mut lead = @pretty.empty()
// The comments after a `->` that trail the type before it.
let mut moved = []
// `-> t`, with the comments `after` the `->`.
let arrow = (
lead : @pretty.Doc,
after : Array[@ast.Node[String]],
parts : (@pretty.Doc, @pretty.Doc, @pretty.Doc),
) => {
let (l_lead, l_body, l_trail) = parts
match split {
Fit =>
@pretty.line() +
lead +
@pretty.text("->") +
comments_after(after) +
@pretty.text(" ") +
l_lead +
l_body +
l_trail
Join | Split =>
split_line(split) +
lead +
@pretty.text("->") +
@pretty.group(
@pretty.tab(
4,
@pretty.line() +
comments_joined(after, @pretty.text(" ")) +
l_lead +
l_body,
),
) +
l_trail
}
}
// elm-format Box.hs FunctionType: a function type in parentheses after
// a `->` is a term of its own (`typeParens ForLambda`), so the source
// parentheses stay.
let flattens = (n : @ast.Node[@ast.TypeAnnotation]) => {
n.value is FunctionTypeAnnotation(_, _) &&
!(self.layout is ElmFormat && self.type_parens(n) is Some(_))
}
while node.value is FunctionTypeAnnotation(left, right) &&
(first || flattens(node)) {
let after_arrow = moved
moved = match split {
Fit => self.after_token(left.range)
Join | Split => {
let next = self.token_after(left.range.end)
self.take_if(Trailing, left.range, c => !ends_before(c, next))
}
}
let l = self.type_parts(p.child("left", 0), level + 1, left, ArrowLeft)
d = if first {
let (l_lead, l_body, l_trail) = l
l_lead + l_body + l_trail
} else {
d + arrow(lead, after_arrow, l)
}
first = false
p = p.child("right", 0)
node = right
lead = if flattens(node) {
self.leading(node.range)
} else {
@pretty.empty()
}
}
if first {
self.type_doc(p, level + 1, node, AnyType)
} else {
// A signature's type has no `type_doc` of its own: its comments are
// taken here.
let own = self.leading(t.range)
let (l_lead, l_body, l_trail) = self.type_parts(
p,
level + 1,
node,
ArrowLeft,
)
let last = (l_lead, l_body, l_trail + comments_after(eol))
own + d + arrow(lead, moved, last) + self.trailing(t.range)
}
}