///|
/// The space before the node at `r`, with its leading comments. Block
/// comments that end on the node's line stay on the line: ` {- a -} x`.
/// Other comments go on lines of their own, indented by `indent`, and the
/// node starts the next line; then the result is true. `previous` is the
/// range of the node before it, if any.
fn Ctx::space_before(
self : Ctx,
previous : @ast.Range?,
r : @ast.Range,
indent : Int,
) -> (@pretty.Doc, Bool) {
let cs = self.take(Leading, r)
if cs.iter().any(c => ends_line(c, r.start)) {
(@pretty.nest(indent, comment_block(cs, before=true, after=true)), true)
} else {
let space = match previous {
Some(p) => self.after(p, " ")
None => @pretty.text(" ")
}
(space + comments_before(cs, r.start), false)
}
}
///|
/// `keyword Name a b` for a type declaration, with the comments of the
/// name and the generics (elm-format puts each one that has a comment on
/// its own line before it on lines of its own).
fn Ctx::type_head(
self : Ctx,
path : @syntax.NodePath,
keyword : String,
name : @ast.Node[String],
names : ArrayView[@ast.Node[String]],
) -> @pretty.Doc raise PrintError {
let (space, nested) = self.space_before(None, name.range, 4)
let mut d = @pretty.text(keyword) +
space +
self.with_comments(
name.range,
upper_name(path.child("name", 0), name.value),
)
let indent = if nested { 8 } else { 4 }
let mut previous = name.range
for i, g in names {
let (space, _) = self.space_before(Some(previous), g.range, indent)
d = d +
space +
self.with_comments(
g.range,
self.lower_name(path.child("generics", i), g.value),
)
previous = g.range
}
d
}
///|
/// The comments before the keyword of a type, type alias or port
/// declaration that has no documentation (`type {- a -} alias A`). They
/// are top-level body comments.
fn Ctx::before_keyword(
self : Ctx,
decl : @ast.Node[@ast.Declaration],
) -> Array[@ast.Node[String]] {
// A type alias with no documentation starts at its `type`: its comments
// before `alias` are its own (see `Ctx::alias_doc`).
let name = match decl.value {
CustomTypeDeclaration(t) if t.documentation is None => t.name.range
PortDeclaration(s) => s.name.range
_ => return []
}
let keyword = self.token_before(name.start)
self.take_if(Leading, name, c => ends_before(c, keyword))
}
///|
/// The documentation of a declaration, then the comments before its
/// keyword (the leading comments of its name that come before the keyword):
/// with documentation, a block between blank lines, as elm-format makes
/// them body comments. The keyword is the token `keyword_back` tokens
/// before the name (2 for the `type` of `type alias`).
fn Ctx::declaration_start(
self : Ctx,
path : @syntax.NodePath,
doc : @ast.Node[String]?,
name : @ast.Range,
keyword_back? : Int = 1,
) -> @pretty.Doc raise PrintError {
let docs = match doc {
Some(x) =>
self.documentation_doc(path.child("documentation", 0), x.value) +
self.trailing(x.range)
None => @pretty.empty()
}
let keyword = self.token_back(name.start, keyword_back)
let cs = self.take_if(Leading, name, c => ends_before(c, keyword))
match (doc, cs.is_empty()) {
(None, true) => @pretty.empty()
(Some(_), true) => docs + @pretty.hardline()
(None, false) => comment_block(cs) + @pretty.hardline()
(Some(_), false) =>
docs + line_breaks(4) + comment_block(cs) + line_breaks(3)
}
}
///|
fn Ctx::declaration_doc(
self : Ctx,
path : @syntax.NodePath,
level : Int,
d : @ast.Node[@ast.Declaration],
) -> @pretty.Doc raise PrintError {
check_level(path, level)
match d.value {
FunctionDeclaration(f) => self.function_doc(path, level, f)
AliasDeclaration(a) => self.alias_doc(path, level, a)
CustomTypeDeclaration(t) => {
guard !t.constructors.is_empty() else {
raise PrintError(path~, problem=NoConstructors)
}
let start = self.declaration_start(path, t.documentation, t.name.range)
let head = self.type_head(path, "type", t.name, t.generics)
let mut ctors = @pretty.empty()
for i, c in t.constructors {
let p = path.child("constructors", i)
// The comments before the `=` or `|` of a constructor: before the
// `=` on lines of their own; before a `|`, after the constructor
// before it, indented by 2 (elm-format).
let bar = self.token_before(c.range.start)
let before = self.take_if(Leading, c.range, x => ends_before(x, bar))
let lead = if i == 0 {
@pretty.hardline() + comment_block(before, after=true)
} else {
let mut d = @pretty.empty()
for x in before {
d = d + @pretty.nest(2, @pretty.hardline() + comment_doc(x))
}
d + @pretty.hardline()
}
// The comments after the `=` or `|`: on the line, or each followed
// by a line break to the column of the name.
let after = self.take(Leading, c.range)
let after_bar = if after.iter().any(x => ends_line(x, c.range.start)) {
let mut d = @pretty.empty()
for x in after {
d = d + comment_doc(x) + @pretty.nest(2, @pretty.hardline())
}
d
} else {
comments_before(after, c.range.start)
}
let name = c.value.name
let args = []
for j, a in c.value.arguments {
args.push(
self.type_doc(p.child("arguments", j), level + 1, a, TypeArg),
)
}
ctors = ctors +
lead +
@pretty.text(if i == 0 { "= " } else { "| " }) +
after_bar +
// elm-format Box.hs `Datatype`: the arguments go on their own
// lines when one of them is multi-line.
spaced(
self.join(),
self.with_comments(
name.range,
upper_name(p.child("name", 0), name.value),
),
args,
) +
self.trailing(c.range)
}
start + head + @pretty.nest(4, ctors)
}
// A port's `name` and `typeAnnotation` are fields of the declaration.
PortDeclaration(s) =>
self.declaration_start(path, None, s.name.range) +
@pretty.text("port") +
self.space_before(None, s.name.range, 4).0 +
self.signature_doc(path, level, s)
InfixDeclaration(i) => {
// elm-format pads the direction to 5 columns: `infix left 0 (|>) = apR`.
let direction = match i.direction.value {
Left => "left "
Right => "right"
Non => "non "
}
let precedence = i.precedence.value
// The precedence is not a node of the syntax tree (`NodeRef`), so the
// path is the declaration's.
guard precedence >= 0 && precedence <= 9 else {
raise PrintError(path~, problem=InvalidPrecedence(precedence))
}
let symbol = self.operator_symbol(
path.child("operator", 0),
i.operator.value,
)
let function = self.lower_name(
path.child("function", 0),
i.function.value,
)
let operator = i.operator.range
let before_operator = self.take(Leading, operator)
// The comments after the operator: before the `=` they follow the
// operator, after it they go before the function.
let (after_operator, after_equals) = self.trailing_split(operator)
let equals = self.token_before(i.function.range.start)
after_operator.append(
self.take_if(Leading, i.function.range, c => ends_before(c, equals)),
)
let before_function = [
..after_equals,
..self.take(Leading, i.function.range),
]
let breaks = before_operator.iter().any(c => ends_line(c, operator.start)) ||
after_operator.iter().any(c => off_line(c, operator)) ||
before_function.iter().any(c => ends_line(c, i.function.range.start))
if !breaks {
@pretty.text("infix " + direction + " " + precedence.to_string() + " ") +
comments_before(before_operator, operator.start) +
@pretty.text("(" + symbol + ")") +
comments_after(after_operator) +
@pretty.text(" = ") +
comments_before(before_function, i.function.range.start) +
function
} else {
// A comment that ends its line: elm-format puts each part on its
// own line.
@pretty.text("infix") +
@pretty.nest(
4,
@pretty.hardline() +
@pretty.text(direction.trim_end().to_owned()) +
@pretty.hardline() +
@pretty.text(precedence.to_string()) +
@pretty.hardline() +
comment_block(before_operator, after=true) +
@pretty.text("(" + symbol + ")") +
comment_block(after_operator, before=true) +
@pretty.hardline() +
@pretty.text("=") +
@pretty.hardline() +
comment_block(before_function, after=true) +
function,
)
}
}
Destructuring(pattern, value) => {
let moved = self.after_token(pattern.range)
// As in a let: a destructuring pattern is a term.
@pretty.nest(
4,
self.pattern_doc(
path.child("pattern", 0),
level + 1,
pattern,
PatternArg,
) +
self.separator(pattern.range, " =", moved),
) +
@pretty.nest(
4,
@pretty.hardline() +
self.expr_doc(path.child("expression", 0), level + 1, value, AnyExpr),
)
}
}
}
///|
/// A part of the module body: a block of regular comments or a
/// declaration (elm-format's `TopLevelStructure`).
priv enum BodyEntry {
Comments(Array[@ast.Node[String]])
/// `{--}`, the start of a comment trick.
Opener(@ast.Node[String])
/// `--}`, the end of a comment trick.
Closer(@ast.Node[String])
Decl(Int, @ast.Node[@ast.Declaration])
}
///|
/// Adds comments `cs` to the body `entries`: each comment trick opener
/// (`{--}`) and closer (`--}`) is an entry of its own (elm-format
/// Parse/Whitespace.hs `CommentTrickOpener`, `CommentTrickCloser`).
fn push_comments(
entries : Array[BodyEntry],
cs : Array[@ast.Node[String]],
) -> Unit {
let run = []
let flush = () => {
if !run.is_empty() {
entries.push(Comments(run.copy()))
run.clear()
}
}
for c in cs {
if c.value == "{--}" {
flush()
entries.push(Opener(c))
} else if is_line_comment(c) && comment_text(c.value) == "--}" {
flush()
entries.push(Closer(c))
} else {
run.push(c)
}
}
flush()
}
///|
/// The number of blank lines between two body entries (elm-format 0.8.7
/// `formatTopLevelBody`, with 2 lines between declarations).
fn blank_lines_between(a : BodyEntry, b : BodyEntry) -> Int {
// The cases in the order of elm-format's `spacer`: the first that
// matches decides.
match (a, b) {
(Opener(_), _) | (_, Closer(_)) => 0
(Comments(_), Comments(_)) => 0
(_, Comments(_)) => 3
(Decl(_, x), Decl(_, y)) =>
if x.value is InfixDeclaration(_) && y.value is InfixDeclaration(_) {
0
} else {
2
}
_ => 2
}
}
///|
/// Checks the parts of `file` that `normalize_file` moves (the items of the
/// module's exposing list and the imports), so that a `PrintError` names
/// their path in `file`, not in the normalized file.
fn Ctx::check_moved(self : Ctx, file : @ast.File) -> Unit raise PrintError {
let check = Ctx::new(self.dialect, layout=self.layout)
let root = @syntax.NodePath::root()
let path = root.child("moduleDefinition", 0).child("exposingList", 0)
if exposing_of(file.module_definition.value).value is Explicit(items) {
guard !items.is_empty() else {
raise PrintError(path~, problem=EmptyExposing)
}
for i, x in items {
ignore(check.expose_doc(path.child("explicit", i), x))
}
}
for i, imp in file.imports {
ignore(
check.import_doc(
root.child("imports", i),
imp,
ExposeComments::new(),
check.join(),
),
)
}
}
///|
/// The whole file in the elm-format layout. The `{-|` comments of
/// `File.comments` are printed: one that ends on the row before a port
/// declaration goes above that port, and the first other one is the
/// module documentation.
///
/// Regular comments at the top level go where elm-format 0.8.7 puts them
/// (`formatModule`, `formatImports`, `formatTopLevelBody`): comments
/// before the module header above it; comments in the header and between
/// imports above the imports (with no imports, also the comments before
/// the first declaration); other comments as blocks between declarations.
///
/// The file is printed in elm-format's order (see `normalize_file`); its
/// comments go with their nodes. A `PrintError` names a path in `source`.
fn Ctx::file_doc(
self : Ctx,
source : @ast.File,
) -> @pretty.Doc raise PrintError {
let root = @syntax.NodePath::root()
self.check_moved(source)
let file = normalize_header(source)
let port_docs : Array[@pretty.Doc?] = Array::make(
file.declarations.length(),
None,
)
let mut module_docs : @pretty.Doc? = None
for i, c in file.comments {
guard c.value.has_prefix("{-|") else { continue }
let doc = self.documentation_doc(root.child("comments", i), c.value)
match documented_port(file, c) {
Some(port) => port_docs[port] = Some(doc)
None => if module_docs is None { module_docs = Some(doc) }
}
}
let header = file.module_definition
let initial = self.take(Leading, header.range)
let mut d = if initial.is_empty() {
@pretty.empty()
} else {
comment_block(initial) + line_breaks(3)
}
let exposing = exposing_of(source.module_definition.value)
let exposing_comments = ExposeComments::new()
self.take_expose_comments(exposing, exposing_comments)
d = d +
self.module_doc(
root.child("moduleDefinition", 0),
header,
exposing_groups(file, self.list_split(exposing)),
exposing_comments,
)
if module_docs is Some(m) {
d = d + line_breaks(2) + m
}
// The comments of the import section, and the imports. The comments
// between imports are taken by the imports of the source; each import
// of the file takes the comments of its exposing list from the source
// imports that it merges.
let import_comments = self.take(Trailing, header.range)
let last_import = source.imports.length() - 1
let merged : Map[String, Array[@ast.Node[@ast.Import]]] = Map([])
for i, imp in source.imports {
import_comments.append(self.take(Leading, imp.range))
if i < last_import {
import_comments.append(self.take(Trailing, imp.range))
}
let key = module_key(imp.value.module_name.value)
match merged.get(key) {
Some(list) => list.push(imp)
None => merged[key] = [imp]
}
}
let imports = []
for i, imp in file.imports {
let sources = merged
.get(module_key(imp.value.module_name.value))
.unwrap_or([imp])
let comments = ExposeComments::new()
let after_name = []
for k in 1.. {
!is_line_comment(c)
}),
)
}
// The comments after `as` of an alias that a later import replaces.
// `Ctx::import_doc` would print them after the new alias.
let replaced = match
(sources[0].value.module_alias, imp.value.module_alias) {
(Some(old), Some(new)) if old.range != new.range =>
self.after_token(sources[0].value.module_name.range)
_ => []
}
// When `(..)` replaces the lists, their comments stay for the sweep
// below.
let mut split = self.join()
if imp.value.exposing_list is Some({ value: Explicit(_), .. }) {
for s in sources {
if s.value.exposing_list is Some(e) {
self.take_expose_comments(e, comments)
if self.list_split(e) is Split {
split = Split
}
}
}
}
imports.push(
self.import_doc(
root.child("imports", i),
imp,
comments,
split,
after_name~,
),
)
// The comments of the nodes that the merge removed (an alias, a
// module name) go before the imports.
import_comments.append(replaced)
if sources.length() > 1 ||
(
sources[0].value.module_alias is Some(_) &&
imp.value.module_alias is None
) {
for s in sources {
import_comments.append(self.take_within(s.range))
}
}
}
let file_range = @syntax.NodeRef::File(source, [][:]).range()
if imports.is_empty() {
// With no imports, the comments before the first declaration (or at the
// end of a module with no declarations) are in the import section.
match file.declarations.get(0) {
Some(first) => {
import_comments.append(self.take(Leading, first.range))
import_comments.append(self.before_keyword(first))
}
None => import_comments.append(self.take(Inner, file_range))
}
}
if !import_comments.is_empty() {
d = d + line_breaks(2) + comment_block(import_comments)
}
if !imports.is_empty() {
d = d + line_breaks(2) + @pretty.join(imports, @pretty.hardline())
}
// The module body.
let entries = []
if last_import >= 0 {
push_comments(
entries,
self.take(Trailing, source.imports[last_import].range),
)
}
let docs = []
for i, decl in file.declarations {
let before = self.take(Leading, decl.range)
before.append(self.before_keyword(decl))
push_comments(entries, before)
let mut doc = self.declaration_doc(root.child("declarations", i), 0, decl)
if port_docs[i] is Some(pd) {
doc = pd + @pretty.hardline() + doc
}
// elm-format keeps a comment after a custom type or a port on its line;
// after other declarations it is a body comment.
if decl.value is (CustomTypeDeclaration(_) | PortDeclaration(_)) {
doc = doc + self.trailing(decl.range)
}
docs.push(doc)
entries.push(Decl(i, decl))
push_comments(entries, self.take(Trailing, decl.range))
}
push_comments(entries, self.take(Inner, file_range))
for i, entry in entries {
let blank = if i == 0 {
if entry is Decl(_, _) {
2
} else {
3
}
} else {
blank_lines_between(entries[i - 1], entry)
}
d = d +
line_breaks(blank + 1) +
(match entry {
Comments(cs) => comment_block(cs)
Opener(c) | Closer(c) => comment_doc(c)
Decl(k, _) => docs[k]
})
}
d + @pretty.hardline()
}
///|
/// The comments `cs` as elm-format joins them (`formatComments`), and
/// whether they are one line; `None` when there are none.
fn comments_part(cs : Array[@ast.Node[String]]) -> (@pretty.Doc, Bool)? {
if cs.is_empty() {
None
} else {
Some((comment_box(cs), cs.iter().all(one_line)))
}
}
///|
/// Parts on one line, separated by spaces, when each is one line; else
/// each on its own line (elm-format `ElmStructure.spaceSepOrStack`). A
/// part is a doc and whether it is one line.
fn row_or_stack(parts : Array[(@pretty.Doc, Bool)]) -> (@pretty.Doc, Bool) {
let single = parts.iter().all(p => p.1)
let gap = if single { @pretty.text(" ") } else { @pretty.hardline() }
(@pretty.join(parts.map(p => p.0), gap), single)
}
///|
/// `first` and `rest` on one line when each is one line, else `first`
/// with each part of `rest` on its own line, indented by 4. With `join`,
/// the first part of `rest` stays on the line of `first` when both are
/// one line (elm-format `ElmStructure.application (FAJoinFirst JoinAll)`).
fn row_or_indented(
first : (@pretty.Doc, Bool),
rest : Array[(@pretty.Doc, Bool)],
join? : Bool = false,
) -> (@pretty.Doc, Bool) {
if first.1 && rest.iter().all(p => p.1) {
let parts = [first]
parts.append(rest)
return row_or_stack(parts)
}
let mut d = first.0
for i, p in rest {
d = if i == 0 && join && first.1 && p.1 {
d + @pretty.text(" ") + p.0
} else {
d + @pretty.nest(4, @pretty.hardline() + p.0)
}
}
(d, false)
}
///|
/// A type alias, as elm-format 0.8.7 writes it (Box.hs `TypeAlias`):
/// `type`, the comments before `alias` and `alias`, then the name with its
/// arguments and their comments (`formatNameWithArgs`), then ` =` on the
/// line when that head is one line, else `=` on a line of its own, and the
/// type on the next line after the comments before it, each on its own
/// line (`formatPreCommentedStack`).
fn Ctx::alias_doc(
self : Ctx,
path : @syntax.NodePath,
level : Int,
a : @ast.TypeAlias,
) -> @pretty.Doc raise PrintError {
let start = self.declaration_start(
path,
a.documentation,
a.name.range,
keyword_back=2,
)
let name = a.name
let alias_at = self.token_before(name.range.start)
let pre_alias = self.take_if(Leading, name.range, c => {
ends_before(c, alias_at)
})
let pre_name = self.take(Leading, name.range)
// The comments before each argument: those after the part before it,
// then its own.
let args = []
let mut previous = name.range
let name_doc = upper_name(path.child("name", 0), name.value)
for i, g in a.generics {
let pre = self.take(Trailing, previous)
pre.append(self.take(Leading, g.range))
let d = self.lower_name(path.child("generics", i), g.value)
args.push(
match comments_part(pre) {
Some(c) => row_or_stack([c, (d, true)])
None => (d, true)
},
)
previous = g.range
}
let t = a.type_annotation
let equals = self.token_after(previous.end)
let post = self.take_if(Trailing, previous, c => ends_before(c, equals))
post.append(self.take_if(Leading, t.range, c => ends_before(c, equals)))
let body_pre = self.take(Trailing, previous)
body_pre.append(self.take(Leading, t.range))
// `formatNameWithArgs`, then `formatCommented` with the comments before
// the name and before the `=`.
let name_args = row_or_indented((name_doc, true), args)
let parts = []
if comments_part(pre_name) is Some(c) {
parts.push(c)
}
parts.push(name_args)
if comments_part(post) is Some(c) {
parts.push(c)
}
let alias_part = match comments_part(pre_alias) {
Some(c) => row_or_stack([c, (@pretty.text("alias"), true)])
None => (@pretty.text("alias"), true)
}
let (head, one_line) = row_or_indented(
(@pretty.text("type"), true),
[alias_part, row_or_stack(parts)],
join=true,
)
let body = comment_block(body_pre, after=true) +
self.type_doc(path.child("typeAnnotation", 0), level + 1, t, AnyType)
start +
head +
(if one_line {
@pretty.text(" =")
} else {
@pretty.nest(4, @pretty.hardline() + @pretty.text("="))
}) +
@pretty.nest(4, @pretty.hardline() + body)
}