diff --git a/.gitignore b/.gitignore index b067edd..dab5446 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /Manifest.toml +*.cov diff --git a/src/Runic.jl b/src/Runic.jl index c5f3612..f7a74a3 100644 --- a/src/Runic.jl +++ b/src/Runic.jl @@ -108,6 +108,9 @@ mutable struct Context debug::Bool check::Bool diff::Bool + # Global state + indent_level::Int # track (hard) indentation level + call_depth::Int # track call-depth level for debug printing # Current state # node::Union{Node, Nothing} prev_sibling::Union{Node, Nothing} @@ -138,9 +141,12 @@ function Context( # Debug mode enforces verbose and assert verbose = debug ? true : verbose assert = debug ? true : assert + indent_level = 0 + call_depth = 0 + prev_sibling = next_sibling = nothing return Context( - src_str, src_tree, src_io, fmt_io, fmt_tree, - quiet, verbose, assert, debug, check, diff, nothing, nothing, + src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check, + diff, call_depth, indent_level, prev_sibling, next_sibling, ) end @@ -180,6 +186,8 @@ function format_node_with_kids!(ctx::Context, node::Node) return nothing end + ctx.call_depth += 1 + # Keep track of the siblings on this stack prev_sibling = ctx.prev_sibling next_sibling = ctx.next_sibling @@ -240,6 +248,7 @@ function format_node_with_kids!(ctx::Context, node::Node) # Reset the siblings ctx.prev_sibling = prev_sibling ctx.next_sibling = next_sibling + ctx.call_depth -= 1 # Return a new node if any of the kids changed if any_kid_changed return make_node(node, kids′) @@ -259,7 +268,18 @@ Format a node. Return values: function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} node_kind = kind(node) + # Not that two separate `if`s are used here because a node like `else` can be both + # dedent and indent + if has_tag(node, TAG_INDENT) + ctx.indent_level += 1 + end + if has_tag(node, TAG_DEDENT) + ctx.indent_level -= 1 + end + # Go through the runestone and apply transformations. + ctx.call_depth += 1 + @return_something insert_delete_mark_newlines(ctx, node) @return_something trim_trailing_whitespace(ctx, node) @return_something format_hex_literals(ctx, node) @return_something format_oct_literals(ctx, node) @@ -268,6 +288,8 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} @return_something spaces_around_assignments(ctx, node) @return_something no_spaces_around_colon_etc(ctx, node) @return_something for_loop_use_in(ctx, node) + @return_something four_space_indent(ctx, node) + ctx.call_depth -= 1 # If the node is unchanged at this point, just keep going. @@ -346,6 +368,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} node_kind === K"where" || node_kind === K"while" ) + @assert !JuliaSyntax.is_trivia(node) node′ = format_node_with_kids!(ctx, node) @assert node′ !== nullnode return node′ diff --git a/src/chisels.jl b/src/chisels.jl index d8aecbd..c4c6874 100644 --- a/src/chisels.jl +++ b/src/chisels.jl @@ -67,6 +67,42 @@ end const TAG_INDENT = TagType(1) << 0 # This node is responsible for decrementing the indentation level const TAG_DEDENT = TagType(1) << 1 +# This (NewlineWs) node is the last one before a TAG_DEDENT +const TAG_PRE_DEDENT = TagType(1) << 2 +# This (NewlineWs) node is a line continuation +const TAG_LINE_CONT = UInt32(1) << 31 + +function add_tag(node::Node, tag::TagType) + @assert is_leaf(node) + return Node(head(node), span(node), node.kids, node.tags | tag) +end + +function tag_leading_newline(node::Node, tag::TagType) + if is_leaf(node) + if kind(node) === K"NewlineWs" && !has_tag(node, tag) + return add_tag(node, tag) + else + return nothing + end + else + kids = verified_kids(node) + if length(kids) < 1 + return nothing + end + # Skip over whitespace + comment which can mask the newline + idx = findfirst(x -> !(kind(x) in KSet"Whitespace Comment"), kids) + if idx === nothing + return nothing + end + kid′ = tag_leading_newline(kids[idx], tag) + if kid′ === nothing + return nothing + else + kids[idx] = kid′ + return node + end + end +end function has_tag(node::Node, tag::TagType) return node.tags & tag != 0 @@ -80,12 +116,18 @@ function stringify_tags(node::Node) if has_tag(node, TAG_DEDENT) write(io, "dedent,") end + if has_tag(node, TAG_PRE_DEDENT) + write(io, "pre-dedent,") + end + if has_tag(node, TAG_LINE_CONT) + write(io, "line-cont.,") + end truncate(io, max(0, position(io) - 1)) # Remove trailing comma return String(take!(io)) end # Create a new node with the same head but new kids -function make_node(node::Node, kids′::Vector{Node}, tags = TagType(0)) +function make_node(node::Node, kids′::Vector{Node}, tags = node.tags) span′ = mapreduce(span, +, kids′; init = 0) return Node(head(node), span′, kids′, tags) end @@ -135,7 +177,7 @@ end # Extract the operator of an infix op call node function infix_op_call_op(node::Node) - @assert is_infix_op_call(node) + @assert is_infix_op_call(node) || kind(node) === K"||" kids = verified_kids(node) first_operand_index = findfirst(!JuliaSyntax.is_whitespace, kids) op_index = findnext(JuliaSyntax.is_operator, kids, first_operand_index + 1) diff --git a/src/runestone.jl b/src/runestone.jl index 1da505f..df2d82c 100644 --- a/src/runestone.jl +++ b/src/runestone.jl @@ -470,3 +470,462 @@ function for_loop_use_in(ctx::Context, node::Node) seek(ctx.fmt_io, pos) # reset return node′ end + +# This function materialized all indentations marked by `insert_delete_mark_newlines`. +function four_space_indent(ctx::Context, node::Node) + kind(node) === K"NewlineWs" || return nothing + next_sibling_kind(ctx) === K"NewlineWs" && return + bytes = read_bytes(ctx, node) + @assert !in(UInt8('\r'), bytes) + @assert bytes[1] == UInt8('\n') + indent_level = ctx.indent_level + # TAG_PRE_DEDENT means this is the newline just before an `end` + if has_tag(node, TAG_PRE_DEDENT) + indent_level -= 1 + end + # TAG_LINE_CONT is a "soft" indentation + if has_tag(node, TAG_LINE_CONT) + indent_level += 1 + end + spn′ = 1 + 4 * indent_level + spn = span(node) + if spn == spn′ + return nothing + end + resize!(bytes, spn′) + fill!(@view(bytes[2:end]), UInt8(' ')) + replace_bytes!(ctx, bytes, spn) + node′ = Node(head(node), spn′, (), node.tags) + return node′ +end + +# This function tags the `function`/`macro` and `end` keywords as well as the trailing +# newline of the function/macro body. +function indent_function_or_macro(ctx::Context, node::Node) + kids = verified_kids(node) + any_kid_changed = false + # First node is the function/macro keyword + func_idx = 1 + func_node = kids[func_idx] + @assert is_leaf(func_node) && kind(func_node) in KSet"function macro" + if !has_tag(func_node, TAG_INDENT) + kids[func_idx] = add_tag(func_node, TAG_INDENT) + any_kid_changed = true + end + # Second node is the space between keyword and name + # TODO: Make sure there is just a single space + space_idx = 2 + space_node = kids[space_idx] + @assert is_leaf(space_node) && kind(space_node) === K"Whitespace" + # Third node is the signature (call/where/::) + sig_idx = 3 + sig_node = kids[sig_idx] + @assert !is_leaf(sig_node) && kind(sig_node) in KSet"call where ::" + # Fourth node is the function/macro body block. + block_idx = 4 + block_node′ = indent_block(ctx, kids[block_idx]) + if block_node′ !== nothing + kids[block_idx] = block_node′ + any_kid_changed = true + end + # Fifth node is the closing end keyword + end_idx = 5 + end_node = kids[end_idx] + @assert is_leaf(end_node) && kind(end_node) === K"end" + if !has_tag(end_node, TAG_DEDENT) + kids[end_idx] = add_tag(end_node, TAG_DEDENT) + any_kid_changed = true + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + +function indent_let(ctx::Context, node::Node) + kids = verified_kids(node) + any_kid_changed = false + # First node is the let keyword + let_idx = 1 + let_node = kids[let_idx] + @assert is_leaf(let_node) && kind(let_node) === K"let" + if !has_tag(let_node, TAG_INDENT) + kids[let_idx] = add_tag(let_node, TAG_INDENT) + any_kid_changed = true + end + # Second node is the variables block (will be soft-indented by the assignments pass) + vars_idx = 2 + vars_node = kids[vars_idx] + @assert !is_leaf(vars_node) && kind(vars_node) === K"block" + @assert kind(last_leaf(vars_node)) !== "NewlineWs" + # Third node is the NewlineWs before the block + ln_idx = 3 + ln_node = kids[ln_idx] + @assert is_leaf(ln_node) && kind(ln_node) === K"NewlineWs" + # Fourth node is the function body block. + block_idx = 4 + block_node = kids[block_idx] + @assert !is_leaf(block_node) && kind(block_node) === K"block" + block_node′ = indent_block(ctx, block_node) + if block_node′ !== nothing + kids[block_idx] = block_node′ + any_kid_changed = true + end + # Fifth node is the closing end keyword + end_idx = 5 + @assert is_leaf(kids[end_idx]) && kind(kids[end_idx]) === K"end" + if !has_tag(kids[end_idx], TAG_DEDENT) + kids[end_idx] = add_tag(kids[end_idx], TAG_DEDENT) + any_kid_changed = true + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + +# TODO: Reuse indent_block? +function indent_begin(ctx::Context, node::Node) + kids = verified_kids(node) + any_kid_changed = false + # First node is the begin keyword + begin_idx = 1 + begin_node = kids[begin_idx] + @assert is_leaf(begin_node) && kind(begin_node) === K"begin" + if !has_tag(begin_node, TAG_INDENT) + kids[begin_idx] = add_tag(begin_node, TAG_INDENT) + any_kid_changed = true + end + # Second node is the newline + ln_idx = 2 + ln_node = kids[ln_idx] + @assert is_leaf(ln_node) && kind(ln_node) === K"NewlineWs" + # After the NewlineWs node we skip over all kids until the end + end_idx = findlast(x -> kind(x) === K"end", kids) + @assert end_idx == lastindex(kids) # ?? + # Tag last newline as pre-dedent + ln_idx = end_idx - 1 + ln_node = kids[ln_idx] + if kind(ln_node) === K"NewlineWs" + if !has_tag(ln_node, TAG_PRE_DEDENT) + kids[ln_idx] = add_tag(ln_node, TAG_PRE_DEDENT) + any_kid_changed = true + end + end + end_node = kids[end_idx] + @assert is_leaf(end_node) && kind(end_node) === K"end" + if !has_tag(end_node, TAG_DEDENT) + kids[end_idx] = add_tag(end_node, TAG_DEDENT) + any_kid_changed = true + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + +# TODO: This needs to be reworked to handle non-standard cases like, for example, one-liners +# of the form `if x y end`. For now we only handle the standard case and ignore the rest. +function indent_block(::Context, node::Node) + @assert kind(node) === K"block" && !is_leaf(node) + kids = verified_kids(node) + any_kid_changed = false + # Expect a NewlineWs node at the end of the block (otherwise the closing `end` is not on + # a separate line). + trailing_idx = findlast(x -> kind(x) === K"NewlineWs", kids) + if trailing_idx === nothing || trailing_idx != lastindex(kids) + return nothing + elseif !has_tag(kids[trailing_idx], TAG_PRE_DEDENT) + kids[trailing_idx] = add_tag(kids[trailing_idx], TAG_PRE_DEDENT) + any_kid_changed = true + end + # Look for a leading NewlineWs node + leading_idx = findfirst(x -> kind(x) === K"NewlineWs", kids) + if leading_idx !== nothing && leading_idx < trailing_idx + # TODO: Forgot why we check for this. I think it is only necessary if we want to + # split a one-liner into multiple lines. + # return nothing + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + +function indent_if(ctx::Context, node::Node) + @assert kind(node) in KSet"if elseif" + @assert !is_leaf(node) + kids = verified_kids(node) + any_kid_changed = false + # First node is either `if` or `elseif` (when called recursively) + if_idx = 1 + if_node = kids[if_idx] + @assert is_leaf(kids[if_idx]) && kind(if_node) in KSet"if elseif" + if !has_tag(if_node, TAG_INDENT) + if_node = add_tag(if_node, TAG_INDENT) + any_kid_changed = true + end + if kind(node) === K"elseif" && !has_tag(if_node, TAG_DEDENT) + if_node = add_tag(if_node, TAG_DEDENT) + any_kid_changed = true + end + kids[if_idx] = if_node + # Look for the condition node + cond_idx = findnext(!JuliaSyntax.is_whitespace, kids, if_idx + 1)::Int + if cond_idx != if_idx + 1 + # TODO: Trim whitespace between the keyword and the condition. It may exist as a + # separate leaf, or hidden in the condition node. + end + cond_node = kids[cond_idx] + @assert kind(last_leaf(cond_node)) !== "NewlineWs" + # Fourth node is the body block. + block_idx = findnext(!JuliaSyntax.is_whitespace, kids, cond_idx + 1)::Int + @assert block_idx == cond_idx + 1 + block_node′ = indent_block(ctx, kids[block_idx]) + if block_node′ !== nothing + kids[block_idx] = block_node′ + any_kid_changed = true + end + # Check for elseif + elseif_idx = findnext(x -> kind(x) === K"elseif", kids, block_idx + 1) + if elseif_idx !== nothing + @assert !is_leaf(kids[elseif_idx]) && kind(kids[elseif_idx]) === K"elseif" + elseif_node′ = indent_if(ctx, kids[elseif_idx]) + if elseif_node′ !== nothing + kids[elseif_idx] = elseif_node′ + any_kid_changed = true + end + end + # Check for else + else_idx = findnext(x -> kind(x) === K"else", kids, something(elseif_idx, block_idx) + 1) + if else_idx !== nothing + @assert is_leaf(kids[else_idx]) && kind(kids[else_idx]) === K"else" + else_node = kids[else_idx] + if !has_tag(else_node, TAG_INDENT) + else_node = add_tag(else_node, TAG_INDENT) + any_kid_changed = true + end + if !has_tag(else_node, TAG_DEDENT) + else_node = add_tag(else_node, TAG_DEDENT) + any_kid_changed = true + end + kids[else_idx] = else_node + else_block_idx = findnext(!JuliaSyntax.is_whitespace, kids, else_idx + 1)::Int + @assert kind(kids[else_block_idx]) === K"block" + else_block′ = indent_block(ctx, kids[else_block_idx]) + if else_block′ !== nothing + kids[else_block_idx] = else_block′ + any_kid_changed = true + end + end + # Check for end + end_idx = findnext(x -> kind(x) === K"end", kids, something(else_idx, elseif_idx, block_idx) + 1) + @assert (kind(node) === K"elseif") == (end_idx === nothing) + if end_idx !== nothing + @assert is_leaf(kids[end_idx]) && kind(kids[end_idx]) === K"end" + if !has_tag(kids[end_idx], TAG_DEDENT) + kids[end_idx] = add_tag(kids[end_idx], TAG_DEDENT) + any_kid_changed = true + end + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + +function indent_call(ctx::Context, node::Node) + @assert kind(node) === K"call" + return indent_paren(ctx, node) +end + + +function indent_newlines_between_indices( + ctx::Context, node::Node, open_idx::Int, close_idx::Int; + indent_closing_token::Bool = false, + ) + kids = verified_kids(node) + any_kid_changed = false + for i in open_idx:close_idx + kid = kids[i] + this_kid_changed = false + # Skip the newline just before the closing token for e.g. (...\n) + # (indent_closing_token = false) but not in e.g. `a+\nb` (indent_closing_token = + # true) where the closing token is part of the expression itself. + if !indent_closing_token && i == close_idx - 1 && kind(kid) === K"NewlineWs" + continue + end + # Tag all direct NewlineWs kids + if kind(kid) === K"NewlineWs" && !has_tag(kid, TAG_LINE_CONT) + kid = add_tag(kid, TAG_LINE_CONT) + this_kid_changed = true + end + # NewlineWs nodes can also hide as the first leaf of a node, tag'em. + kid′ = tag_leading_newline(kid, TAG_LINE_CONT) + if kid′ !== nothing + kid = kid′ + this_kid_changed = true + end + if this_kid_changed + kids[i] = kid + end + any_kid_changed |= this_kid_changed + end + @assert verified_kids(node) === kids + return any_kid_changed ? node : nothing +end + + +# Mark opening and closing parentheses, in a call or a tuple, with indent and dedent tags. +function indent_paren(ctx::Context, node::Node) + @assert kind(node) in KSet"call tuple parens" + kids = verified_kids(node) + opening_paren_idx = findfirst(x -> kind(x) === K"(", kids)::Int + closing_paren_idx = findnext(x -> kind(x) === K")", kids, opening_paren_idx + 1)::Int + return indent_newlines_between_indices(ctx, node, opening_paren_idx, closing_paren_idx) +end + +# Insert line-continuation nodes instead of bumping the indent level. +function indent_op_call(ctx::Context, node::Node) + kids = verified_kids(node) + first_operand_idx = findfirst(!JuliaSyntax.is_whitespace, kids)::Int + last_operand_idx = findlast(!JuliaSyntax.is_whitespace, kids)::Int + return indent_newlines_between_indices( + ctx, node, first_operand_idx, last_operand_idx; indent_closing_token = true, + ) +end + +function indent_loop(ctx, node) + @assert kind(node) in KSet"for while" + kids = verified_kids(node) + any_kid_changed = false + for_idx = findfirst(x -> kind(x) in KSet"for while", kids)::Int + if !has_tag(kids[for_idx], TAG_INDENT) + kids[for_idx] = add_tag(kids[for_idx], TAG_INDENT) + any_kid_changed = true + end + block_idx = findnext(x -> kind(x) === K"block", kids, for_idx + 1)::Int + block_node′ = indent_block(ctx, kids[block_idx]) + if block_node′ !== nothing + kids[block_idx] = block_node′ + any_kid_changed = true + end + end_idx = findlast(x -> kind(x) === K"end", kids)::Int + if !has_tag(kids[end_idx], TAG_DEDENT) + kids[end_idx] = add_tag(kids[end_idx], TAG_DEDENT) + any_kid_changed = true + end + return any_kid_changed ? node : nothing +end + +function indent_tuple(ctx, node) + @assert kind(node) === K"tuple" + return indent_paren(ctx, node) +end + +function indent_parens(ctx, node) + @assert kind(node) in KSet"parens" + return indent_paren(ctx, node) +end + +function indent_parameters(ctx, node) + kids = verified_kids(node) + # TODO: This is always here? + semicolon_idx = findfirst(x -> kind(x) === K";", kids)::Int + last_non_ws_idx = findlast(!JuliaSyntax.is_whitespace, kids)::Int + return indent_newlines_between_indices( + ctx, node, semicolon_idx, last_non_ws_idx; indent_closing_token = true, + ) +end + +function indent_struct(ctx, node) + @assert kind(node) === K"struct" + kids = verified_kids(node) + any_kid_changed = false + struct_idx = findfirst(!JuliaSyntax.is_whitespace, kids)::Int + @assert kind(kids[struct_idx]) in KSet"mutable struct" + if !has_tag(kids[struct_idx], TAG_INDENT) + kids[struct_idx] = add_tag(kids[struct_idx], TAG_INDENT) + any_kid_changed = true + end + block_idx = findnext(x -> kind(x) === K"block", kids, struct_idx + 1)::Int + block_node′ = indent_block(ctx, kids[block_idx]) + if block_node′ !== nothing + kids[block_idx] = block_node′ + any_kid_changed = true + end + end_idx = findlast(x -> kind(x) === K"end", kids)::Int + if !has_tag(kids[end_idx], TAG_DEDENT) + kids[end_idx] = add_tag(kids[end_idx], TAG_DEDENT) + any_kid_changed = true + end + return any_kid_changed ? node : nothing +end + +function indent_short_circuit(ctx, node) + return indent_op_call(ctx, node) +end + +# TODO: This function can be used for more things than just indent_using I think. Perhaps +# with a max_depth parameter. +function continue_all_newlines(ctx, node) + if is_leaf(node) + if kind(node) === K"NewlineWs" && !has_tag(node, TAG_LINE_CONT) + return add_tag(node, TAG_LINE_CONT) + else + return nothing + end + else + any_kid_changed = false + kids = verified_kids(node) + for (i, kid) in pairs(kids) + kid′ = continue_all_newlines(ctx, kid) + if kid′ !== nothing + kids[i] = kid′ + any_kid_changed = true + end + end + return any_kid_changed ? node : nothing + end +end + +function indent_using_import_export(ctx, node) + @assert kind(node) in KSet"using import export" + return continue_all_newlines(ctx, node) +end + +function indent_assignment(ctx, node) + kids = verified_kids(node) + # Also catches for loop specifications (but at this point we have normalized `=` and `∈` + # to `in`). + op_idx = findfirst(x -> is_assignment(x) || kind(x) === K"in", kids)::Int + last_non_ws_idx = findlast(!JuliaSyntax.is_whitespace, kids)::Int + return indent_newlines_between_indices( + ctx, node, op_idx, last_non_ws_idx; indent_closing_token = true, + ) +end + +function insert_delete_mark_newlines(ctx::Context, node::Node) + if is_leaf(node) + return nothing + elseif kind(node) in KSet"function macro" + return indent_function_or_macro(ctx, node) + elseif kind(node) === K"if" + return indent_if(ctx, node) + elseif kind(node) === K"let" + return indent_let(ctx, node) + elseif kind(node) === K"block" && length(verified_kids(node)) > 0 && kind(verified_kids(node)[1]) === K"begin" + return indent_begin(ctx, node) + elseif kind(node) === K"call" && flags(node) == 0 + return indent_call(ctx, node) + elseif is_infix_op_call(node) + return indent_op_call(ctx, node) + elseif kind(node) in KSet"for while" + return indent_loop(ctx, node) + elseif kind(node) === K"tuple" + return indent_tuple(ctx, node) + elseif kind(node) === K"struct" + return indent_struct(ctx, node) + elseif kind(node) === K"parens" + return indent_parens(ctx, node) + elseif kind(node) in KSet"|| &&" + return indent_short_circuit(ctx, node) + elseif kind(node) in KSet"using import export" + return indent_using_import_export(ctx, node) + elseif is_assignment(node) + return indent_assignment(ctx, node) + elseif kind(node) === K"parameters" + return indent_parameters(ctx, node) + end + return nothing +end diff --git a/test/runtests.jl b/test/runtests.jl index 0e9e1de..065e151 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -168,10 +168,10 @@ end "$(sp)sin(α) $(op) cos(β) $(op) tan(γ)$(sp)" # a op \n b @test format_string("$(sp)a$(sp)$(op)$(sp)\nb$(sp)") == - "$(sp)a $(op)\nb$(sp)" + "$(sp)a $(op)\n b$(sp)" # a op # comment \n b @test format_string("$(sp)a$(sp)$(op)$(sp)# comment\nb$(sp)") == - "$(sp)a $(op) # comment\nb$(sp)" + "$(sp)a $(op) # comment\n b$(sp)" end # Exceptions to the rule: `:` and `^` # a:b @@ -194,7 +194,7 @@ end @test format_string("$(sp)z$(sp)+$(sp)2x$(sp)+$(sp)z$(sp)") == "$(sp)z + 2x + z$(sp)" # Edgecase where the NewlineWs ends up inside the second call in a chain @test format_string("$(sp)a$(sp)\\$(sp)b$(sp)≈ $(sp)\n$(sp)c$(sp)\\$(sp)d$(sp)") == - "$(sp)a \\ b ≈\n$(sp)c \\ d$(sp)" + "$(sp)a \\ b ≈\n c \\ d$(sp)" end end @@ -285,3 +285,87 @@ end end end end + +@testset "block/hard indentation" begin + for sp1 in ("", " ", " ", " "), sp2 in ("", " ", " ", " ") + # function-end + @test format_string("function f()\n$(sp1)x\n$(sp2)end") == + "function f()\n x\nend" + # macro-end + @test format_string("macro f()\n$(sp1)x\n$(sp2)end") == + "macro f()\n x\nend" + # let-end + @test format_string("let a = 1\n$(sp1)x\n$(sp2)end") == + "let a = 1\n x\nend" + # if-end + @test format_string("if a\n$(sp1)x\n$(sp2)end") == + "if a\n x\nend" + # if-else-end + @test format_string("if a\n$(sp1)x\n$(sp2)else\n$(sp1)y\n$(sp2)end") == + "if a\n x\nelse\n y\nend" + # if-elseif-end + @test format_string("if a\n$(sp1)x\n$(sp2)elseif b\n$(sp1)y\n$(sp2)end") == + "if a\n x\nelseif b\n y\nend" + # if-elseif-elseif-end + @test format_string( + "if a\n$(sp1)x\n$(sp2)elseif b\n$(sp1)y\n$(sp2)elseif c\n$(sp1)z\n$(sp2)end", + ) == "if a\n x\nelseif b\n y\nelseif c\n z\nend" + # if-elseif-else-end + @test format_string( + "if a\n$(sp1)x\n$(sp2)elseif b\n$(sp1)y\n$(sp2)else\n$(sp1)z\n$(sp2)end", + ) == "if a\n x\nelseif b\n y\nelse\n z\nend" + # if-elseif-elseif-else-end + @test format_string( + "if a\n$(sp1)x\n$(sp2)elseif b\n$(sp1)y\n$(sp2)elseif " * + "c\n$(sp1)z\n$(sp2)else\n$(sp1)u\n$(sp2)end" + ) == + "if a\n x\nelseif b\n y\nelseif c\n z\nelse\n u\nend" + # begin-end + @test format_string("begin\n$(sp1)x\n$(sp2)end") == "begin\n x\nend" + # (mutable) struct + for mut in ("", "mutable ") + @test format_string("$(mut)struct A\n$(sp1)x\n$(sp2)end") == + "$(mut)struct A\n x\nend" + end + # for-end + @test format_string("for i in I\n$(sp1)x\n$(sp2)end") == "for i in I\n x\nend" + @test format_string("for i in I, j in J\n$(sp1)x\n$(sp2)end") == "for i in I, j in J\n x\nend" + # while-end + @test format_string("while x\n$(sp1)y\n$(sp2)end") == "while x\n y\nend" + # using/import + for verb in ("using", "import") + @test format_string("$(verb) A,\nB") == "$(verb) A,\n B" + @test format_string("$(verb) A: a,\nb") == "$(verb) A: a,\n b" + @test format_string("$(verb) A:\na,\nb") == "$(verb) A:\n a,\n b" + end + # export + @test format_string("export a,\n$(sp1)b") == "export a,\n b" + @test format_string("export\n$(sp1)a,\n$(sp2)b") == "export\n a,\n b" + end +end + +@testset "continuation/soft indentation" begin + for sp1 in ("", " ", " ", " "), sp2 in ("", " ", " ", " ") + # tuple + @test format_string("(a,\n$(sp1)b)") == "(a,\n b)" + @test format_string("(a,\n$(sp1)b\n$(sp2))") == "(a,\n b\n)" + @test format_string("(a,\n$(sp1)b,\n$(sp2))") == "(a,\n b,\n)" + @test format_string("(\n$(sp1)a,\n$(sp1)b,\n$(sp2))") == "(\n a,\n b,\n)" + # call + for sep in (",", ";") + @test format_string("f(a$(sep)\n$(sp1)b)") == "f(a$(sep)\n b)" + @test format_string("f(a$(sep)\n$(sp1)b\n$(sp2))") == "f(a$(sep)\n b\n)" + @test format_string("f(a$(sep)\n$(sp1)b,\n$(sp2))") == "f(a$(sep)\n b,\n)" + @test format_string("f(\n$(sp1)a$(sep)\n$(sp1)b,\n$(sp2))") == "f(\n a$(sep)\n b,\n)" + end + # op-call + @test format_string("a +\n$(sp1)b") == "a +\n b" + @test format_string("a + b *\n$(sp1)c") == "a + b *\n c" + @test format_string("a +\n$(sp1)b *\n$(sp2)c") == "a +\n b *\n c" + @test format_string("a ||\n$(sp1)b") == "a ||\n b" + # assignment + for op in ("=", "+=") + @test format_string("a $(op)\n$(sp1)b") == "a $(op)\n b" + end + end +end