Repository: fredrikekre/Runic.jl (julia)

Issue:
List of `let` variables not indented
For example,

```julia
let a = 1,
    b = 2
    a + b
end
```

should become

```julia
let a = 1,
        b = 2
    a + b
end
```

Pull request description:
This patch fixes a bug where multiline variable blocks in let would not intent correctly, fixes #97.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52d5801..53e50c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
  - Fix a bug that caused "single space after keyword" to not apply after the `function`
    keyword in non-standard function definitions. ([#113])
+ - Fix a bug that caused multiline variable blocks in `let` to not indent correctly ([#97],
+   [#116]). This bug is classified as a [spec-bug] and the fix will result in diffs like the
+   following:
+   ```diff
+    let a = 1,
+   -    b = 2
+   +        b = 2
+        a + b
+    end
+   ```
 
 ## [v1.0.1] - 2024-11-28
 ### Fixed
@@ -17,11 +27,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [v1.0.0] - 2024-11-06
 First stable release of Runic.jl. See [README.md](README.md) for details and documentation.
 
+[spec-bug]: https://github.com/fredrikekre/Runic.jl?tab=readme-ov-file#version-policy
+
 
 <!-- Links generated by Changelog.jl -->
 
 [v1.0.0]: https://github.com/fredrikekre/Runic.jl/releases/tag/v1.0.0
 [v1.0.1]: https://github.com/fredrikekre/Runic.jl/releases/tag/v1.0.1
+[#97]: https://github.com/fredrikekre/Runic.jl/issues/97
 [#109]: https://github.com/fredrikekre/Runic.jl/issues/109
 [#110]: https://github.com/fredrikekre/Runic.jl/issues/110
 [#113]: https://github.com/fredrikekre/Runic.jl/issues/113
+[#116]: https://github.com/fredrikekre/Runic.jl/issues/116
diff --git a/src/runestone.jl b/src/runestone.jl
index cfb2541..5b492e1 100644
--- a/src/runestone.jl
+++ b/src/runestone.jl
@@ -1640,6 +1640,33 @@ function indent_function_or_macro(ctx::Context, node::Node)
     return any_kid_changed ? make_node(node, kids) : nothing
 end
 
+# Soft-indentation between the variables
+function indent_let_varblock(ctx::Context, node::Node)
+    @assert kind(node) === K"block" && !is_leaf(node)
+    kids = verified_kids(node)
+    changed = false
+    if length(kids) == 0
+        @assert span(node) == 0
+        # Empty block, but where are spaces and comments?
+        return nothing
+    end
+    # First node *must* be a space (?)
+    i = 1
+    kid = kids[i]
+    @assert kind(kid) === K"Whitespace"
+    i = findnext(x -> !JuliaSyntax.is_whitespace(x), kids, i + 1)
+    i === nothing && return nothing
+    @assert kind(kids[i]) in KSet"Identifier = $ macrocall" # This is a bit unnecessary
+    while (i = findnext(x -> kind(x) === K"NewlineWs", kids, i + 1); i !== nothing)
+        @assert kind(kids[i]) === K"NewlineWs"
+        if !has_tag(kids[i], TAG_LINE_CONT)
+            kids[i] = add_tag(kids[i], TAG_LINE_CONT)
+            changed = true
+        end
+    end
+    return changed ? make_node(node, kids) : nothing
+end
+
 function indent_let(ctx::Context, node::Node)
     kids = verified_kids(node)
     any_kid_changed = false
@@ -1651,13 +1678,24 @@ function indent_let(ctx::Context, node::Node)
         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)
+    # Second node is the variables block
     vars_idx = 2
     vars_node = kids[vars_idx]
     @assert !is_leaf(vars_node) && kind(vars_node) === K"block"
     if span(vars_node) > 0 && length(verified_kids(vars_node)) > 0
         @assert kind(last_leaf(vars_node)) !== "NewlineWs"
     end
+    let p = position(ctx.fmt_io)
+        for i in 1:(vars_idx - 1)
+            accept_node!(ctx, kids[i])
+        end
+        vars_node′ = indent_let_varblock(ctx, vars_node)
+        if vars_node′ !== nothing
+            kids[vars_idx] = vars_node′
+            any_kid_changed = true
+        end
+        seek(ctx.fmt_io, p)
+    end
     # # Third node is the NewlineWs before the block
     # ln_idx = 3
     # ln_node = kids[ln_idx]


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 0445b39..b491339 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -912,6 +912,12 @@ end
         # K"cartesian_iterator"
         @test format_string("for i in I,\n$(sp)j in J\n# body\nend") ==
             "for i in I,\n        j in J\n    # body\nend"
+        # K"let"
+        for a in ("x = 1", "x", "@inline foo() = 1", "\$x"),
+                b in ("y = 1", "y", "@inline bar() = 1", "\$y")
+            @test format_string("let $(a),\n$(sp)$(b)\n    nothing\nend") ==
+                "let $(a),\n        $(b)\n    nothing\nend"
+        end
     end
 end