diff --git a/src/Runic.jl b/src/Runic.jl index 4414ccc..a371a46 100644 --- a/src/Runic.jl +++ b/src/Runic.jl @@ -125,6 +125,7 @@ mutable struct Context debug::Bool check::Bool diff::Bool + filemode::Bool # Global state indent_level::Int # track (hard) indentation level call_depth::Int # track call-depth level for debug printing @@ -138,7 +139,7 @@ end function Context( src_str::String; assert::Bool = true, debug::Bool = false, verbose::Bool = debug, - diff::Bool = false, check::Bool = false, quiet::Bool = false, + diff::Bool = false, check::Bool = false, quiet::Bool = false, filemode::Bool = true, ) src_io = IOBuffer(src_str) src_tree = Node( @@ -165,7 +166,7 @@ function Context( lineage_kinds = JuliaSyntax.Kind[] return Context( src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check, - diff, call_depth, indent_level, prev_sibling, next_sibling, lineage_kinds, + diff, filemode, call_depth, indent_level, prev_sibling, next_sibling, lineage_kinds, ) end @@ -297,9 +298,10 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode} # Go through the runestone and apply transformations. ctx.call_depth += 1 + @return_something no_leading_and_single_trailing_newline(ctx, node) + @return_something max_three_consecutive_newlines(ctx, node) @return_something insert_delete_mark_newlines(ctx, node) @return_something trim_trailing_whitespace(ctx, node) - @return_something max_three_consecutive_newlines(ctx, node) @return_something format_hex_literals(ctx, node) @return_something format_oct_literals(ctx, node) @return_something format_float_literals(ctx, node) @@ -371,8 +373,8 @@ end Format string `str` and return the formatted string. """ -function format_string(str::AbstractString) - ctx = Context(str) +function format_string(str::AbstractString; filemode::Bool = false) + ctx = Context(str; filemode = filemode) format_tree!(ctx) return String(take!(ctx.fmt_io)) end diff --git a/src/chisels.jl b/src/chisels.jl index 8fab1c1..6ec43c3 100644 --- a/src/chisels.jl +++ b/src/chisels.jl @@ -187,32 +187,32 @@ function make_node(node::Node, kids′::Vector{Node}, tags = node.tags) return Node(head(node), span′, kids′, tags) end -function first_leaf(node::Node) - if is_leaf(node) - return node - else - return first_leaf(first(verified_kids(node))) - end -end +# TODO: Remove? +first_leaf(node::Node) = nth_leaf(node, 1) -function second_leaf(node::Node) +function nth_leaf(node::Node, nth::Int) + leaf, n_seen = nth_leaf(node, nth, 0) + return n_seen == nth ? leaf : nothing +end +function nth_leaf(node::Node, nth::Int, n_seen::Int) if is_leaf(node) - return nothing + return node, n_seen + 1 else kids = verified_kids(node) - if length(kids) == 0 - return nothing - elseif !is_leaf(kids[1]) - return second_leaf(kids[1]) - elseif length(kids) > 1 - @assert is_leaf(kids[1]) - return first_leaf(kids[2]) - else - @assert false + for kid in kids + leaf, n_seen = nth_leaf(kid, nth, n_seen) + if n_seen == nth + return leaf, n_seen + end end + return nothing, n_seen end end +function second_leaf(node::Node) + return nth_leaf(node, 2) +end + # Return number of non-whitespace kids, basically the length the equivalent # (expr::Expr).args function meta_nargs(node::Node) @@ -258,8 +258,33 @@ function last_leaf(node::Node) if is_leaf(node) return node else - return last_leaf(last(verified_kids(node))) + kids = verified_kids(node) + if length(kids) == 0 + return nothing + else + return last_leaf(last(kids)) + end + end +end + +function second_last_leaf(node::Node) + node, n = second_last_leaf(node, 0) + return n == 2 ? node : nothing +end + +function second_last_leaf(node::Node, n_seen::Int) + if is_leaf(node) + return node, n_seen + 1 + else + kids = verified_kids(node) + for i in reverse(1:length(kids)) + kid, n_seen = second_last_leaf(kids[i], n_seen) + if n_seen == 2 + return kid, n_seen + end + end end + return nothing, n_seen end function has_newline_after_non_whitespace(node::Node) diff --git a/src/debug.jl b/src/debug.jl index a94f546..4b8cedc 100644 --- a/src/debug.jl +++ b/src/debug.jl @@ -31,4 +31,3 @@ function macroexpand_assert(expr) msg = string(expr) return :($(esc(expr)) || throw(AssertionError($msg))) end - diff --git a/src/runestone.jl b/src/runestone.jl index 1cd666f..f148e65 100644 --- a/src/runestone.jl +++ b/src/runestone.jl @@ -1316,8 +1316,49 @@ function parens_around_op_calls_in_colon(ctx::Context, node::Node) end end +# No newline at the beginning and single newline at the end of the file +function no_leading_and_single_trailing_newline(ctx::Context, node::Node) + if !(ctx.filemode && length(ctx.lineage_kinds) == 0) + return nothing + end + @assert kind(node) === K"toplevel" + @assert !is_leaf(node) + @assert position(ctx.fmt_io) == 0 + changed = false + while (l = first_leaf(node); l !== nothing && kind(l) === K"NewlineWs" && length(verified_kids(node)) > 1) + changed = true + replace_bytes!(ctx, "", span(l)) + node = replace_first_leaf(node, nullnode) + end + accept_node!(ctx, node) + l = last_leaf(node) + if l === nothing || kind(l) !== K"NewlineWs" + kids′ = copy(verified_kids(node)) + push!(kids′, Node(JuliaSyntax.SyntaxHead(K"NewlineWs", JuliaSyntax.TRIVIA_FLAG), 1)) + replace_bytes!(ctx, "\n", 0) + changed = true + node = make_node(node, kids′) + else + ll = second_last_leaf(node) + while ll !== nothing && kind(l) === kind(ll) === K"NewlineWs" + changed = true + seek(ctx.fmt_io, position(ctx.fmt_io) - span(l)) + # replace_bytes!(ctx, "", span(l)) + node = replace_last_leaf(node, nullnode) + @assert last_leaf(node) === ll + l = ll + ll = second_last_leaf(node) + end + end + if changed + return node + else + seek(ctx.fmt_io, 0) + return nothing + end +end -# Remove more than two newlines in a row +# Remove more than three newlines in a row function max_three_consecutive_newlines(ctx::Context, node::Node) is_leaf(node) && return nothing kids = verified_kids(node)