///|
/// 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
}
///|
/// The type at `path` with its comments. It runs on the work stack
/// (`Ctx::run`), so nested types do not recurse: a type of any depth
/// prints on every target.
fn Ctx::type_doc(
self : Ctx,
path : @syntax.NodePath,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> @pretty.Doc raise PrintError {
self.run(Type(path, t, at))
}
///|
/// The three items of a type: its leading comments, its body and its
/// trailing comments, in that order (an arrow chain places them apart).
/// Build them when their `Parts` item is popped, not while the plan of
/// the type above is built: the leaves then run, and errors come, in
/// source order, and nothing recurses per level.
fn Ctx::type_parts_plan(
self : Ctx,
path : @syntax.NodePath,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> Array[Work] raise PrintError {
// The comments in the type's own parentheses lie inside its range, so
// taking them before its leading comments changes nothing.
let paren_comments = self.type_paren_comments(t)
let (items, combine) = match paren_comments {
Some(_) => self.type_body_plan(path, t, AnyType)
None => self.type_body_plan(path, t, at)
}
let body = Plan(items, docs => {
let d = combine(docs)
match paren_comments {
Some((pre, post)) => {
// elm-format Parse/Type.hs tuple: parentheses that hold comments
// are `TypeParens`, printed as `parens (formatCommented ...)`.
let (inner, flat) = commented(pre, d, post)
@pretty.group(
@pretty.align(
@pretty.text("(") +
@pretty.nest(1, inner) +
(if flat { @pretty.softline() } else { @pretty.hardline() }) +
@pretty.text(")"),
),
)
}
None => d
}
})
[Leaf(() => self.leading(t.range)), body, Leaf(() => 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 plan of a type without its own comments. The leaves take comments
/// in source order; a nested type is a `Type` work item.
fn Ctx::type_body_plan(
self : Ctx,
path : @syntax.NodePath,
t : @ast.Node[@ast.TypeAnnotation],
at : TypeAt,
) -> (Array[Work], (Array[@pretty.Doc]) -> @pretty.Doc) raise PrintError {
match t.value {
GenericType(name) => done(self.lower_name(path, name))
Unit =>
leaf(() => {
@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 done(head) }
let items : Array[Work] = []
for i, a in args {
items.push(Type(path.child("args", i), 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,
})
(
items,
docs => {
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 count = items.length()
let work : Array[Work] = []
for i, x in items {
if i > 0 {
// elm-format Box.hs TupleType (`formatC2Eol`): the comments before
// a `,` go under the item before it.
work.push(
Leaf(() => {
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() {
@pretty.empty()
} else {
@pretty.hardline() + comment_box(cs)
}
}),
)
}
work.push(Type(path.child("values", i), x, AnyType))
}
work.push(Leaf(() => @pretty.nest(2, self.inner_close(t.range))))
// elm-format Parse/Type.hs tuple (`checkMultiline`).
let split = self.split_within(t.range)
(
work,
docs => {
// docs: item 0, then (comma comments, item) per later item, then
// the inner comments.
let ds = [docs[0]]
for i in 1.. {
let (work, combine_fields) = self.record_fields_plan(
path, "value", fields,
)
let last = fields.last().map(f => f.range)
work.push(
Leaf(() => {
match last {
Some(r) => self.inner(t.range, last=Some(r))
None => self.inner_alone(t.range, brace=true)
}
}),
)
// elm-format Parse/Type.hs record (`checkMultiline`).
let split = self.split_within(t.range)
(
work,
docs => {
let (ds, leads) = combine_fields(docs, 0)
sequence(split, "{", "}", ds, leads~, inner=docs[docs.length() - 1])
},
)
}
GenericRecord(name, fields) => {
guard !fields.value.is_empty() else {
raise PrintError(path~, problem=NoFields)
}
let work : Array[Work] = [
Leaf(() => {
// 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)
})
@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 (field_work, combine_fields) = self.record_fields_plan(
path,
"values",
fields.value,
)
work.append(field_work)
let last = fields.value.last().map(f => f.range)
work.push(Leaf(() => self.inner(t.range, last~)))
// elm-format Parse/Type.hs record (`checkMultiline`).
let split = self.split_within(t.range)
(
work,
docs => {
let (ds, leads) = combine_fields(docs, 1)
extension(split, docs[0], ds, leads~, inner=docs[docs.length() - 1])
},
)
}
FunctionTypeAnnotation(_, _) => {
let (work, combine) = self.arrow_chain_plan(path, t)
(
work,
docs => {
let d = @pretty.group(combine(docs))
if at is AnyType {
d
} else {
parens(self.join(), d)
}
},
)
}
}
}
///|
/// The comments and docs of one record field that are taken before its
/// type prints (see `Ctx::record_fields_plan`).
priv struct FieldParts {
/// The comments before the `,` of the field (see `Ctx::before_item`).
lead_before : @pretty.Doc
/// The comments between the label and the `:`.
label_post : Array[@ast.Node[String]]
/// The comments after the `,` (see `Ctx::after_comma`).
lead : @pretty.Doc
/// The comments after the `:`.
moved : Array[@ast.Node[String]]
label : @pretty.Doc
/// The comments after the type, printed after the field.
value_trail : Array[@ast.Node[String]]
}
///|
/// The plan of `a : A` for each field, at `path.child(field_name, i)`:
/// three items per field (the comments taken before the type, the type,
/// the comments before the next `,`). The combine function reads the
/// docs from `start` and gives the field docs and the comments before each
/// `,` (see `Ctx::before_item`).
fn Ctx::record_fields_plan(
self : Ctx,
path : @syntax.NodePath,
field_name : String,
fields : @ast.RecordDefinition,
) -> (
Array[Work],
(Array[@pretty.Doc], Int32) -> (Array[@pretty.Doc], Array[@pretty.Doc]),
) {
let work : Array[Work] = []
let parts : Array[FieldParts] = []
let splits : Array[Split] = []
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 }
let value = f.value.type_annotation
// elm-format Parse/Common.hs pair (`checkMultiline`): a line break
// from the label to the end of the type.
splits.push(
self.split_within({ start: name.range.start, end: value.range.end, }),
)
work.push(
Leaf(() => {
let lead_before = 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, value.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, value.range)
parts.push({ lead_before, label_post, lead, moved, label, value_trail, })
@pretty.empty()
}),
)
work.push(Type(p.child("typeAnnotation", 0), value, AnyType))
let next = fields.get(i + 1).map(n => n.range)
work.push(Leaf(() => self.trailing_before_comma(f.range, next)))
}
(
work,
(docs, start) => {
let ds = []
let leads = []
for i in 0.. b -> c` as a doc (see `Ctx::arrow_chain_plan`); for a signature,
/// which has no `Type` item of its own.
fn Ctx::arrow_chain(
self : Ctx,
path : @syntax.NodePath,
t : @ast.Node[@ast.TypeAnnotation],
eol~ : Array[@ast.Node[String]] = [],
) -> @pretty.Doc raise PrintError {
let (items, combine) = self.arrow_chain_plan(path, t, eol~)
self.run(Plan(items, combine))
}
///|
/// The plan of `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_plan(
self : Ctx,
path : @syntax.NodePath,
t : @ast.Node[@ast.TypeAnnotation],
eol~ : Array[@ast.Node[String]] = [],
) -> (Array[Work], (Array[@pretty.Doc]) -> @pretty.Doc) {
let split = self.split_within(t.range)
// `-> 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(_))
}
let work : Array[Work] = []
// Per step: the comments after the `->` that trail the type before it
// (taken before that type prints), and the leading comments of the
// flattened type after the `->`.
let moved : Array[Array[@ast.Node[String]]] = []
let leads : Array[@pretty.Doc] = []
let mut node = t
let mut p = path
let mut first = true
let mut steps = 0
while node.value is FunctionTypeAnnotation(left, right) &&
(first || flattens(node)) {
work.push(
Leaf(() => {
moved.push(
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))
}
},
)
@pretty.empty()
}),
)
work.push(Parts(p.child("left", 0), left, ArrowLeft))
first = false
p = p.child("right", 0)
node = right
let next = node
if flattens(next) {
work.push(
Leaf(() => {
leads.push(self.leading(next.range))
@pretty.empty()
}),
)
} else {
work.push(
Leaf(() => {
leads.push(@pretty.empty())
@pretty.empty()
}),
)
}
steps += 1
}
if first {
return ([Parts(p, node, AnyType)], docs => docs[0] + docs[1] + docs[2])
}
// A signature's type has no `Type` item of its own: its comments are
// taken here.
work.push(Leaf(() => self.leading(t.range)))
work.push(Parts(p, node, ArrowLeft))
work.push(Leaf(() => self.trailing(t.range)))
(
work,
docs => {
// Per step five docs: the moved marker, lead, body, trail, the lead
// marker. Then the chain's own leading comments, the last type's
// three docs and the chain's trailing comments.
let parts = (k : Int32) => {
(docs[5 * k + 1], docs[5 * k + 2], docs[5 * k + 3])
}
let (f_lead, f_body, f_trail) = parts(0)
let mut d = f_lead + f_body + f_trail
for k in 1..