Browse Source

Support range formatting

This patch implements the `--lines=a:b` command line argument for
limiting the formatting to the line range `a:b`. Multiple ranges are
supported. Closes #114.
pull/120/head
Fredrik Ekre 1 year ago
parent
commit
78cbef2a05
No known key found for this signature in database
GPG Key ID: DE82E6D5E364C0A2
  1. 5
      CHANGELOG.md
  2. 104
      src/Runic.jl
  3. 7
      src/debug.jl
  4. 31
      src/main.jl
  5. 45
      test/maintests.jl
  6. 46
      test/runtests.jl

5
CHANGELOG.md

@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- New command line option `--lines=a:b` for limiting formatting to lines `a` to `b`.
`--lines` can be repeated to specify multiple ranges ([#114], [#120]).
## [v1.1.0] - 2024-12-04 ## [v1.1.0] - 2024-12-04
### Changed ### Changed
- Fix a bug that caused "single space after keyword" to not apply after the `function` - Fix a bug that caused "single space after keyword" to not apply after the `function`

104
src/Runic.jl

@ -131,6 +131,7 @@ mutable struct Context
check::Bool check::Bool
diff::Bool diff::Bool
filemode::Bool filemode::Bool
line_ranges::Vector{UnitRange{Int}}
# Global state # Global state
indent_level::Int # track (hard) indentation level indent_level::Int # track (hard) indentation level
call_depth::Int # track call-depth level for debug printing call_depth::Int # track call-depth level for debug printing
@ -144,11 +145,104 @@ mutable struct Context
lineage_macros::Vector{String} lineage_macros::Vector{String}
end end
const RANGE_FORMATTING_BEGIN = "#= RUNIC RANGE FORMATTING " * "BEGIN =#"
const RANGE_FORMATTING_END = "#= RUNIC RANGE FORMATTING " * "END =#"
function add_line_range_markers(str, line_ranges)
lines = collect(eachline(IOBuffer(str); keep = true))
sort!(line_ranges, rev = true)
for r in line_ranges
a, b = extrema(r)
if a < 1 || b > length(lines)
throw(MainError("`--lines` range out of bounds"))
end
if b == length(lines) && !endswith(lines[end], "\n")
lines[end] *= "\n"
end
insert!(lines, b + 1, RANGE_FORMATTING_END * "\n")
insert!(lines, a, RANGE_FORMATTING_BEGIN * "\n")
end
io = IOBuffer(; maxsize = sum(sizeof, lines; init = 0))
join(io, lines)
src_str = String(take!(io))
return src_str
end
function remove_line_range_markers(src_io, fmt_io)
src_lines = eachline(seekstart(src_io); keep = true)
fmt_lines = eachline(seekstart(fmt_io); 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(fmt_io), take!(io))
truncate(fmt_io, position(fmt_io))
return
end
function Context( function Context(
src_str::String; assert::Bool = true, debug::Bool = false, verbose::Bool = debug, 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}[]
) )
if !isempty(line_ranges)
# If formatting is limited to certain line ranges we modify the source string to
# include begin and end marker comments.
src_str = add_line_range_markers(src_str, line_ranges)
end
src_io = IOBuffer(src_str) src_io = IOBuffer(src_str)
# TODO: If parsing here fails, and we have line ranges, perhaps try to parse without the
# markers to check whether the markers are the cause of the failure.
src_tree = Node( src_tree = Node(
JuliaSyntax.parseall(JuliaSyntax.GreenNode, src_str; ignore_warnings = true, version = v"2-") JuliaSyntax.parseall(JuliaSyntax.GreenNode, src_str; ignore_warnings = true, version = v"2-")
) )
@ -176,7 +270,7 @@ function Context(
format_on = true format_on = true
return Context( return Context(
src_str, src_tree, src_io, fmt_io, fmt_tree, quiet, verbose, assert, debug, check, 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 lineage_kinds, lineage_macros
) )
end end
@ -501,14 +595,20 @@ function format_tree!(ctx::Context)
end end
# Truncate the output at the root span # Truncate the output at the root span
truncate(ctx.fmt_io, span(root′)) truncate(ctx.fmt_io, span(root′))
# Remove line range markers if any
if !isempty(ctx.line_ranges)
remove_line_range_markers(ctx.src_io, ctx.fmt_io)
end
# Check that the output is parseable # Check that the output is parseable
try try
fmt_str = String(read(seekstart(ctx.fmt_io))) fmt_str = String(read(seekstart(ctx.fmt_io)))
# TODO: parsing may fail here because of the removal of the range comments
JuliaSyntax.parseall(JuliaSyntax.GreenNode, fmt_str; ignore_warnings = true, version = v"2-") JuliaSyntax.parseall(JuliaSyntax.GreenNode, fmt_str; ignore_warnings = true, version = v"2-")
catch catch
throw(AssertionError("re-parsing the formatted output failed")) throw(AssertionError("re-parsing the formatted output failed"))
end end
# Set the final tree # Set the final tree
# TODO: When range formatting this doesn't match the content of ctx.fmt_io
ctx.fmt_tree = root′ ctx.fmt_tree = root′
return nothing return nothing
end end

7
src/debug.jl

@ -10,6 +10,13 @@ struct AssertionError <: RunicException
msg::String msg::String
end end
# Thrown from internal code when invalid CLI arguments can not be validated directly in
# `Runic.main`: `throw(MainError("message"))` from internal code is like calling
# `panic("message")` in `Runic.main`.
struct MainError <: RunicException
msg::String
end
function Base.showerror(io::IO, err::AssertionError) function Base.showerror(io::IO, err::AssertionError)
print( print(
io, io,

31
src/main.jl

@ -196,6 +196,24 @@ function writeo(output::Output, iob)
return return
end end
function insert_line_range(line_ranges, lines)
m = match(r"^(\d+):(\d+)$", lines)
if m === nothing
return panic("can not parse `--lines` argument as an integer range")
end
range_start = parse(Int, m.captures[1]::SubString)
range_end = parse(Int, m.captures[2]::SubString)
if range_start > range_end
return panic("empty `--lines` range")
end
range = range_start:range_end
if !all(x -> isdisjoint(x, range), line_ranges)
return panic("`--lines` ranges cannot overlap")
end
push!(line_ranges, range)
return 0
end
function main(argv) function main(argv)
# Reset errno # Reset errno
global errno = 0 global errno = 0
@ -210,6 +228,7 @@ function main(argv)
diff = false diff = false
check = false check = false
fail_fast = false fail_fast = false
line_ranges = typeof(1:2)[]
# Parse the arguments # Parse the arguments
while length(argv) > 0 while length(argv) > 0
@ -234,6 +253,10 @@ function main(argv)
check = true check = true
elseif x == "-vv" || x == "--debug" elseif x == "-vv" || x == "--debug"
debug = verbose = true debug = verbose = true
elseif (m = match(r"^--lines=(.*)$", x); m !== nothing)
if insert_line_range(line_ranges, m.captures[1]::SubString) != 0
return errno
end
elseif x == "-o" elseif x == "-o"
if length(argv) < 1 if length(argv) < 1
return panic("expected output file argument after `-o`") return panic("expected output file argument after `-o`")
@ -272,6 +295,9 @@ function main(argv)
if outputfile != "" && length(inputfiles) > 1 if outputfile != "" && length(inputfiles) > 1
return panic("option `--output` can not be used together with multiple input files") return panic("option `--output` can not be used together with multiple input files")
end end
if !isempty(line_ranges) && length(inputfiles) > 1
return panic("option `--lines` can not be used together with multiple input files")
end
if length(inputfiles) > 1 && !(inplace || check) if length(inputfiles) > 1 && !(inplace || check)
return panic("option `--inplace` or `--check` required with multiple input files") return panic("option `--inplace` or `--check` required with multiple input files")
end end
@ -363,7 +389,7 @@ function main(argv)
# Call the library to format the text # Call the library to format the text
ctx = try ctx = try
ctx′ = Context(sourcetext; quiet, verbose, debug, diff, check) ctx′ = Context(sourcetext; quiet, verbose, debug, diff, check, line_ranges)
format_tree!(ctx′) format_tree!(ctx′)
ctx′ ctx′
catch err catch err
@ -371,6 +397,9 @@ function main(argv)
if err isa JuliaSyntax.ParseError if err isa JuliaSyntax.ParseError
panic("failed to parse input: ", err) panic("failed to parse input: ", err)
continue continue
elseif err isa MainError
panic(err.msg)
continue
end end
msg = "failed to format input: " msg = "failed to format input: "
@static if juliac @static if juliac

45
test/maintests.jl

@ -343,7 +343,7 @@ function maintests(f::R) where {R}
end end
# runic -o readonly.jl in.jl # runic -o readonly.jl in.jl
return cdtmp() do cdtmp() do
f_in = "in.jl" f_in = "in.jl"
write(f_in, bad) write(f_in, bad)
f_out = "readonly.jl" f_out = "readonly.jl"
@ -356,6 +356,49 @@ function maintests(f::R) where {R}
@test isempty(fd1) @test isempty(fd1)
@test occursin("could not write to output file", fd2) @test occursin("could not write to output file", fd2)
end 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"
# Errors
rc, fd1, fd2 = runic(["--lines=1:2", "--lines=2:3"], src)
@test rc == 1
@test isempty(fd1)
@test occursin("`--lines` ranges cannot overlap", fd2)
rc, fd1, fd2 = runic(["--lines=0:1"], src)
@test rc == 1 && isempty(fd1)
@test occursin("`--lines` range out of bounds", fd2)
rc, fd1, fd2 = runic(["--lines=3:4"], src)
@test rc == 1 && isempty(fd1)
@test occursin("`--lines` range out of bounds", fd2)
rc, fd1, fd2 = runic(["--lines=3:4", "foo.jl", "bar.jl"], src)
@test rc == 1 && isempty(fd1)
@test occursin("option `--lines` can not be used together with multiple input files", fd2)
end
return
end end
# rc = let argv = pushfirst!(copy(argv), "runic"), argc = length(argv) % Cint # rc = let argv = pushfirst!(copy(argv), "runic"), argc = length(argv) % Cint

46
test/runtests.jl

@ -1541,6 +1541,52 @@ end
end 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 module RunicMain1
using Test: @testset using Test: @testset
using Runic: main using Runic: main

Loading…
Cancel
Save