Browse Source

Add indentation formatting

This is a huge patch and adds formatting of indentation. It is slightly
difficult to come up with a good set of rules but I think what is here
is pretty nice. There are two types of indentation: block/hard
indentation and continuation/soft indentation.

Block indentation are context independent and are thus "hard" or
required in the sense that they always happen. Block indentation are
enabled for all `x - end` blocks where `x` is one of `begin`, `do`,
`for`, `function`, `if`, `let`, `quote`, `struct`, `try`, `while`. A
notable exception is `module`, but one idea is to indent nested modules
(in the same file).

Continuation or soft indents are context dependent in the sense that if
nested within another soft indent they extra indents are skipped. Soft
indentation applies to all expressions spanning multiple lines.
For example, newlines in operator chains:
```julia
a + b * c +
    d          # soft indent
```
function calls
```julia
foo(
    a, b c, d  # soft indent
)
```
etc.
pull/19/head
Fredrik Ekre 2 years ago
parent
commit
9a7e2e27a1
No known key found for this signature in database
GPG Key ID: DE82E6D5E364C0A2
  1. 1
      .gitignore
  2. 42
      src/Runic.jl
  3. 106
      src/chisels.jl
  4. 2
      src/main.jl
  5. 672
      src/runestone.jl
  6. 155
      test/runtests.jl

1
.gitignore vendored

@ -1 +1,2 @@ @@ -1 +1,2 @@
/Manifest.toml
*.cov

42
src/Runic.jl

@ -115,6 +115,9 @@ mutable struct Context @@ -115,6 +115,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}
@ -145,9 +148,12 @@ function Context( @@ -145,9 +148,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
@ -187,6 +193,8 @@ function format_node_with_kids!(ctx::Context, node::Node) @@ -187,6 +193,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
@ -247,6 +255,7 @@ function format_node_with_kids!(ctx::Context, node::Node) @@ -247,6 +255,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′)
@ -266,7 +275,18 @@ Format a node. Return values: @@ -266,7 +275,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)
@ -275,10 +295,10 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} @@ -275,10 +295,10 @@ 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.
# Nodes that always recurse!
if (
node_kind === K"block" ||
node_kind === K"braces" ||
@ -313,12 +333,11 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} @@ -313,12 +333,11 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
node_kind === K"vcat" ||
node_kind === K"vect"
)
# Nodes that always recurse!
@assert !JuliaSyntax.is_trivia(node)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′
# Nodes that recurse! if not trivia
elseif !JuliaSyntax.is_trivia(node) && (
node_kind === K"abstract" ||
node_kind === K"as" ||
@ -353,27 +372,25 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} @@ -353,27 +372,25 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
node_kind === K"where" ||
node_kind === K"while"
)
# Nodes that recurse! if not trivia
@assert !JuliaSyntax.is_trivia(node)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′
# Nodes that should recurse if they have kids (all??)
elseif !is_leaf(node) && (
JuliaSyntax.is_operator(node) ||
node_kind === K"else" # try-(catch|finally)-else
)
# Nodes that should recurse if they have kids (all??)
node′ = format_node_with_kids!(ctx, node)
@assert node′ !== nullnode
return node′
# Whitespace and comments emitted verbatim for now
elseif node_kind === K"Whitespace" ||
node_kind === K"NewlineWs" ||
node_kind === K"Comment"
# Whitespace and comments emitted verbatim for now
accept_node!(ctx, node)
return nothing
# Nodes that always emit like the source code
elseif (
node_kind === K"(" ||
node_kind === K")" ||
@ -446,6 +463,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} @@ -446,6 +463,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
node_kind === K"}"
)
)
# Nodes that always emit like the source code
accept_node!(ctx, node)
return nothing
else

106
src/chisels.jl

@ -4,6 +4,12 @@ @@ -4,6 +4,12 @@
# Node utilities extensions and JuliaSyntax extensions #
########################################################
# JuliaSyntax.jl overloads == for this but seems easier to just define a new function
function nodes_equal(n1::Node, n2::Node)
return head(n1) == head(n2) && span(n1) == span(n2) && n1.tags == n2.tags &&
all(((x, y),) -> nodes_equal(x, y), zip(n1.kids, n2.kids))
end
# See JuliaSyntax/src/parse_stream.jl
function stringify_flags(node::Node)
io = IOBuffer()
@ -67,6 +73,87 @@ end @@ -67,6 +73,87 @@ 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
# Tags all leading NewlineWs nodes as continuation nodes. Note that comments are skipped
# over so that cases like `\n#comment\ncode` works as expected.
function continue_newlines(node::Node; leading::Bool = true, trailing::Bool = true)
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
end
kids = verified_kids(node)
if length(kids) == 1
return nothing
end
any_kid_changed = false
if leading
idx = firstindex(kids) - 1
while true
# Skip over whitespace + comments which can mask the newlines
idx = findnext(x -> !(kind(x) in KSet"Whitespace Comment"), kids, idx + 1)
if idx === nothing
# No matching kid found
break
elseif kind(kids[idx]) === K"NewlineWs"
# Kid is a NewlineWs node, tag and keep looking
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
else
# This kid is not Whitespace, Comment or NewlineWs.
# Recurse but break out of the loop
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
break
end
end
end
if trailing
idx = lastindex(kids) + 1
while true
# Skip over whitespace + comments which can mask the newlines
idx = findprev(x -> !(kind(x) in KSet"Whitespace Comment"), kids, idx - 1)
if idx === nothing
# No matching kid found
break
elseif kind(kids[idx]) === K"NewlineWs"
# Kid is a NewlineWs node, tag and keep looking
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
else
# This kid is not Whitespace, Comment or NewlineWs.
# Recurse but break out of the loop
kid′ = continue_newlines(kids[idx]; leading = leading, trailing = trailing)
if kid′ !== nothing
kids[idx] = kid′
any_kid_changed = false
end
break
end
end
end
return any_kid_changed ? node : nothing
end
function has_tag(node::Node, tag::TagType)
return node.tags & tag != 0
@ -80,12 +167,18 @@ function stringify_tags(node::Node) @@ -80,12 +167,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 +228,7 @@ end @@ -135,7 +228,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)
@ -165,6 +258,15 @@ function first_non_whitespace_kid(node::Node) @@ -165,6 +258,15 @@ function first_non_whitespace_kid(node::Node)
return kids[idx]
end
function is_begin_block(node::Node)
return kind(node) === K"block" && length(verified_kids(node)) > 0 &&
kind(verified_kids(node)[1]) === K"begin"
end
function is_paren_block(node::Node)
return kind(node) === K"block" && JuliaSyntax.has_flags(node, JuliaSyntax.PARENS_FLAG)
end
##########################
# Utilities for IOBuffer #
##########################

2
src/main.jl

@ -265,7 +265,7 @@ function main(argv) @@ -265,7 +265,7 @@ function main(argv)
end
# Output the result
changed = ctx.fmt_tree !== ctx.src_tree
changed = !nodes_equal(ctx.fmt_tree, ctx.src_tree)
if check
if changed
print_progress && errln()

672
src/runestone.jl

@ -470,3 +470,675 @@ function for_loop_use_in(ctx::Context, node::Node) @@ -470,3 +470,675 @@ 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, block_kind = K"begin")
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) === block_kind
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_catch(ctx::Context, node::Node)
@assert kind(node) in KSet"catch else finally"
kids = verified_kids(node)
any_kid_changed = false
catch_idx = 1
catch_node = kids[catch_idx]
@assert is_leaf(catch_node) && kind(catch_node) in KSet"catch else finally"
if !has_tag(catch_node, TAG_INDENT)
kids[catch_idx] = add_tag(catch_node, TAG_INDENT)
any_kid_changed = true
end
if !has_tag(catch_node, TAG_DEDENT)
kids[catch_idx] = add_tag(catch_node, TAG_DEDENT)
any_kid_changed = true
end
# Skip over the catch-identifier (if any)
block_idx = findnext(x -> kind(x) === K"block", kids, catch_idx + 1)::Int
@assert kind(kids[block_idx]) === K"block"
block_node′ = indent_block(ctx, kids[block_idx])
if block_node′ !== nothing
kids[block_idx] = block_node′
any_kid_changed = true
end
return any_kid_changed ? node : nothing
end
function indent_try(ctx::Context, node::Node)
@assert kind(node) in KSet"try"
@assert !is_leaf(node)
kids = verified_kids(node)
any_kid_changed = false
# First node is `try`
try_idx = 1
try_node = kids[try_idx]
@assert is_leaf(kids[try_idx]) && kind(try_node) in KSet"try"
if !has_tag(try_node, TAG_INDENT)
kids[try_idx] = add_tag(try_node, TAG_INDENT)
any_kid_changed = true
end
# Second node the try-block
try_block_idx = findnext(!JuliaSyntax.is_whitespace, kids, try_idx + 1)::Int
try_block_node′ = indent_block(ctx, kids[try_block_idx])
if try_block_node′ !== nothing
kids[try_block_idx] = try_block_node′
any_kid_changed = true
end
# Check for catch/finally. They can be in any order
catch_idx = findnext(x -> kind(x) in KSet"catch finally", kids, try_block_idx + 1)::Int
@assert !is_leaf(kids[catch_idx]) && kind(kids[catch_idx]) in KSet"catch finally"
catch_node′ = indent_catch(ctx, kids[catch_idx])
if catch_node′ !== nothing
kids[catch_idx] = catch_node′
any_kid_changed = true
end
# There may be an else in between catch and finally (lol)
else_idx = findnext(x -> kind(x) === K"else", kids, catch_idx + 1)
if else_idx !== nothing
else_node′ = indent_catch(ctx, kids[else_idx])
if else_node′ !== nothing
kids[else_idx] = else_node′
any_kid_changed = true
end
end
# Check for the other one
other_kind = kind(kids[catch_idx]) === K"catch" ? K"finally" : K"catch"
finally_idx = findnext(
x -> kind(x) === other_kind, kids, something(else_idx, catch_idx) + 1,
)
if finally_idx !== nothing
finally_node′ = indent_catch(ctx, kids[finally_idx])
if finally_node′ !== nothing
kids[finally_idx] = finally_node′
any_kid_changed = true
end
end
# Check for end
end_idx = findnext(
x -> kind(x) === K"end", kids, something(finally_idx, else_idx, catch_idx) + 1,
)::Int
@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
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 or last leaf of a node, tag'em.
# TODO: The trailing argument should maybe depend on `indent_closing_token` or
# possibly set to `false` if `i == close_idx - 1`?
kid′ = continue_newlines(kid; leading = true, trailing = true)
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::Context, node::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::Context, node::Node)
@assert kind(node) === K"tuple"
kids = verified_kids(node)
# Check whether this is an explicit tuple, e.g. `(a, b)`,
# or an implicit tuple, e.g. `a, b`.
opening_paren_idx = findfirst(x -> kind(x) === K"(", kids)
if opening_paren_idx === nothing
# Implicit tuple: don't indent the closing token
first_item_idx = findfirst(!JuliaSyntax.is_whitespace, kids)
if first_item_idx === nothing
# Empty implicit tuple can happen in e.g. a do-block without arguments
return nothing
end
last_item_idx = findlast(!JuliaSyntax.is_whitespace, kids)::Int
return indent_newlines_between_indices(
ctx, node, first_item_idx, last_item_idx; indent_closing_token = true,
)
else
# Explicit tuple: indent the closing token
closing_paren_idx = findnext(x -> kind(x) === K")", kids, opening_paren_idx + 1)::Int
@assert opening_paren_idx == firstindex(kids)
@assert closing_paren_idx == lastindex(kids)
return indent_newlines_between_indices(
ctx, node, opening_paren_idx, closing_paren_idx; indent_closing_token = false,
)
end
end
function indent_parens(ctx::Context, node::Node)
@assert kind(node) in KSet"parens"
return indent_paren(ctx, node)
end
function indent_parameters(ctx::Context, node::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::Context, node::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::Context, node::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::Context, node::Node; skip_last::Bool = true, is_last::Bool = is_leaf(node),
)
if is_leaf(node)
if kind(node) === K"NewlineWs" && !has_tag(node, TAG_LINE_CONT) &&
!(skip_last && is_last)
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; skip_last = skip_last, is_last = i == lastindex(kids),
)
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::Context, node::Node)
@assert kind(node) in KSet"using import export"
return continue_all_newlines(ctx, node)
end
function indent_ternary(ctx::Context, node::Node)
@assert kind(node) === K"?"
return continue_all_newlines(ctx, node)
end
function indent_assignment(ctx::Context, node::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 indent_paren_block(ctx::Context, node::Node)
@assert kind(node) === K"block"
@assert JuliaSyntax.has_flags(node, JuliaSyntax.PARENS_FLAG)
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
function indent_do(ctx::Context, node::Node)
@assert kind(node) === K"do"
kids = verified_kids(node)
any_kid_changed = false
# Skip over the call and go directly to the do-keyword
do_idx = findfirst(x -> kind(x) === K"do", kids)::Int
if !has_tag(kids[do_idx], TAG_INDENT)
kids[do_idx] = add_tag(kids[do_idx], TAG_INDENT)
any_kid_changed = true
end
# Find the do body block
block_idx = findnext(x -> kind(x) === K"block", kids, do_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
# Closing `end`
end_idx = findnext(x -> kind(x) === K"end", kids, block_idx + 1)::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_quote(ctx::Context, node::Node)
@assert kind(node) === K"quote"
kids = verified_kids(node)
any_kid_changed = false
# K"quote" can be `quote ... end` or `:(...)`.
block_form = !JuliaSyntax.has_flags(node, JuliaSyntax.COLON_QUOTE)
if block_form
block_idx = findfirst(x -> kind(x) === K"block", kids)
if block_idx === nothing
# `bar` in `foo.bar` is a quote block...
return nothing
end
block_node′ = indent_begin(ctx, kids[block_idx], K"quote")
if block_node′ !== nothing
kids[block_idx] = block_node′
any_kid_changed = true
end
return any_kid_changed ? node : nothing
else
# The short form can be ignored since the inside (K"block", K"tuple", or
# K"Identifier") of the quote will be handled by other passes.
return nothing
end
end
function indent_array(ctx::Context, node::Node)
@assert kind(node) in KSet"vect vcat ncat"
kids = verified_kids(node)
any_kid_changed = false
opening_bracket_idx = findfirst(x -> kind(x) === K"[", kids)::Int
closing_bracket_idx = findnext(x -> kind(x) === K"]", kids, opening_bracket_idx + 1)::Int
return indent_newlines_between_indices(
ctx, node, opening_bracket_idx, closing_bracket_idx,
)
end
function indent_array_row(ctx::Context, node::Node)
@assert kind(node) === K"row"
return continue_all_newlines(ctx, node)
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 is_begin_block(node)
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)
elseif kind(node) === K"?"
return indent_ternary(ctx, node)
elseif kind(node) === K"try"
return indent_try(ctx, node)
elseif kind(node) === K"quote"
return indent_quote(ctx, node)
elseif kind(node) === K"do"
return indent_do(ctx, node)
elseif is_paren_block(node)
return indent_paren_block(ctx, node)
elseif kind(node) in KSet"vect vcat ncat"
return indent_array(ctx, node)
elseif kind(node) in KSet"row"
return indent_array_row(ctx, node)
end
return nothing
end

155
test/runtests.jl

@ -168,10 +168,10 @@ end @@ -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 @@ -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,152 @@ end @@ -285,3 +285,152 @@ end
end
end
end
@testset "block/hard indentation" begin
for sp in ("", " ", " ", " ")
# function-end
@test format_string("function f()\n$(sp)x\n$(sp)end") ==
"function f()\n x\nend"
# macro-end
@test format_string("macro f()\n$(sp)x\n$(sp)end") ==
"macro f()\n x\nend"
# let-end
@test format_string("let a = 1\n$(sp)x\n$(sp)end") ==
"let a = 1\n x\nend"
# begin-end
@test format_string("begin\n$(sp)x\n$(sp)end") ==
"begin\n x\nend"
# quote-end
@test format_string("quote\n$(sp)x\n$(sp)end") ==
"quote\n x\nend"
# if-end
@test format_string("if a\n$(sp)x\n$(sp)end") ==
"if a\n x\nend"
# if-else-end
@test format_string("if a\n$(sp)x\n$(sp)else\n$(sp)y\n$(sp)end") ==
"if a\n x\nelse\n y\nend"
# if-elseif-end
@test format_string("if a\n$(sp)x\n$(sp)elseif b\n$(sp)y\n$(sp)end") ==
"if a\n x\nelseif b\n y\nend"
# if-elseif-elseif-end
@test format_string(
"if a\n$(sp)x\n$(sp)elseif b\n$(sp)y\n$(sp)elseif c\n$(sp)z\n$(sp)end",
) == "if a\n x\nelseif b\n y\nelseif c\n z\nend"
# if-elseif-else-end
@test format_string(
"if a\n$(sp)x\n$(sp)elseif b\n$(sp)y\n$(sp)else\n$(sp)z\n$(sp)end",
) == "if a\n x\nelseif b\n y\nelse\n z\nend"
# if-elseif-elseif-else-end
@test format_string(
"if a\n$(sp)x\n$(sp)elseif b\n$(sp)y\n$(sp)elseif " *
"c\n$(sp)z\n$(sp)else\n$(sp)u\n$(sp)end"
) ==
"if a\n x\nelseif b\n y\nelseif c\n z\nelse\n u\nend"
# begin-end
@test format_string("begin\n$(sp)x\n$(sp)end") == "begin\n x\nend"
# (mutable) struct
for mut in ("", "mutable ")
@test format_string("$(mut)struct A\n$(sp)x\n$(sp)end") ==
"$(mut)struct A\n x\nend"
end
# for-end
@test format_string("for i in I\n$(sp)x\n$(sp)end") == "for i in I\n x\nend"
@test format_string("for i in I, j in J\n$(sp)x\n$(sp)end") == "for i in I, j in J\n x\nend"
# while-end
@test format_string("while x\n$(sp)y\n$(sp)end") == "while x\n y\nend"
# try-catch-end
@test format_string("try\n$(sp)x\n$(sp)catch\n$(sp)y\n$(sp)end") ==
"try\n x\ncatch\n y\nend"
# try-catch(err)-end
@test format_string("try\n$(sp)x\n$(sp)catch err\n$(sp)y\n$(sp)end") ==
"try\n x\ncatch err\n y\nend"
# try-catch-finally-end
@test format_string(
"try\n$(sp)x\n$(sp)catch\n$(sp)y\n$(sp)finally\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch\n y\nfinally\n z\nend"
# try-catch(err)-finally-end
@test format_string(
"try\n$(sp)x\n$(sp)catch err\n$(sp)y\n$(sp)finally\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch err\n y\nfinally\n z\nend"
# try-finally-catch-end (yes, this is allowed...)
@test format_string(
"try\n$(sp)x\n$(sp)finally\n$(sp)y\n$(sp)catch\n$(sp)z\n$(sp)end",
) == "try\n x\nfinally\n y\ncatch\n z\nend"
# try-finally-catch(err)-end
@test format_string(
"try\n$(sp)x\n$(sp)finally\n$(sp)y\n$(sp)catch err\n$(sp)z\n$(sp)end",
) == "try\n x\nfinally\n y\ncatch err\n z\nend"
if VERSION >= v"1.8"
# try-catch-else-end
@test format_string(
"try\n$(sp)x\n$(sp)catch\n$(sp)y\n$(sp)else\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch\n y\nelse\n z\nend"
# try-catch(err)-else-end
@test format_string(
"try\n$(sp)x\n$(sp)catch err\n$(sp)y\n$(sp)else\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch err\n y\nelse\n z\nend"
# try-catch-else-finally-end
@test format_string(
"try\n$(sp)x\n$(sp)catch\n$(sp)y\n$(sp)else\n$(sp)z\n$(sp)finally\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch\n y\nelse\n z\nfinally\n z\nend"
# try-catch(err)-else-finally-end
@test format_string(
"try\n$(sp)x\n$(sp)catch err\n$(sp)y\n$(sp)else\n$(sp)z\n$(sp)finally\n$(sp)z\n$(sp)end",
) == "try\n x\ncatch err\n y\nelse\n z\nfinally\n z\nend"
end
# do-end
@test format_string("open() do\n$(sp)a\n$(sp)end") == "open() do\n a\nend"
@test format_string("open() do io\n$(sp)a\n$(sp)end") == "open() do io\n a\nend"
end
end
@testset "continuation/soft indentation" begin
for sp in ("", " ", " ", " ")
# tuple
@test format_string("(a,\n$(sp)b)") == "(a,\n b)"
@test format_string("(a,\n$(sp)b\n$(sp))") == "(a,\n b\n)"
@test format_string("(a,\n$(sp)b,\n$(sp))") == "(a,\n b,\n)"
@test format_string("(\n$(sp)a,\n$(sp)b,\n$(sp))") == "(\n a,\n b,\n)"
# call
for sep in (",", ";")
@test format_string("f(a$(sep)\n$(sp)b)") == "f(a$(sep)\n b)"
@test format_string("f(a$(sep)\n$(sp)b\n$(sp))") == "f(a$(sep)\n b\n)"
@test format_string("f(a$(sep)\n$(sp)b,\n$(sp))") == "f(a$(sep)\n b,\n)"
@test format_string("f(\n$(sp)a$(sep)\n$(sp)b,\n$(sp))") == "f(\n a$(sep)\n b,\n)"
end
# op-call
@test format_string("a +\n$(sp)b") == "a +\n b"
@test format_string("a + b *\n$(sp)c") == "a + b *\n c"
@test format_string("a +\n$(sp)b *\n$(sp)c") == "a +\n b *\n c"
@test format_string("a ||\n$(sp)b") == "a ||\n b"
# assignment
for op in ("=", "+=")
@test format_string("a $(op)\n$(sp)b") == "a $(op)\n b"
end
# using/import
for verb in ("using", "import")
@test format_string("$(verb) A,\n$(sp)B") == "$(verb) A,\n B"
@test format_string("$(verb) A: a,\n$(sp)b") == "$(verb) A: a,\n b"
@test format_string("$(verb) A:\n$(sp)a,\n$(sp)b") == "$(verb) A:\n a,\n b"
end
# export
@test format_string("export a,\n$(sp)b") == "export a,\n b"
@test format_string("export\n$(sp)a,\n$(sp)b") == "export\n a,\n b"
# ternary
@test format_string("a ?\n$(sp)b : c") == "a ?\n b : c"
@test format_string("a ? b :\n$(sp)c") == "a ? b :\n c"
@test format_string("a ?\n$(sp)b :\n$(sp)c") == "a ?\n b :\n c"
@test format_string("a ?\n$(sp)b :\n$(sp)c ?\n$(sp)d : e") ==
"a ?\n b :\n c ?\n d : e"
# paren-quote
@test format_string(":(a,\n$(sp)b)") == ":(a,\n b)"
@test format_string(":(a,\n$(sp)b)") == ":(a,\n b)"
@test format_string(":(a;\n$(sp)b)") == ":(a;\n b)"
# paren-block
@test format_string("(a;\n$(sp)b)") == "(a;\n b)"
# array literals
@test format_string("[a,\n$(sp)b]") == "[a,\n b]"
@test format_string("[\n$(sp)a,\n$(sp)b\n$(sp)]") == "[\n a,\n b\n]"
@test format_string("[a b\n$(sp)c d]") == "[a b\n c d]"
end
end

Loading…
Cancel
Save