Skip to content

Commit

Permalink
Support range formatting
Browse files Browse the repository at this point in the history
This patch implements the `--lines=a:b` command line argument for
limiting the formatting to the line range `a:b`. Multiple ranges are
supported. Close #114.
  • Loading branch information
fredrikekre committed Dec 6, 2024
1 parent d66286d commit 9536a3f
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 4 deletions.
82 changes: 80 additions & 2 deletions src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mutable struct Context
check::Bool
diff::Bool
filemode::Bool
line_ranges::Vector{UnitRange{Int}}
# Global state
indent_level::Int # track (hard) indentation level
call_depth::Int # track call-depth level for debug printing
Expand All @@ -144,11 +145,26 @@ mutable struct Context
lineage_macros::Vector{String}
end

const RANGE_FORMATTING_BEGIN = "#= RUNIC RANGE FORMATTING BEGIN =#"
const RANGE_FORMATTING_END = "#= RUNIC RANGE FORMATTING END =#"

function Context(
src_str::String; assert::Bool = true, debug::Bool = false, verbose::Bool = debug,
diff::Bool = false, check::Bool = false, quiet::Bool = false, filemode::Bool = true
diff::Bool = false, check::Bool = false, quiet::Bool = false, filemode::Bool = true,
line_ranges::Vector{UnitRange{Int}} = UnitRange{Int}[]
)
src_io = IOBuffer(src_str)
if !isempty(line_ranges)
# Modify the source string
lines = collect(eachline(src_io; keep = true))
for r in reverse(line_ranges)
a, b = extrema(r)
insert!(lines, min(length(lines), b) + 1, RANGE_FORMATTING_END * "\n")
insert!(lines, a, RANGE_FORMATTING_BEGIN * "\n")
end
src_str = join(lines)
src_io = IOBuffer(src_str)
end
src_tree = Node(
JuliaSyntax.parseall(JuliaSyntax.GreenNode, src_str; ignore_warnings = true, version = v"2-")
)
Expand Down Expand Up @@ -176,7 +192,7 @@ function Context(
format_on = true
return Context(
src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check,
diff, filemode, indent_level, call_depth, format_on, prev_sibling, next_sibling,
diff, filemode, line_ranges, indent_level, call_depth, format_on, prev_sibling, next_sibling,
lineage_kinds, lineage_macros
)
end
Expand Down Expand Up @@ -502,12 +518,74 @@ function format_tree!(ctx::Context)
# Truncate the output at the root span
truncate(ctx.fmt_io, span(root′))
# Check that the output is parseable
local fmt_str

Check warning on line 521 in src/Runic.jl

View check run for this annotation

Codecov / codecov/patch

src/Runic.jl#L521

Added line #L521 was not covered by tests
try
fmt_str = String(read(seekstart(ctx.fmt_io)))
JuliaSyntax.parseall(JuliaSyntax.GreenNode, fmt_str; ignore_warnings = true, version = v"2-")
catch
throw(AssertionError("re-parsing the formatted output failed"))
end
# Line filtering
if !isempty(ctx.line_ranges)
src_lines = eachline(IOBuffer(ctx.src_str); keep = true)
fmt_lines = eachline(IOBuffer(fmt_str); keep = true)
io = IOBuffer()
# These can't fail because we will at the minimum have the begin/end comments
src_itr = iterate(src_lines)
@assert src_itr !== nothing
src_ln, src_token = src_itr
itr_fmt = iterate(fmt_lines)
@assert itr_fmt !== nothing
fmt_ln, fmt_token = itr_fmt
eof = false
while true
# Take source lines until range start or eof
while !occursin(RANGE_FORMATTING_BEGIN, src_ln)
if !occursin(RANGE_FORMATTING_END, src_ln)
write(io, src_ln)
end
src_itr = iterate(src_lines, src_token)
if src_itr === nothing
eof = true
break
end
src_ln, src_token = src_itr
end
eof && break
@assert occursin(RANGE_FORMATTING_BEGIN, src_ln) &&
strip(src_ln) == RANGE_FORMATTING_BEGIN
# Skip ahead in the source lines until the range end
while !occursin(RANGE_FORMATTING_END, src_ln)
src_itr = iterate(src_lines, src_token)
@assert src_itr !== nothing
src_ln, src_token = src_itr
end
@assert occursin(RANGE_FORMATTING_END, src_ln) &&
strip(src_ln) == RANGE_FORMATTING_END
# Skip ahead in the formatted lines until range start
while !occursin(RANGE_FORMATTING_BEGIN, fmt_ln)
fmt_itr = iterate(fmt_lines, fmt_token)
@assert fmt_itr !== nothing
fmt_ln, fmt_token = fmt_itr
end
@assert occursin(RANGE_FORMATTING_BEGIN, fmt_ln) &&
strip(fmt_ln) == RANGE_FORMATTING_BEGIN
# Take formatted lines until range end
while !occursin(RANGE_FORMATTING_END, fmt_ln)
if !occursin(RANGE_FORMATTING_BEGIN, fmt_ln)
write(io, fmt_ln)
end
fmt_itr = iterate(fmt_lines, fmt_token)
@assert fmt_itr !== nothing
fmt_ln, fmt_token = fmt_itr
end
@assert occursin(RANGE_FORMATTING_END, fmt_ln) &&
strip(fmt_ln) == RANGE_FORMATTING_END
eof && break
end
write(seekstart(ctx.fmt_io), take!(io))
truncate(ctx.fmt_io, position(ctx.fmt_io))
end
# Set the final tree
ctx.fmt_tree = root′
return nothing
Expand Down
31 changes: 30 additions & 1 deletion src/main.jl
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,29 @@ function writeo(output::Output, iob)
return
end

function insert_line_range(line_ranges, line_start, line_end)
if line_start > line_end || line_start < 1 || line_end < 1
error("invalid range")

Check warning on line 201 in src/main.jl

View check run for this annotation

Codecov / codecov/patch

src/main.jl#L201

Added line #L201 was not covered by tests
end
for (i, r) in pairs(line_ranges)
if line_start in r && line_end in r
return

Check warning on line 205 in src/main.jl

View check run for this annotation

Codecov / codecov/patch

src/main.jl#L205

Added line #L205 was not covered by tests
elseif line_start in r
@assert line_end > r[end]
line_ranges[i] = r[1]:line_end
return

Check warning on line 209 in src/main.jl

View check run for this annotation

Codecov / codecov/patch

src/main.jl#L207-L209

Added lines #L207 - L209 were not covered by tests
elseif line_end in r
@assert line_start < r[1]
line_ranges[i] = line_start:r[end]
return

Check warning on line 213 in src/main.jl

View check run for this annotation

Codecov / codecov/patch

src/main.jl#L211-L213

Added lines #L211 - L213 were not covered by tests
end
end
r = line_start:line_end
j = searchsortedfirst(line_ranges, r)
insert!(line_ranges, j, r)
return
end

function main(argv)
# Reset errno
global errno = 0
Expand All @@ -210,6 +233,7 @@ function main(argv)
diff = false
check = false
fail_fast = false
line_ranges = typeof(1:2)[]

# Parse the arguments
while length(argv) > 0
Expand All @@ -234,6 +258,11 @@ function main(argv)
check = true
elseif x == "-vv" || x == "--debug"
debug = verbose = true
elseif (m = match(r"^--lines=(\d+):(\d+)$", x); m !== nothing)
# TODO: Error handling
line_start = parse(Int, m.captures[1])
line_end = parse(Int, m.captures[2])
insert_line_range(line_ranges, line_start, line_end)
elseif x == "-o"
if length(argv) < 1
return panic("expected output file argument after `-o`")
Expand Down Expand Up @@ -363,7 +392,7 @@ function main(argv)

# Call the library to format the text
ctx = try
ctx′ = Context(sourcetext; quiet, verbose, debug, diff, check)
ctx′ = Context(sourcetext; quiet, verbose, debug, diff, check, line_ranges)
format_tree!(ctx′)
ctx′
catch err
Expand Down
31 changes: 30 additions & 1 deletion test/maintests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ function maintests(f::R) where {R}
end

# runic -o readonly.jl in.jl
return cdtmp() do
cdtmp() do
f_in = "in.jl"
write(f_in, bad)
f_out = "readonly.jl"
Expand All @@ -356,6 +356,35 @@ function maintests(f::R) where {R}
@test isempty(fd1)
@test occursin("could not write to output file", fd2)
end

# runic --lines
cdtmp() do
src = """
function f(a,b)
return a+b
end
"""
rc, fd1, fd2 = runic(["--lines=1:1"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a, b)\n return a+b\n end\n"
rc, fd1, fd2 = runic(["--lines=2:2"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a,b)\n return a + b\n end\n"
rc, fd1, fd2 = runic(["--lines=3:3"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a,b)\n return a+b\nend\n"
rc, fd1, fd2 = runic(["--lines=1:1", "--lines=3:3"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a, b)\n return a+b\nend\n"
rc, fd1, fd2 = runic(["--lines=1:1", "--lines=2:2", "--lines=3:3"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a, b)\n return a + b\nend\n"
rc, fd1, fd2 = runic(["--lines=1:2"], src)
@test rc == 0 && isempty(fd2)
@test fd1 == "function f(a, b)\n return a + b\n end\n"
end

return
end

# rc = let argv = pushfirst!(copy(argv), "runic"), argc = length(argv) % Cint
Expand Down
46 changes: 46 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,52 @@ end
end
end

# TODO: Support lines in format_string and format_file
function format_lines(str, lines)
line_ranges = lines isa UnitRange ? [lines] : lines
ctx = Runic.Context(str; filemode = false, line_ranges = line_ranges)
Runic.format_tree!(ctx)
return String(take!(ctx.fmt_io))
end

@testset "--lines" begin
str = """
function f(a,b)
return a+b
end
"""
@test format_lines(str, 1:1) == """
function f(a, b)
return a+b
end
"""
@test format_lines(str, 2:2) == """
function f(a,b)
return a + b
end
"""
@test format_lines(str, 3:3) == """
function f(a,b)
return a+b
end
"""
@test format_lines(str, [1:1, 3:3]) == """
function f(a, b)
return a+b
end
"""
@test format_lines(str, [1:1, 2:2, 3:3]) == """
function f(a, b)
return a + b
end
"""
@test format_lines(str, [1:2]) == """
function f(a, b)
return a + b
end
"""
end

module RunicMain1
using Test: @testset
using Runic: main
Expand Down

0 comments on commit 9536a3f

Please sign in to comment.