Browse Source

Indent: recurse into multiline strings, closes #27

pull/30/head
Fredrik Ekre 1 year ago
parent
commit
aeabe11718
No known key found for this signature in database
GPG Key ID: DE82E6D5E364C0A2
  1. 1
      src/Runic.jl
  2. 52
      src/main.jl
  3. 123
      src/runestone.jl
  4. 30
      test/runtests.jl

1
src/Runic.jl

@ -318,6 +318,7 @@ function format_node!(ctx::Context, node::Node)::Union{Node, Nothing, NullNode}
@return_something parens_around_op_calls_in_colon(ctx, node) @return_something parens_around_op_calls_in_colon(ctx, node)
@return_something for_loop_use_in(ctx, node) @return_something for_loop_use_in(ctx, node)
@return_something braces_around_where_rhs(ctx, node) @return_something braces_around_where_rhs(ctx, node)
@return_something indent_multiline_strings(ctx, node)
@return_something four_space_indent(ctx, node) @return_something four_space_indent(ctx, node)
@return_something spaces_in_listlike(ctx, node) @return_something spaces_in_listlike(ctx, node)
ctx.call_depth -= 1 ctx.call_depth -= 1

52
src/main.jl

@ -37,42 +37,42 @@ function print_help()
printstyled(io, "DESCRIPTION\n", bold = true) printstyled(io, "DESCRIPTION\n", bold = true)
println( println(
io, """ io, """
`Runic.main` (typically invoked as `julia -m Runic`) formats Julia source `Runic.main` (typically invoked as `julia -m Runic`) formats Julia source
code using the Runic.jl formatter. code using the Runic.jl formatter.
""", """,
) )
printstyled(io, "OPTIONS\n", bold = true) printstyled(io, "OPTIONS\n", bold = true)
println( println(
io, """ io, """
<path>... <path>...
Input path(s) (files and/or directories) to process. For directories, Input path(s) (files and/or directories) to process. For directories,
all files (recursively) with the '*.jl' suffix are used as input files. all files (recursively) with the '*.jl' suffix are used as input files.
If path is `-` input is read from stdin and output written to stdout. If path is `-` input is read from stdin and output written to stdout.
-c, --check -c, --check
Do not write output and exit with a non-zero code if the input is not Do not write output and exit with a non-zero code if the input is not
formatted correctly. formatted correctly.
-d, --diff -d, --diff
Print the diff between the input and formatted output to stderr. Print the diff between the input and formatted output to stderr.
Requires `git` or `diff` to be installed. Requires `git` or `diff` to be installed.
--fail-fast --fail-fast
Exit immediately after the first error. Only applicable when formatting Exit immediately after the first error. Only applicable when formatting
multiple files in the same invocation. multiple files in the same invocation.
--help --help
Print this message. Print this message.
-i, --inplace -i, --inplace
Edit files in place. This option is required when passing multiple input Edit files in place. This option is required when passing multiple input
paths. paths.
-o, --output <file> -o, --output <file>
Output file to write formatted code to. If the specified file is `-` Output file to write formatted code to. If the specified file is `-`
output is written to stdout. This option can not be used together with output is written to stdout. This option can not be used together with
multiple input paths. multiple input paths.
""", """,
) )
return return
end end

123
src/runestone.jl

@ -2757,3 +2757,126 @@ function insert_delete_mark_newlines(ctx::Context, node::Node)
end end
return nothing return nothing
end end
function indent_multiline_strings(ctx::Context, node::Node)
if !(kind(node) in KSet"string cmdstring" && JuliaSyntax.has_flags(node, JuliaSyntax.TRIPLE_STRING_FLAG))
return nothing
end
triplekind = kind(node) === K"string" ? K"\"\"\"" : K"```"
itemkind = kind(node) === K"string" ? K"String" : K"CmdString"
indent_span = 4 * ctx.indent_level
indented = indent_span > 0
pos = position(ctx.fmt_io)
kids = verified_kids(node)
kids′ = kids
any_changes = false
# Fastpath for the common case of top level multiline strings like e.g. docstrings
if !indented && findfirst(x -> kind(x) === K"Whitespace", kids) === nothing
return nothing
end
# Opening triple quote
open_idx = findfirst(x -> kind(x) === triplekind, kids)
close_idx = findlast(x -> kind(x) === triplekind, kids)
@assert close_idx == length(kids) # ?
open_kid = kids[open_idx]
@assert kind(open_kid) === triplekind
accept_node!(ctx, open_kid)
# Loop over the lines/expressions
idx = open_idx + 1
state = :expect_something
while idx < close_idx
kid = kids[idx]
if state === :expect_something
if kind(kid) === itemkind
if indented && read_bytes(ctx, kid)[end] == UInt8('\n')
state = :expect_indent_ws
end
accept_node!(ctx, kid)
any_changes && push!(kids′, kid)
elseif kind(kid) === K"Whitespace"
# Delete this one
replace_bytes!(ctx, "", span(kid))
if kids′ === kids
kids′ = kids[1:(idx - 1)]
end
any_changes = true
else
accept_node!(ctx, kid)
any_changes && push!(kids′, kid)
end
else
@assert state === :expect_indent_ws
state = :expect_something
if kind(kid) === itemkind && span(kid) == 1 && peek(ctx.fmt_io) == UInt8('\n')
# If this line is empty there shouldn't be a whitespace node. Switch the
# state and loop around with the same idx.
state = :expect_something
continue # Skip the index increment
elseif begin
cond = false
if kind(kid) === K"Whitespace" && idx + 1 < close_idx &&
kind(kids[idx + 1]) === itemkind && span(kids[idx + 1]) == 1
peekpos = position(ctx.fmt_io)
accept_node!(ctx, kid)
accept_node!(ctx, kids[idx + 1])
seek(ctx.fmt_io, position(ctx.fmt_io) - 1)
cond = peek(ctx.fmt_io) == UInt8('\n')
seek(ctx.fmt_io, peekpos)
end
cond
end
# If this whitespace is followed by an empty string it should be deleted
state = :expect_something
continue # Skip the index increment
elseif kind(kid) === K"Whitespace" && span(kid) == indent_span
@assert all(x -> x === UInt8(' '), read_bytes(ctx, kid))
accept_node!(ctx, kid)
any_changes && push!(kids′, kid)
elseif kind(kid) === K"Whitespace"
replace_bytes!(ctx, " "^indent_span, span(kid))
if kids′ === kids
kids′ = kids[1:(idx - 1)]
end
kid′ = Node(head(kid), indent_span, tags(kid))
any_changes = true
push!(kids′, kid′)
accept_node!(ctx, kid′)
else
replace_bytes!(ctx, " "^indent_span, 0)
if kids′ === kids
kids′ = kids[1:(idx - 1)]
end
kid′ = Node(JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), indent_span)
any_changes = true
push!(kids′, kid′)
accept_node!(ctx, kid′)
continue # Skip the index increment
end
end
idx += 1
end
# Make sure to add indent before the closing triple quote
if state === :expect_indent_ws
replace_bytes!(ctx, " "^indent_span, 0)
if kids′ === kids
kids′ = kids[1:(idx - 1)]
end
kid′ = Node(JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), indent_span)
any_changes = true
push!(kids′, kid′)
accept_node!(ctx, kid′)
end
@assert idx == close_idx
# Closing triple quote
close_kid = kids[close_idx]
@assert kind(close_kid) === triplekind
accept_node!(ctx, close_kid)
any_changes && push!(kids′, close_kid)
# Reset stream
seek(ctx.fmt_io, pos)
return any_changes ? make_node(node, kids′) : nothing
end

30
test/runtests.jl

@ -893,3 +893,33 @@ end
@testset "parsing new syntax" begin @testset "parsing new syntax" begin
@test format_string("public a, b") == "public a, b" # Julia 1.11 @test format_string("public a, b") == "public a, b" # Julia 1.11
end end
@testset "indent of multiline strings" begin
for triple in ("\"\"\"", "```"), sp in ("", " ", " "),
(pre, post) in (("", ""), ("pre", ""), ("pre", "post"))
otriple = pre * triple
ctriple = triple * post
# Level 0
@test format_string("$(sp)$(otriple)\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)") ===
"$(sp)$(otriple)\na\nb\n$(ctriple)"
@test format_string("$(sp)$(otriple)\n$(sp)a\n\n$(sp)b\n$(sp)$(ctriple)") ===
"$(sp)$(otriple)\na\n\nb\n$(ctriple)"
@test format_string("x = $(otriple)\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)") ===
"x = $(otriple)\na\nb\n$(ctriple)"
@test format_string("$(sp)$(otriple)a\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)") ===
"$(sp)$(otriple)a\na\nb\n$(ctriple)"
@test format_string("$(sp)$(otriple)\n$(sp)a\$(b)c\n$(sp)$(ctriple)") ===
"$(sp)$(otriple)\na\$(b)c\n$(ctriple)"
# Level 1
@test format_string("begin\n$(sp)$(otriple)\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)\nend") ===
"begin\n $(otriple)\n a\n b\n $(ctriple)\nend"
@test format_string("begin\n$(sp)$(otriple)\n$(sp)a\n$(sp)\n$(sp)b\n$(sp)$(ctriple)\nend") ===
"begin\n $(otriple)\n a\n\n b\n $(ctriple)\nend"
@test format_string("begin\n$(sp)x = $(otriple)\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)\nend") ===
"begin\n x = $(otriple)\n a\n b\n $(ctriple)\nend"
@test format_string("begin\n$(sp)$(otriple)a\n$(sp)a\n$(sp)b\n$(sp)$(ctriple)\nend") ===
"begin\n $(otriple)a\n a\n b\n $(ctriple)\nend"
@test format_string("begin\n$(sp)$(otriple)\n$(sp)a\$(b)c\n$(sp)$(ctriple)\nend") ===
"begin\n $(otriple)\n a\$(b)c\n $(ctriple)\nend"
end
end

Loading…
Cancel
Save