Repository: fredrikekre/Runic.jl
Pull request #101 (2024-11-13 22:08:45)

Better tree normalization

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre) Report
Attention: Patch coverage is `69.60000%` with `38 lines` in your changes missing coverage. Please review.
> Project coverage is 55.12%. Comparing base [(`ad205bc`)](https://app.codecov.io/gh/fredrikekre/Runic.jl/commit/ad205bc38d33d6ff357d4560abc8907e1233f619?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre) to head [(`9538fe1`)](https://app.codecov.io/gh/fredrikekre/Runic.jl/commit/9538fe1de5b76b7061259a2d6c5bdfa5300e17da?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre).

| [Files with missing lines](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre) | Patch % | Lines |
|---|---|---|
| [src/runestone.jl](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?src=pr&el=tree&filepath=src%2Frunestone.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre#diff-c3JjL3J1bmVzdG9uZS5qbA==) | 47.69% | [34 Missing :warning: ](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre) |
| [src/chisels.jl](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?src=pr&el=tree&filepath=src%2Fchisels.jl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre#diff-c3JjL2NoaXNlbHMuamw=) | 93.33% | [4 Missing :warning: ](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre) |

> :exclamation:  There is a different number of reports uploaded between BASE (ad205bc) and HEAD (9538fe1). Click for more details.
> 
> <details><summary>HEAD has 7 uploads less than BASE</summary>
>
>| Flag | BASE (ad205bc) | HEAD (9538fe1) |
>|------|------|------|
>||8|1|
></details>

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master     #101       +/-   ##
===========================================
- Coverage   90.74%   55.12%   -35.63%     
===========================================
  Files           8        8               
  Lines        3684     3730       +46     
===========================================
- Hits         3343     2056     -1287     
- Misses        341     1674     +1333     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/fredrikekre/Runic.jl/pull/101?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fredrik+Ekre).


Patch:
diff --git a/src/chisels.jl b/src/chisels.jl
index b556e71..d1ec258 100644
--- a/src/chisels.jl
+++ b/src/chisels.jl
@@ -77,81 +77,97 @@ function stringify_flags(node::Node)
     return String(take!(io))
 end
 
+function pop_if_whitespace!(popf!::F, node) where {F}
+    if !is_leaf(node) && begin
+            kids = verified_kids(node)
+            length(kids) > 0 &&
+                JuliaSyntax.is_whitespace(popf! === popfirst! ? kids[1] : kids[end])
+        end
+        ws = popf!(kids)
+        @assert JuliaSyntax.is_whitespace(ws)
+        node′ = make_node(node, kids)
+        @assert span(node) == span(node′) + span(ws)
+        return node′, ws
+    end
+    return nothing, nothing
+end
+
 # The parser is somewhat inconsistent(?) with where e.g. whitespace nodes end up so in order
 # to simplify the formatting code we normalize some things.
 function normalize_tree!(node)
-    is_leaf(node) && return
+    is_leaf(node) && return node
     kids = verified_kids(node)
 
-    # Move standalone K"NewlineWs" (and other??) nodes from between the var block and the
-    # body block in K"let" nodes.
-    # Note that this happens before the whitespace into block normalization below because
-    # for let we want to move it to the subsequent block instead.
+    # For K"let" the token (K"NewlineWs" or K";") separating the vars block from the
+    # body ends up in between the two K"block" nodes. Move it into the body block.
     if kind(node) === K"let"
         varsidx = findfirst(x -> kind(x) === K"block", kids)::Int
-        bodyidx = findnext(x -> kind(x) === K"block", kids, varsidx + 1)::Int
-        r = (varsidx + 1):(bodyidx - 1)
-        if length(r) > 0
-            items = kids[r]
-            deleteat!(kids, r)
-            bodyidx -= length(r)
-            body = kids[bodyidx]
-            prepend!(verified_kids(body), items)
-            kids[bodyidx] = make_node(body, verified_kids(body))
+        blockidx = findnext(x -> kind(x) === K"block", kids, varsidx + 1)::Int
+        while blockidx != varsidx + 1
+            grandkid = popat!(kids, blockidx - 1)
+            blockidx -= 1
+            block = kids[blockidx]
+            pushfirst!(verified_kids(block), grandkid)
+            kids[blockidx] = make_node(block, verified_kids(block))
         end
+        @assert span(node) == mapreduce(span, +, kids; init = 0)
     end
 
-    # Normalize K"Whitespace" nodes in blocks. For example in `if x y end` the space will be
-    # outside the block just before the K"end" node, but in `if x\ny\nend` the K"NewlineWs"
-    # will end up inside the block.
+    # Normalize K"Whitespace" nodes in blocks. For example in `if x y end` the space
+    # will be outside the block just before the K"end" node, but in `if x\ny\nend` the
+    # K"NewlineWs" will end up inside the block.
     if kind(node) in KSet"function if elseif for while try do macro module baremodule let struct module"
-        blockidx = findfirst(x -> kind(x) === K"block", kids)
-        while blockidx !== nothing && blockidx < length(kids)
-            if kind(kids[blockidx + 1]) !== K"Whitespace"
-                # TODO: This repeats the computation below...
+        blockidx = 0
+        while let
                 blockidx = findnext(x -> kind(x) === K"block", kids, blockidx + 1)
+                blockidx !== nothing && blockidx < length(kids)
+            end
+            if kind(kids[blockidx + 1]) !== K"Whitespace"
                 continue
             end
-            # Pop the ws and push it into the block instead
             block = kids[blockidx]
             blockkids = verified_kids(block)
             @assert !(kind(blockkids[end]) in KSet"Whitespace NewlineWs")
             push!(blockkids, popat!(kids, blockidx + 1))
-            # Remake the block to recompute the span
             kids[blockidx] = make_node(block, blockkids)
-            # Find next block
-            blockidx = findnext(x -> kind(x) === K"block", kids, blockidx + 1)
         end
+        @assert span(node) == mapreduce(span, +, kids; init = 0)
     end
 
-    # Normalize K"Whitespace" nodes in if-elseif-else chains where the node needs to move
-    # many steps into the last else block...
+    # Normalize K"Whitespace" nodes in if-elseif-else chains where the node needs to
+    # move many steps into the last else block.
     if kind(node) === K"if"
         elseifidx = findfirst(x -> kind(x) === K"elseif", kids)
         if elseifidx !== nothing
             endidx = findnext(x -> kind(x) === K"end", kids, elseifidx + 1)::Int
             if elseifidx + 2 == endidx && kind(kids[elseifidx + 1]) === K"Whitespace"
-                # Pop the ws and push it into the last block instead
                 ws = popat!(kids, elseifidx + 1)
                 elseifnode = insert_into_last_else_block(kids[elseifidx], ws)
                 @assert elseifnode !== nothing
                 kids[elseifidx] = elseifnode
             end
         end
+        @assert span(node) == mapreduce(span, +, kids; init = 0)
     end
 
     # Normalize K"Whitespace" nodes in try-catch-finally-else
     if kind(node) === K"try"
-        catchidx = findfirst(x -> kind(x) in KSet"catch finally else", kids)
-        while catchidx !== nothing
+        catchidx = 0
+        while let
+                catchidx = findnext(
+                    x -> kind(x) in KSet"catch finally else", kids, catchidx + 1
+                )
+                catchidx !== nothing
+            end
+            @assert catchidx + 1 <= length(kids)
             if kind(kids[catchidx + 1]) === K"Whitespace"
                 ws = popat!(kids, catchidx + 1)
                 catchnode = insert_into_last_catchlike_block(kids[catchidx], ws)
                 @assert catchnode !== nothing
                 kids[catchidx] = catchnode
             end
-            catchidx = findnext(x -> kind(x) in KSet"catch finally else", kids, catchidx + 1)
         end
+        @assert span(node) == mapreduce(span, +, kids; init = 0)
     end
 
     # Normalize K"NewlineWs" nodes in empty do-blocks
@@ -161,7 +177,7 @@ function normalize_tree!(node)
         @assert tupleidx + 1 == blockidx
         tuple = kids[tupleidx]
         tuplekids = verified_kids(tuple)
-        if kind(tuplekids[end]) === K"NewlineWs"
+        if length(tuplekids) > 0 && kind(tuplekids[end]) === K"NewlineWs"
             # If the tuple ends with a K"NewlineWs" node we move it into the block
             block = kids[blockidx]
             blockkids = verified_kids(block)
@@ -173,13 +189,63 @@ function normalize_tree!(node)
             kids[tupleidx] = make_node(tuple, tuplekids)
             kids[blockidx] = make_node(block, blockkids)
         end
+        @assert span(node) == mapreduce(span, +, kids; init = 0)
     end
 
+    @assert span(node) == mapreduce(span, +, kids; init = 0)
     @assert kids === verified_kids(node)
-    for kid in kids
-        ksp = span(kid)
-        normalize_tree!(kid)
-        @assert span(kid) == ksp
+
+    # Loop over the kids to bubble up whitespace
+    i = 0
+    while i < lastindex(kids)
+        i += 1
+        kid = kids[i]
+
+        # Recursively normalize the kid to bubble up whitespace to the front. This should
+        # never change the node kind or the span, just internally reorganize the tokens.
+        kid′ = normalize_tree!(kid)
+        @assert kid === kid′
+        is_leaf(kid) ||
+            @assert span(kid) == mapreduce(span, +, verified_kids(kid); init = 0)
+
+        # If the kid is a K"block" we don't bubble up whitespace since it is more convenient
+        # to keep it inside the block (see e.g. `indent_block` which relies on this for
+        # making sure blocks starts/ends with a K"NewlineWs").
+        if kind(kid) === K"block"
+            # If this is a begin-block (or quote-block) we can still bubble up the
+            # whitespace since the begin and end tokens are inside of the block. I don't
+            # think this currently happens though so for now we just assert.
+            if is_begin_block(kid)
+                @assert kind(verified_kids(kid)[1]) === K"begin"
+                @assert kind(verified_kids(kid)[end]) === K"end"
+            elseif is_begin_block(kid, K"quote")
+                @assert kind(verified_kids(kid)[1]) === K"quote"
+                @assert kind(verified_kids(kid)[end]) === K"end"
+            elseif is_paren_block(kid)
+                @assert kind(verified_kids(kid)[1]) === K"("
+                @assert kind(verified_kids(kid)[end]) === K")"
+            end
+            continue
+        end
+
+        # Extract any leading whitespace and insert it before the node.
+        while ((kid′, ws) = pop_if_whitespace!(popfirst!, kid); ws !== nothing)
+            @assert kid′ !== nothing
+            @assert kind(kid′) === kind(kid)
+            kids[i] = kid = kid′
+            insert!(kids, i, ws)
+            i += 1
+        end
+
+        # Extract any trailing whitespace and insert it after the node.
+        while ((kid′, ws) = pop_if_whitespace!(pop!, kid); ws !== nothing)
+            # Trailing whitespace have only been seen in the wild in these three cases
+            @assert kind(kid) in KSet"row nrow parameters"
+            @assert kid′ !== nothing
+            @assert kind(kid′) === kind(kid)
+            kids[i] = kid = kid′
+            insert!(kids, i + 1, ws)
+        end
     end
     # We only move around things inside this node so the span should be unchanged
     @assert span(node) == mapreduce(span, +, kids; init = 0)
@@ -378,21 +444,6 @@ function nth_leaf(node::Node, nth::Int, n_seen::Int)
     end
 end
 
-function second_leaf(node::Node)
-    return nth_leaf(node, 2)
-end
-
-# TODO: This should probably be merged with kmatch or something...
-function peek_leafs(node::Node, leaf_kinds::Tuple)
-    for (i, leaf_kind) in pairs(leaf_kinds)
-        ith = nth_leaf(node, i)
-        if ith === nothing || kind(ith) !== leaf_kind
-            return false
-        end
-    end
-    return true
-end
-
 # Return number of non-whitespace kids, basically the length the equivalent
 # (expr::Expr).args
 function meta_nargs(node::Node)
@@ -482,27 +533,6 @@ function second_last_leaf(node::Node, n_seen::Int)
     return nothing, n_seen
 end
 
-function has_newline_after_non_whitespace(node::Node)
-    if is_leaf(node)
-        @assert kind(node) !== K"NewlineWs"
-        return false
-    else
-        kids = verified_kids(node)
-        idx = findlast(!JuliaSyntax.is_whitespace, kids)
-        if idx === nothing
-            unreachable()
-            # Everything is whitespace...
-            return any(x -> kind(x) === K"NewlineWs", kids)
-        end
-        return any(x -> kind(x) === K"NewlineWs", kids[(idx + 1):end]) ||
-            has_newline_after_non_whitespace(kids[idx])
-        # if idx === nothing
-        #     # All is whitespace, check if any of the kids is a newline
-        #     return any(x -> kind(x) === K"NewlineWs", kids)
-        # end
-    end
-end
-
 function is_assignment(node::Node)
     return JuliaSyntax.is_prec_assignment(node)
 end
@@ -622,9 +652,10 @@ function first_non_whitespace_kid(node::Node)
     return kids[idx]
 end
 
-function is_begin_block(node::Node)
+function is_begin_block(node::Node, token = K"begin")
+    @assert token in KSet"begin quote"
     return kind(node) === K"block" && length(verified_kids(node)) > 0 &&
-        kind(verified_kids(node)[1]) === K"begin"
+        kind(verified_kids(node)[1]) === token
 end
 
 function is_paren_block(node::Node)
diff --git a/src/runestone.jl b/src/runestone.jl
index a6e7874..9f6fcf7 100644
--- a/src/runestone.jl
+++ b/src/runestone.jl
@@ -178,38 +178,8 @@ function spaces_around_x(ctx::Context, node::Node, is_x::F, n_leaves_per_x::Int
                 replace_bytes!(ctx, " ", span(kid))
                 accept_node!(ctx, ws)
                 looking_for_whitespace = false
-            elseif !is_leaf(kid) && kind(first_leaf(kid)) === K"Whitespace"
-                # Whitespace found at the beginning of next kid.
-                kid_ws = first_leaf(kid)
-                looking_for_whitespace = kind(last_leaf(kid)) !== K"Whitespace"
-                @assert !is_x(kid)::Bool
-                looking_for_x = true
-                if span(kid_ws) == 1
-                    # Accept the node
-                    accept_node!(ctx, kid)
-                    any_changes && push!(kids′, kid)
-                else
-                    # Replace the whitespace node of the kid
-                    kid′ = replace_first_leaf(kid, ws)
-                    @assert span(kid′) == span(kid) - span(kid_ws) + 1
-                    replace_bytes!(ctx, " ", span(kid_ws))
-                    accept_node!(ctx, kid′)
-                    any_changes = true
-                    if kids′ === kids
-                        kids′ = kids[1:(i - 1)]
-                    end
-                    push!(kids′, kid′)
-                end
-            elseif !is_leaf(kid) && kind(first_leaf(kid)) === K"NewlineWs"
-                # NewlineWs have to be accepted as is
-                # @info "     ... kids first leaf is NewlineWs I'll take it"
-                accept_node!(ctx, kid)
-                any_changes && push!(kids′, kid)
-                looking_for_whitespace = kind(last_leaf(kid)) !== K"Whitespace"
-                @assert !is_x(kid)::Bool
-                looking_for_x = true
             else
-                # @info "     ... no whitespace, inserting" kind(kid)
+                @assert !(first_leaf(kid) in KSet"Whitespace NewlineWs")
                 # Not a whitespace node, insert one
                 any_changes = true
                 if kids′ === kids
@@ -399,7 +369,7 @@ function spaces_in_listlike(ctx::Context, node::Node)
     elseif n_items > 0
         require_trailing_comma = any(
             x -> kind(x) === K"NewlineWs", @view(kids[(last_item_idx + 1):(closing_leaf_idx - 1)])
-        ) || has_newline_after_non_whitespace(kids[last_item_idx])
+        )
     end
 
     # Helper to compute the new state after a given item
@@ -440,14 +410,8 @@ function spaces_in_listlike(ctx::Context, node::Node)
     for i in (opening_leaf_idx + 1):(closing_leaf_idx - 1)
         kid′ = kids[i]
         this_kid_changed = false
-        first_item_in_implicit_tuple = implicit_tuple && i == opening_leaf_idx + 1
         if state === :expect_item
-            if first_item_in_implicit_tuple && kind(kid′) === K"Whitespace" && peek(i) !== K"Comment"
-                # Not allowed to touch this one I think?
-                accept_node!(ctx, kid′)
-                any_kid_changed && push!(kids′, kid′)
-            elseif kind(kid′) === K"Whitespace" && peek(i) !== K"Comment"
-                @assert !first_item_in_implicit_tuple # Unreachable?
+            if kind(kid′) === K"Whitespace" && peek(i) !== K"Comment"
                 # Delete whitespace unless followed by a comment
                 replace_bytes!(ctx, "", span(kid′))
                 this_kid_changed = true
@@ -456,37 +420,18 @@ function spaces_in_listlike(ctx::Context, node::Node)
                 end
             elseif kind(kid′) === K"NewlineWs" ||
                     (kind(kid′) === K"Whitespace" && peek(i) === K"Comment")
-                @assert !first_item_in_implicit_tuple # Unreachable?
                 # Newline here can happen if this kid is just after the opening leaf or if
                 # there is an empty line between items. No state change.
                 accept_node!(ctx, kid′)
                 any_kid_changed && push!(kids′, kid′)
             elseif kind(kid′) === K"Comment"
-                @assert !first_item_in_implicit_tuple # Unreachable?
                 accept_node!(ctx, kid′)
                 any_kid_changed && push!(kids′, kid′)
                 state = :expect_space # To ensure space after the comment
             else
                 # This is an item (probably?).
-                # Make sure it doesn't have leading or trailing whitespace.
-                if kind(first_leaf(kid′)) === K"Whitespace" && kind(second_leaf(kid′)) !== K"Comment" && !first_item_in_implicit_tuple
-                    # Delete the whitespace leaf
-                    kid_ws = first_leaf(kid′)
-                    replace_bytes!(ctx, "", span(kid_ws))
-                    kid′ = replace_first_leaf(kid′, nullnode)
-                    this_kid_changed = true
-                end
-                if kind(last_leaf(kid′)) === K"Whitespace"
-                    # Delete the whitespace leaf
-                    kid_ws = last_leaf(kid′)
-                    let pos = position(ctx.fmt_io)
-                        seek(ctx.fmt_io, pos + span(kid′) - span(kid_ws))
-                        replace_bytes!(ctx, "", span(kid_ws))
-                        seek(ctx.fmt_io, pos)
-                    end
-                    kid′ = replace_last_leaf(kid′, nullnode)
-                    this_kid_changed = true
-                end
+                @assert !JuliaSyntax.is_whitespace(first_leaf(kid′))
+                @assert !JuliaSyntax.is_whitespace(last_leaf(kid′))
                 if kind(kid′) === K"parameters" && require_trailing_comma &&
                         i == last_item_idx && !has_tag(kid′, TAG_TRAILING_COMMA)
                     # Tag the node to require a trailing comma
@@ -504,22 +449,15 @@ function spaces_in_listlike(ctx::Context, node::Node)
                         x -> !(JuliaSyntax.is_whitespace(x) || kind(x) in KSet", ;"),
                         verified_kids(kid′)
                     ) == 0
-                    # If kid is K"parameters" without items and we don't want a trailing
-                    # comma/semicolon we need to eat any whitespace kids (e.g. comments)
+                    # If kid is K"parameters" without items and we don't want the
+                    # K"parameters" node for the trailing semicolon we drop the entire node
                     grandkids = verified_kids(kid′)
-                    semi_idx = 1
-                    @assert kind(grandkids[semi_idx]) === K";"
-                    ws_idx = something(findnext(x -> kind(x) !== K"Whitespace", grandkids, semi_idx + 1), lastindex(grandkids) + 1)
+                    @assert length(grandkids) == 1 && kind(grandkids[1]) === K";"
                     any_kid_changed = true
                     if kids′ === kids
                         kids′ = kids[1:(i - 1)]
                     end
-                    replace_bytes!(ctx, "", mapreduce(span, +, grandkids[1:(ws_idx - 1)]; init = 0))
-                    for j in ws_idx:length(grandkids)
-                        grandkid = grandkids[j]
-                        accept_node!(ctx, grandkid)
-                        push!(kids′, grandkid)
-                    end
+                    replace_bytes!(ctx, "", span(kid′))
                 else
                     # Kid is now acceptable
                     any_kid_changed |= this_kid_changed
@@ -589,16 +527,7 @@ function spaces_in_listlike(ctx::Context, node::Node)
             elseif kind(kid′) === K"parameters"
                 # Note that some of these are not valid Julia syntax still parse
                 @assert kind(node) in KSet"call dotcall macrocall curly tuple vect ref braces"
-                if kind(first_leaf(kid′)) === K"Whitespace"
-                    # Delete the whitespace leaf
-                    kid_ws = first_leaf(kid′)
-                    replace_bytes!(ctx, "", span(kid_ws))
-                    kid′ = replace_first_leaf(kid′, nullnode)
-                    this_kid_changed = true
-                    # if kids′ === kids
-                    #     kids′ = kids[1:i - 1]
-                    # end
-                end
+                @assert !JuliaSyntax.is_whitespace(first_leaf(kid′))
                 if require_trailing_comma && !has_tag(kid′, TAG_TRAILING_COMMA)
                     # Tag the parameters node to require a trailing comma
                     kid′ = add_tag(kid′, TAG_TRAILING_COMMA)
@@ -617,32 +546,14 @@ function spaces_in_listlike(ctx::Context, node::Node)
                         x -> !(JuliaSyntax.is_whitespace(x) || kind(x) in KSet", ;"),
                         verified_kids(kid′)
                     ) == 0
-                    # If kid is K"parameters" without items and we don't want a trailing
-                    # comma/semicolon we need to eat any whitespace kids (e.g. comments)
+                    # If kid is K"parameters" without items and we don't want the
+                    # K"parameters" node for the trailing semicolon we drop the entire node
                     grandkids = verified_kids(kid′)
-                    semi_idx = 1
-                    @assert kind(grandkids[semi_idx]) === K";"
-                    ws_idx = findnext(x -> kind(x) !== K"Whitespace", grandkids, semi_idx + 1)
-                    if ws_idx !== nothing
-                        any_kid_changed = true
-                        if kids′ === kids
-                            kids′ = kids[1:(i - 1)]
-                        end
-                        replace_bytes!(ctx, "", mapreduce(span, +, grandkids[1:(ws_idx - 1)]; init = 0))
-                        for j in ws_idx:length(grandkids)
-                            grandkid = grandkids[j]
-                            accept_node!(ctx, grandkid)
-                            push!(kids′, grandkid)
-                        end
-                    else
-                        # Nothing in the parameter node needed, overwrite it fully
-                        any_kid_changed = true
-                        replace_bytes!(ctx, "", span(kid′))
-                        if any_kid_changed
-                            if kids′ === kids
-                                kids′ = kids[1:(i - 1)]
-                            end
-                        end
+                    @assert length(grandkids) == 1 && kind(grandkids[1]) === K";"
+                    any_kid_changed = true
+                    replace_bytes!(ctx, "", span(kid′))
+                    if kids′ === kids
+                        kids′ = kids[1:(i - 1)]
                     end
                 else
                     # TODO: Tag for requiring trailing comma.
@@ -686,52 +597,21 @@ function spaces_in_listlike(ctx::Context, node::Node)
                 accept_node!(ctx, kid′)
                 any_kid_changed && push!(kids′, kid′)
                 state = :expect_item
-            elseif kind(kid′) === K"Comment"
-                # Comments are accepted, state stays the same
-                # TODO: Make sure there is a space before the comment? Maybe that's not the
-                # responsibility of this function though.
-                accept_node!(ctx, kid′)
-                any_kid_changed && push!(kids′, kid′)
             else
-                # Probably a list item, look for leading whitespace, or insert.
+                # Probably a list item, insert a space before it
                 @assert !(kind(kid′) in KSet", ;")
-                if kind(first_leaf(kid′)) === K"NewlineWs" ||
-                        kind(first_leaf(kid′)) === K"Comment" ||
-                        (kind(first_leaf(kid′)) === K"Whitespace" && kind(second_leaf(kid′)) === K"Comment")
-                    # Newline, comment, or whitespace followed by comment
-                    accept_node!(ctx, kid′)
-                    any_kid_changed && push!(kids′, kid′)
-                    state = state_after_item(i, last_item_idx, require_trailing_comma)
-                elseif kind(first_leaf(kid′)) === K"Whitespace"
-                    ws_node = first_leaf(kid′)
-                    if span(ws_node) == 1
-                        accept_node!(ctx, kid′)
-                        any_kid_changed && push!(kids′, kid′)
-                    else
-                        kid′ = replace_first_leaf(kid′, ws)
-                        this_kid_changed = true
-                        if kids′ === kids
-                            kids′ = kids[1:(i - 1)]
-                        end
-                        replace_bytes!(ctx, " ", span(ws_node))
-                        accept_node!(ctx, kid′)
-                        push!(kids′, kid′)
-                    end
-                    state = state_after_item(i, last_item_idx, require_trailing_comma)
-                else
-                    # Insert a standalone space kid and then accept the current node
-                    this_kid_changed = true
-                    if kids′ === kids
-                        kids′ = kids[1:(i - 1)]
-                    end
-                    replace_bytes!(ctx, " ", 0)
-                    push!(kids′, ws)
-                    accept_node!(ctx, ws)
-                    push!(kids′, kid′)
-                    accept_node!(ctx, kid′)
-                    # Here we inserted a space and consumed the next item, moving on to comma
-                    state = state_after_item(i, last_item_idx, require_trailing_comma)
+                @assert !JuliaSyntax.is_whitespace(first_leaf(kid′))
+                this_kid_changed = true
+                if kids′ === kids
+                    kids′ = kids[1:(i - 1)]
                 end
+                replace_bytes!(ctx, " ", 0)
+                push!(kids′, ws)
+                accept_node!(ctx, ws)
+                push!(kids′, kid′)
+                accept_node!(ctx, kid′)
+                # Here we inserted a space and consumed the next item, moving on to comma
+                state = state_after_item(i, last_item_idx, require_trailing_comma)
             end
         else
             @assert state === :expect_closing
@@ -867,11 +747,10 @@ function no_spaces_around_x(ctx::Context, node::Node, is_x::F) where {F}
     end
 
     for (i, kid) in pairs(kids)
-        if (i == 1 || i == length(kids)) && kind(kid) === K"Whitespace"
-            # Leave any leading and trailing whitespace
-            accept_node!(ctx, kid)
-            any_changes && push!(kids′, kid)
-        elseif kind(kid) === K"Whitespace"
+        if kind(kid) === K"Whitespace"
+            # Leading and trailing whitespace should not be dropped but normalization should
+            # make sure these never exist.
+            @assert 1 < i < length(kids)
             # Ignore it but need to copy kids and re-write bytes
             any_changes = true
             if kids′ === kids
@@ -882,23 +761,6 @@ function no_spaces_around_x(ctx::Context, node::Node, is_x::F) where {F}
             @assert !JuliaSyntax.is_whitespace(kid) # Filtered out above
             if looking_for_x
                 @assert is_x(kid)::Bool
-            else
-                if i > first_x_idx
-                    # Remove leading whitespace
-                    ws_kid = first_leaf(kid)
-                    if kind(ws_kid) === K"Whitespace"
-                        kid = replace_first_leaf(kid, nullnode)
-                        replace_bytes!(ctx, "", span(ws_kid))
-                        any_changes = true
-                    end
-                end
-                if i < last_x_idx
-                    # Remove trailing whitespace
-                    ws_kid = last_leaf(kid)
-                    if kind(ws_kid) === K"Whitespace"
-                        unreachable()
-                    end
-                end
             end
             if any_changes
                 if kids === kids′
@@ -948,14 +810,13 @@ function spaces_in_export_public(ctx::Context, node::Node)
                 any_changes && push!(kids′, kid)
                 accept_node!(ctx, kid)
             elseif kind(kid) === K"Whitespace"
-                kid′ = replace_first_leaf(kid, spacenode)
-                replace_bytes!(ctx, " ", span(first_leaf(kid)))
+                replace_bytes!(ctx, " ", span(kid))
                 any_changes = true
                 if kids′ === kids
                     kids′ = kids[1:(i - 1)]
                 end
-                accept_node!(ctx, kid′)
-                push!(kids′, kid′)
+                accept_node!(ctx, spacenode)
+                push!(kids′, spacenode)
             elseif kind(kid) === K"Comment"
                 any_changes && push!(kids′, kid)
                 accept_node!(ctx, kid)
@@ -1021,35 +882,17 @@ function spaces_in_export_public(ctx::Context, node::Node)
     return any_changes ? make_node(node, kids′) : nothing
 end
 
-# Used in spaces_in_import_using. Well formatted importpath should have a single leading
-# space or a newline.
+# Used in `spaces_in_import_using` and `format_as`
 function format_importpath(ctx::Context, node::Node)
     @assert kind(node) === K"importpath"
-    pos = position(ctx.fmt_io)
-    spacebar = Node(JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), 1)
-    if kind(first_leaf(node)) === K"NewlineWs" ||
-            (kind(first_leaf(node)) === K"Whitespace" && span(first_leaf(node)) == 1)
-        # Newline or whitespace with correct span
-        node′ = nothing
-    elseif kind(first_leaf(node)) === K"Whitespace"
-        # Whitespace with incorrect span; replace with a single space
-        replace_bytes!(ctx, " ", span(first_leaf(node)))
-        node′ = replace_first_leaf(node, spacebar)
-    else
-        # No whitespace, insert
-        @assert kind(first_leaf(node)) in KSet"Identifier @ Comment" ||
-            JuliaSyntax.is_operator(first_leaf(node))
-        kids′ = copy(verified_kids(node))
-        pushfirst!(kids′, spacebar)
-        replace_bytes!(ctx, " ", 0)
-        node′ = make_node(node, kids′)
-    end
-    # Reset stream
-    seek(ctx.fmt_io, pos)
-    return node′
+    @assert !JuliaSyntax.is_whitespace(first_leaf(node))
+    return nothing
 end
 
-# Used in spaces_in_import_using.
+# Used in `spaces_in_import_using`
+# TODO: This doesn't handle comments and newlines which can be present on either side of the
+#       `as`. However, the asserts don't trigger on any julia package so can be fixed
+#       whenever someone files an issue about this :^)
 function format_as(ctx::Context, node::Node)
     @assert kind(node) === K"as"
     kids = verified_kids(node)
@@ -1062,11 +905,7 @@ function format_as(ctx::Context, node::Node)
     kid′ = kids[idx]
     @assert kind(kid′) === K"importpath"
     kid′′ = format_importpath(ctx, kid′)
-    if kid′′ !== nothing
-        any_changes = true
-        kid′ = kid′′
-        kids′ = [kid′]
-    end
+    @assert kid′′ === nothing
     accept_node!(ctx, kid′)
     # space before `as`
     idx += 1
@@ -1110,7 +949,7 @@ function format_as(ctx::Context, node::Node)
         accept_node!(ctx, spacebar)
         push!(kids′, spacebar)
     end
-    # Alias-identifier
+    # Alias-identifier (RHS of the `as`)
     idx += 1
     kid = kids[idx]
     @assert kind(kid) in KSet"Identifier $ @"
@@ -1134,6 +973,7 @@ function format_as(ctx::Context, node::Node)
     return any_changes ? make_node(node, kids′) : nothing
 end
 
+# TODO: This method is very similar to `spaces_in_export_public`
 function spaces_in_import_using(ctx::Context, node::Node)
     if !(kind(node) in KSet"import using" && !is_leaf(node))
         return nothing
@@ -1154,44 +994,86 @@ function spaces_in_import_using(ctx::Context, node::Node)
     @assert kind(kids[1]) in KSet"import using"
     accept_node!(ctx, kids[1])
 
-    state = :expect_item
+    spacebar = Node(JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), 1)
+
+    state = :expect_space
     i = 2
     while i <= length(kids)
         kid = kids[i]
         if state === :expect_item
-            @assert kind(kid) in KSet"importpath as"
-            if kind(kid) === K"importpath"
-                kid′ = format_importpath(ctx, kid)
+            state = :expect_comma
+            if kind(kid) in KSet"importpath as"
+                if kind(kid) === K"importpath"
+                    kid′ = format_importpath(ctx, kid)
+                    @assert kid′ === nothing
+                else
+                    kid′ = format_as(ctx, kid)
+                end
+                if kid′ !== nothing
+                    any_changes = true
+                    if kids′ === kids
+                        kids′ = kids[1:(i - 1)]
+                    end
+                    accept_node!(ctx, kid′)
+                    push!(kids′, kid′)
+                else
+                    accept_node!(ctx, kid)
+                    any_changes && push!(kids′, kid)
+                end
+            elseif kind(kid) in KSet"Comment NewlineWs"
+                any_changes && push!(kids′, kid)
+                accept_node!(ctx, kid)
+                state = kind(kid) === K"Comment" ? (:expect_space) : (:expect_item)
             else
-                @assert kind(kid) === K"as"
-                kid′ = format_as(ctx, kid)
+                unreachable()
             end
-            if kid′ !== nothing
+        elseif state === :expect_comma
+            state = :expect_space
+            if kind(kid) === K"Whitespace"
+                # Drop this node
                 any_changes = true
+                replace_bytes!(ctx, "", span(kid))
                 if kids′ === kids
                     kids′ = kids[1:(i - 1)]
                 end
-                accept_node!(ctx, kid′)
-                push!(kids′, kid′)
+                state = :expect_comma
             else
+                @assert kind(kid) in KSet": ,"
                 accept_node!(ctx, kid)
                 any_changes && push!(kids′, kid)
             end
-            state = :expect_comma
         else
-            @assert state === :expect_comma
-            if kind(kid) === K"Whitespace"
-                # Drop this node
+            @assert state === :expect_space
+            state = :expect_item
+            if kind(kid) === K"NewlineWs" || (kind(kid) === K"Whitespace" && span(kid) == 1)
+                # Newline or whitespace with correct span
+                accept_node!(ctx, kid)
+                any_changes && push!(kids′, kid)
+            elseif kind(kid) === K"Whitespace"
+                # Whitespace with incorrect span; replace with a single space
                 any_changes = true
-                replace_bytes!(ctx, "", span(kid))
+                replace_bytes!(ctx, " ", span(kid))
                 if kids′ === kids
                     kids′ = kids[1:(i - 1)]
                 end
-            else
-                @assert kind(kid) in KSet": ,"
-                accept_node!(ctx, kid)
+                accept_node!(ctx, spacebar)
+                push!(kids′, spacebar)
+            elseif kind(kid) === K"Comment"
                 any_changes && push!(kids′, kid)
-                state = :expect_item
+                accept_node!(ctx, kid)
+                state = :expect_space
+            else
+                # No whitespace, insert
+                @assert kind(kid) in KSet"Identifier importpath as"
+                @assert !JuliaSyntax.is_whitespace(first_leaf(kid))
+                any_changes = true
+                replace_bytes!(ctx, " ", 0)
+                if kids′ === kids
+                    kids′ = kids[1:(i - 1)]
+                end
+                accept_node!(ctx, spacebar)
+                push!(kids′, spacebar)
+                continue # Skip increment of i
             end
         end
         i += 1
@@ -1279,9 +1161,9 @@ function spaces_around_keywords(ctx::Context, node::Node)
                 state = :looking_for_space
                 # `do` should only be followed by space if the argument-tuple is non-empty
                 if kind(node) === K"do"
-                    nkid = kids[i + 1]
-                    @assert kind(nkid) === K"tuple"
-                    if !any(x -> !(JuliaSyntax.is_whitespace(x) || kind(x) === K";"), verified_kids(nkid))
+                    tupleidx = findnext(x -> kind(x) === K"tuple", kids, i + 1)::Int
+                    tuple = kids[tupleidx]
+                    if !any(x -> !(JuliaSyntax.is_whitespace(x) || kind(x) === K";"), verified_kids(tuple))
                         state = :closing
                     end
                 end
@@ -1298,7 +1180,7 @@ function spaces_around_keywords(ctx::Context, node::Node)
             end
         elseif state === :looking_for_space
             if (kind(kid) === K"Whitespace" && span(kid) == 1) ||
-                    kind(kid) === K"NewlineWs" || kind(first_leaf(kid)) === K"NewlineWs"
+                    kind(kid) === K"NewlineWs"
                 if kind(kid) === K"NewlineWs"
                     # Is a newline instead of a space accepted for any other case?
                     @assert kind(node) in KSet"where local global const"
@@ -1314,25 +1196,8 @@ function spaces_around_keywords(ctx::Context, node::Node)
                 replace_bytes!(ctx, " ", span(kid))
                 push!(kids′, ws)
                 accept_node!(ctx, ws)
-            elseif space_after && kind(first_leaf(kid)) === K"Whitespace"
-                kid_ws = first_leaf(kid)
-                if span(kid_ws) == 1
-                    accept_node!(ctx, kid)
-                    any_changes && push!(kids′, kid)
-                else
-                    kid′ = replace_first_leaf(kid, ws)
-                    @assert span(kid′) == span(kid) - span(kid_ws) + 1
-                    replace_bytes!(ctx, " ", span(kid_ws))
-                    accept_node!(ctx, kid′)
-                    any_changes = true
-                    if kids′ === kids
-                        kids′ = kids[1:(i - 1)]
-                    end
-                    push!(kids′, kid′)
-                end
-            elseif !space_after && kind(last_leaf(kid)) === K"Whitespace"
-                unreachable()
             else
+                @assert kind(first_leaf(kid)) !== K"Whitespace"
                 # Reachable in e.g. `T where{T}`, `if(`, ... insert space
                 @assert kind(node) in KSet"where if elseif while do function return local global module baremodule"
                 any_changes = true
@@ -1416,11 +1281,8 @@ function replace_with_in_filter(ctx::Context, node::Node)
     @assert kind(node) === K"filter" && !is_leaf(node)
     pos = position(ctx.fmt_io)
     kids = verified_kids(node)
-    idx = findfirst(x -> kind(x) in KSet"= cartesian_iterator" && !is_leaf(x), kids)::Int
-    for i in 1:(idx - 1)
-        accept_node!(ctx, kids[i])
-    end
-    kid = kids[idx]
+    kid = kids[1]
+    @assert kind(kid) in KSet"= cartesian_iterator" && !is_leaf(kid)
     if kind(kid) === K"="
         kid′ = replace_with_in(ctx, kid)
     else
@@ -1432,7 +1294,7 @@ function replace_with_in_filter(ctx::Context, node::Node)
         return nothing
     else
         kids = copy(kids)
-        kids[idx] = kid′
+        kids[1] = kid′
         return make_node(node, kids)
     end
 end
@@ -1490,10 +1352,13 @@ function for_loop_use_in(ctx::Context, node::Node)
         for i in next_index:for_index
             accept_node!(ctx, kids[i])
         end
-        # The for loop specification node can be either K"=" or K"cartesian_iterator"
-        for_spec_index = for_index + 1
+        # The for loop specification node can be one of K"=", K"cartesian_iterator", or
+        # K"filter"
+        for_spec_index = findnext(x -> kind(x) in KSet"= cartesian_iterator filter", kids, for_index + 1)::Int
         for_spec_node = kids[for_spec_index]
-        @assert kind(for_spec_node) in KSet"= cartesian_iterator filter"
+        for i in (for_index + 1):(for_spec_index - 1)
+            accept_node!(ctx, kids[i])
+        end
         if kind(for_spec_node) === K"="
             for_spec_node′ = replace_with_in(ctx, for_spec_node)
         elseif kind(for_spec_node) === K"filter"
@@ -1544,6 +1409,7 @@ function braces_around_where_rhs(ctx::Context, node::Node)
     pos = position(ctx.fmt_io)
     where_idx = findfirst(x -> is_leaf(x) && kind(x) === K"where", kids)::Int
     rhs_idx = findnext(!JuliaSyntax.is_whitespace, kids, where_idx + 1)::Int
+    @assert rhs_idx == lastindex(kids)
     rhs = kids[rhs_idx]
     if kind(rhs) === K"braces"
         return nothing
@@ -1566,11 +1432,6 @@ function braces_around_where_rhs(ctx::Context, node::Node)
     accept_node!(ctx, rhs)
     replace_bytes!(ctx, "}", 0)
     accept_node!(ctx, closing_brace)
-    # Accept any remaining kids
-    for i in (rhs_idx + 1):length(kids)
-        accept_node!(ctx, kids[i])
-        push!(kids′, kids[i])
-    end
     # Reset stream and return
     seek(ctx.fmt_io, pos)
     return make_node(node, kids′)
@@ -1594,18 +1455,15 @@ function parens_around_op_calls_in_colon(ctx::Context, node::Node)
             end
             grandkids = verified_kids(kid)
             first_non_ws = findfirst(!JuliaSyntax.is_whitespace, grandkids)::Int
+            @assert first_non_ws == firstindex(grandkids)
             last_non_ws = findlast(!JuliaSyntax.is_whitespace, grandkids)::Int
-            # Extract whitespace grandkids to become kids
-            for j in 1:(first_non_ws - 1)
-                accept_node!(ctx, grandkids[j])
-                push!(kids′, grandkids[j])
-            end
+            @assert last_non_ws == lastindex(grandkids)
             # Create the parens node
             opening_paren = Node(JuliaSyntax.SyntaxHead(K"(", 0), 1)
             replace_bytes!(ctx, "(", 0)
             accept_node!(ctx, opening_paren)
             parens_kids = [opening_paren]
-            kid′_kids = grandkids[first_non_ws:last_non_ws]
+            kid′_kids = copy(grandkids)
             kid′ = make_node(kid, kid′_kids)
             accept_node!(ctx, kid′)
             push!(parens_kids, kid′)
@@ -1615,10 +1473,6 @@ function parens_around_op_calls_in_colon(ctx::Context, node::Node)
             push!(parens_kids, closing_paren)
             parens = Node(JuliaSyntax.SyntaxHead(K"parens", 0), parens_kids)
             push!(kids′, parens)
-            for j in (last_non_ws + 1):length(grandkids)
-                accept_node!(ctx, grandkids[j])
-                push!(kids′, grandkids[j])
-            end
             any_changes = true
         else
             accept_node!(ctx, kid)
@@ -1895,6 +1749,8 @@ function indent_begin(ctx::Context, node::Node, block_kind = K"begin")
     return any_kid_changed ? make_node(node, kids) : nothing
 end
 
+# This function ensures that the block start, and ends, with a newline, and make sure that
+# the trailing newline is tagged with TAG_PRE_DEDENT.
 function indent_block(
         ctx::Context, node::Node; allow_empty::Bool = true, do_indent::Bool = true
     )
@@ -2032,12 +1888,6 @@ function indent_block(
         if kind(kids′[insert_idx]) === K"Whitespace"
             kids′, ws = popatview!(kids′, insert_idx)
             wsspn = span(ws)
-        elseif !is_leaf(kids′[insert_idx]) &&
-                kind(first_leaf(kids′[insert_idx])) === K"Whitespace"
-            ws = first_leaf(kids′[insert_idx])
-            kids′[insert_idx] = replace_first_leaf(kids′[insert_idx], nullnode)
-            any_kid_changed = true
-            wsspn = span(ws)
         end
         # If we end up in this code path we are most likely splitting a single line block
         # into multiples lines. This means that we haven't yet updated the indent level for
@@ -2373,9 +2223,7 @@ function indent_listlike(
     @assert multiline
     kid = kids[open_idx + 1]
     idx_after_leading_nl = open_idx + 2
-    if kind(kid) === K"NewlineWs" ||
-            kind(first_leaf(kid)) === K"NewlineWs" ||
-            peek_leafs(kid, KSet"Comment NewlineWs") || peek_leafs(kid, KSet"Whitespace Comment NewlineWs")
+    if kind(kid) === K"NewlineWs"
         # Newline or newlinde hidden in first item
         any_kid_changed && push!(kids′, kid)
         accept_node!(ctx, kid)
@@ -2405,17 +2253,6 @@ function indent_listlike(
             any_kid_changed = true
             push!(kids′, kid)
             accept_node!(ctx, kid)
-        elseif kind(first_leaf(kid)) === K"Whitespace"
-            grandkid = first_leaf(kid)
-            grandkid = Node(JuliaSyntax.SyntaxHead(K"NewlineWs", JuliaSyntax.TRIVIA_FLAG), span(grandkid) + 1)
-            replace_bytes!(ctx, "\n", 0)
-            kid = replace_first_leaf(kid, grandkid)
-            if kids′ === kids
-                kids′ = kids[1:(open_idx - 1)]
-            end
-            any_kid_changed = true
-            push!(kids′, kid)
-            accept_node!(ctx, kid)
         elseif kind(kid) === K"parameters"
             # For parameters we want the newline after the semi-colon
             grandkids = verified_kids(kid)
@@ -2465,6 +2302,7 @@ function indent_listlike(
                 accept_node!(ctx, kid)
             end
         else
+            @assert kind(first_leaf(kid)) !== K"Whitespace"
             nlws = Node(JuliaSyntax.SyntaxHead(K"NewlineWs", JuliaSyntax.TRIVIA_FLAG), 1)
             replace_bytes!(ctx, "\n", 0)
             if kids′ === kids
@@ -2498,8 +2336,7 @@ function indent_listlike(
     else
         kid = kids[close_idx - 1]
     end
-    if (kind(kid) === K"NewlineWs" && has_tag(kid, TAG_PRE_DEDENT)) ||
-            (kind(last_leaf(kid)) === K"NewlineWs" && has_tag(last_leaf(kid), TAG_PRE_DEDENT))
+    if kind(kid) === K"NewlineWs" && has_tag(kid, TAG_PRE_DEDENT)
         # Newline or newlinde hidden in first item with tag
         any_kid_changed && push!(kids′, kid)
         accept_node!(ctx, kid)
@@ -2513,19 +2350,8 @@ function indent_listlike(
         any_kid_changed = true
         push!(kids′, kid)
         accept_node!(ctx, kid)
-    elseif kind(last_leaf(kid)) === K"NewlineWs"
-        # Hidden newline without tag
-        grandkid = last_leaf(kid)
-        @assert !has_tag(grandkid, TAG_PRE_DEDENT)
-        grandkid = add_tag(grandkid, TAG_PRE_DEDENT)
-        kid = replace_last_leaf(kid, grandkid)
-        if kids′ === kids
-            kids′ = kids[1:(close_idx - 2)]
-        end
-        any_kid_changed = true
-        push!(kids′, kid)
-        accept_node!(ctx, kid)
     else
+        @assert kind(last_leaf(kid)) !== K"NewlineWs"
         # Need to insert a newline. Note that we tag the new newline directly since it
         # is the responsibility of this function (otherwise there would just be an extra
         # repetitive call to add it anyway).
@@ -2540,26 +2366,8 @@ function indent_listlike(
             any_kid_changed = true
             push!(kids′, kid)
             accept_node!(ctx, kid)
-        elseif kind(last_leaf(kid)) === K"Whitespace"
-            # TODO: I believe this is only reachable because whitespace isn't trimmed from
-            #       vcat nodes like function calls and tuples etc.
-            @assert kind(node) in KSet"vcat typed_vcat"
-            # Since the whitespace is inside the kid node we assume that a newlinews would
-            # have ended up inside too. We also remove all existing whitespace since it
-            # would otherwise have to be cleaned up by the trailing whitespace pass.
-            accept_node!(ctx, kid)
-            seek(ctx.fmt_io, position(ctx.fmt_io) - span(last_leaf(kid)))
-            replace_bytes!(ctx, "\n", span(last_leaf(kid)))
-            nlws = Node(JuliaSyntax.SyntaxHead(K"NewlineWs", JuliaSyntax.TRIVIA_FLAG), 1)
-            nlws = add_tag(nlws, TAG_PRE_DEDENT)
-            accept_node!(ctx, nlws)
-            kid′ = replace_last_leaf(kid, nlws)
-            if kids′ === kids
-                kids′ = kids[1:(open_idx - 1)]
-            end
-            any_kid_changed = true
-            push!(kids′, kid′)
         else
+            @assert kind(last_leaf(kid)) !== K"Whitespace"
             # Note that this is a trailing newline and should be put after this item
             if kids′ === kids
                 kids′ = kids[1:(open_idx - 1)]
@@ -2587,11 +2395,7 @@ function indent_listlike(
     any_kid_changed && push!(kids′, kid)
     accept_node!(ctx, kid)
     # Keep any remaining kids
-    for i in (close_idx + 1):length(kids)
-        kid = kids[i]
-        any_kid_changed && push!(kids′, kid)
-        accept_node!(ctx, kid)
-    end
+    @assert close_idx == lastindex(kids)
     # Reset stream
     seek(ctx.fmt_io, pos)
     # Make a new node and return
@@ -2815,6 +2619,7 @@ function indent_assignment(ctx::Context, node::Node)
     @assert kind(node) === kind(kids[eqidx])
     @assert length(kids) > eqidx
     rhsidx = findnext(!JuliaSyntax.is_whitespace, kids, eqidx + 1)::Int
+    @assert rhsidx == lastindex(kids)
     r = (eqidx + 1):(rhsidx - 1)
     length(r) == 0 && return nothing
     rhs = kids[rhsidx]
@@ -2869,9 +2674,6 @@ function indent_assignment(ctx::Context, node::Node)
     end
     if changed
         @assert kids !== kids′
-        for i in (rhsidx + 1):length(kids)
-            push!(kids′, kids[i])
-        end
         return make_node(node, kids′)
     else
         return nothing
@@ -3001,24 +2803,11 @@ function indent_module(ctx::Context, node::Node; do_indent::Bool = true)
         kids[mod_idx] = add_tag(mod_node, TAG_INDENT)
         any_kid_changed = true
     end
-    # Next we expect whitespace + identifier, but can also be expression with whitespace
-    # hidden inside...
-    space_idx = 2
-    space_node = kids[space_idx]
-    if kind(space_node) === K"Whitespace"
-        # Now we need an identifier, var" or parens...
-        id_idx = 3
-        id_node = kids[id_idx]
-        @assert kind(id_node) in KSet"Identifier var parens"
-        block_idx = 4
-    else
-        # This can be reached if the module name is interpolated or parenthesized, for
-        # example.
-        @assert kind(first_leaf(space_node)) in KSet"Whitespace ("
-        @assert !JuliaSyntax.is_whitespace(space_node)
-        block_idx = 3
-    end
+    # Next we expect whitespace + module identifier
+    modname_idx = findnext(x -> !JuliaSyntax.is_whitespace(x), kids, mod_idx + 1)::Int
     # Next node is the module body block.
+    block_idx = findnext(x -> kind(x) === K"block", kids, modname_idx + 1)::Int
+    @assert block_idx == modname_idx + 1
     let p = position(ctx.fmt_io)
         for i in 1:(block_idx - 1)
             accept_node!(ctx, kids[i])
@@ -3085,17 +2874,12 @@ function indent_local_global(ctx::Context, node::Node)
     kids = verified_kids(node)
     kw = findfirst(x -> is_leaf(x) && kind(x) in KSet"local global", kids)::Int
     nonws = findnext(!JuliaSyntax.is_whitespace, kids, kw + 1)::Int
+    @assert kind(first_leaf(kids[nonws])) !== K"NewlineWs"
     changed = false
-    for i in (kw + 1):nonws
+    for i in (kw + 1):(nonws - 1)
         if kind(kids[i]) === K"NewlineWs" && !has_tag(kids[i], TAG_LINE_CONT)
             kids[i] = add_tag(kids[i], TAG_LINE_CONT)
             changed = true
-        elseif i == nonws && kind(first_leaf(kids[i])) === K"NewlineWs" &&
-                !has_tag(first_leaf(kids[i]), TAG_LINE_CONT)
-            kid′ = replace_first_leaf(kids[i], add_tag(first_leaf(kids[i]), TAG_LINE_CONT))
-            @assert kid′ !== nothing
-            kids[i] = kid′
-            changed = true
         end
     end
     return changed ? make_node(node, kids) : nothing
@@ -3503,16 +3287,10 @@ function return_node(ctx::Context, ret::Node)
         push!(kids, ws)
         push!(kids, ret)
     else
-        @assert kind(first_leaf(ret)) !== K"NewlineWs"
-        has_ws = kind(first_leaf(ret)) === K"Whitespace"
-        if has_ws
-            replace_bytes!(ctx, "return", 0)
-            push!(kids, ret)
-        else
-            replace_bytes!(ctx, "return ", 0)
-            push!(kids, ws)
-            push!(kids, ret)
-        end
+        @assert !(kind(first_leaf(ret)) in KSet"NewlineWs Whitespace")
+        replace_bytes!(ctx, "return ", 0)
+        push!(kids, ws)
+        push!(kids, ret)
     end
     return Node(JuliaSyntax.SyntaxHead(K"return", 0), kids)
 end
@@ -3549,7 +3327,7 @@ function has_return(node::Node)
         # that is the job of a linter or something I guess.
         return findfirst(x -> kind(x) === K"return", kids) !== nothing
     else
-        error("unreachable")
+        unreachable()
     end
 end
 
@@ -3587,11 +3365,9 @@ function explicit_return_block(ctx, node)
         if kind(rexpr) === K"call"
             call_kids = verified_kids(rexpr)
             fname_idx = findfirst(!JuliaSyntax.is_whitespace, call_kids)::Int
+            @assert fname_idx == firstindex(call_kids)
             local fname
             let p = position(ctx.fmt_io)
-                for i in 1:(fname_idx - 1)
-                    accept_node!(ctx, call_kids[i])
-                end
                 fname = String(read_bytes(ctx, call_kids[fname_idx]))
                 seek(ctx.fmt_io, p)
             end
@@ -3620,11 +3396,8 @@ function explicit_return_block(ctx, node)
                 seek(ctx.fmt_io, position(ctx.fmt_io) - spn)
                 rexpr_idx -= 1
                 @assert kind(first_leaf(rexpr)) !== K"Whitespace"
-            elseif kind(first_leaf(rexpr)) === K"Whitespace"
-                # The first child in the return node is whitespace
-                spn = span(first_leaf(rexpr))
-                rexpr = replace_first_leaf(rexpr, nullnode)
             end
+            @assert kind(first_leaf(rexpr)) !== K"Whitespace"
             nl = "\n" * " "^(4 * ctx.indent_level)
             nlnode = Node(JuliaSyntax.SyntaxHead(K"NewlineWs", JuliaSyntax.TRIVIA_FLAG), sizeof(nl))
             insert!(kids′, rexpr_idx, nlnode)


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index f1b08dc..78e9d28 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -59,6 +59,30 @@ end
     @test KSet"Whitespace ; Whitespace" == (K"Whitespace", K";", K"Whitespace")
 end
 
+@testset "syntax tree normalization" begin
+    function rparse(str)
+        node = Runic.Node(JuliaSyntax.parseall(JuliaSyntax.GreenNode, str))
+        Runic.normalize_tree!(node)
+        return node
+    end
+    function check_no_leading_trailing_ws(node)
+        Runic.is_leaf(node) && return
+        kids = Runic.verified_kids(node)
+        if length(kids) > 0
+            @test !JuliaSyntax.is_whitespace(kids[1])
+            @test !JuliaSyntax.is_whitespace(kids[end])
+        end
+        for kid in kids
+            check_no_leading_trailing_ws(kid)
+        end
+    end
+    # Random things found in the ecosystem
+    str = "[\n    # c\n    0.5 1.0\n    1.0 2.0\n    # c\n]"
+    check_no_leading_trailing_ws(rparse(str))
+    str = "[0.5 1.0; 1.0 2.0;;; 4.5 6.0; 6.0 8.0]"
+    check_no_leading_trailing_ws(rparse(str))
+end
+
 @testset "Trailing whitespace" begin
     io = IOBuffer()
     println(io, "a = 1  ") # Trailing space
@@ -516,8 +540,8 @@ end
             # After `local`, `global`, and `const` a newline can be used instead of a space
             @test format_string("$(word)$(sp)\n$(sp)$(rhs)") == "$(word)\n    $(rhs)"
         end
-        @test_broken format_string("global\n\nx = 1") == "global\n\n    x = 1" # TODO: Fix double peek
-        @test_broken format_string("lobal\n\nx = 1") == "local\n\n    x = 1" # TODO: Fix double peek
+        @test format_string("global\n\nx = 1") == "global\n\n    x = 1"
+        @test format_string("local\n\nx = 1") == "local\n\n    x = 1"
         @test format_string("const$(sp)x = 1") == "const x = 1"
         # After `where` a newline can be used instead of a space
         @test format_string("A$(sp)where$(sp)\n{A}") == "A where\n{A}"
@@ -935,6 +959,7 @@ end
         @test format_string("$(verb) $(sp)A$(sp),$(sp)B") == "$(verb) A, B"
         @test format_string("$(verb) A$(sp),\nB") == "$(verb) A,\n    B"
         @test format_string("$(verb) \nA$(sp),\nB") == "$(verb)\n    A,\n    B"
+        @test format_string("$(verb) $(sp)A,$(sp)\n\n$(sp)B") == "$(verb) A,\n\n    B"
         # Colon lists
         for a in ("a", "@a", "*")
             @test format_string("$(verb) $(sp)A: $(sp)$(a)") == "$(verb) A: $(a)"
@@ -1229,6 +1254,8 @@ end
         # Trailing semicolon before ws+comment
         f; # comment
         f;; # comment
+        # Trailing semicolon with whitespace on both sides
+        g ; # comment
     """
     bodyfmt = """
         # Semicolons on their own lines
@@ -1252,6 +1279,8 @@ end
         # Trailing semicolon before ws+comment
         f  # comment
         f   # comment
+        # Trailing semicolon with whitespace on both sides
+        g   # comment
     """
     for prefix in (
             "begin", "quote", "for i in I", "let", "let x = 1", "while cond",
@@ -1453,6 +1482,17 @@ end
             "1 + 1\n$off #src\n1+1\n$on #src\n1 + 1"
         @test format_string("1+1\n#src $off\n1+1\n#src $on\n1+1") ==
             "1 + 1\n#src $off\n1+1\n#src $on\n1 + 1"
+        # Toggles inside literal array expression (fixed by normalization, PR #101)
+        let str = """
+            [
+                $off
+                 1.10  1.20  1.30
+                -2.10 -1.20 +1.30
+                f( a+b )
+                $on
+            ]"""
+            @test format_string(str) == str
+        end
     end
 end