Skip to content

Commit 9bcce68

Browse files
aviateskclaudepenelopeysm
authored
Support line-range formatting (lines kwarg and jlfmt --lines) (#1100)
Add a way to restrict formatting to specific line ranges, emitting all other lines verbatim. This is useful for formatting only the lines touched by a PR/diff, enforcing "newly-changed code must be formatted" in CI without churning unrelated lines, and backing the LSP 3.18 `textDocument/rangesFormatting` capability (multiple ranges per request). - Core API: a `lines` keyword on `format_text` accepting a collection of inclusive, 1-based `(start, stop)` tuples and/or ranges, e.g. `format_text(text; lines = [(1, 10), (42, 47)])`. Overlapping and adjacent ranges are merged. - CLI: a repeatable `--lines=<start>:<stop>` flag forwarding to the `lines` kwarg. Limited to a single input file (or stdin); it can not be combined with multiple inputs/directories or Markdown. Rather than teaching the formatting pipeline about line ranges, the feature is kept orthogonal to it (the approach Runic.jl takes): marker comments are inserted around the requested ranges, the whole text is formatted as usual so in-range code reflows in its real context, and the result is spliced back together line by line -- formatted lines between markers, original source verbatim elsewhere, markers dropped. Line comments are used as markers so they reliably survive on their own line. - Closes #191 - Closes #1099 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Penelope Yong <penelopeysm@gmail.com>
1 parent 149ce51 commit 9bcce68

9 files changed

Lines changed: 680 additions & 139 deletions

File tree

HISTORY_v2.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Improved usage messages for the `jlfmt` command-line tool. (#1098)
44

5+
Added the ability to format only specific lines of a file, either via the `--lines` option to `jlfmt`, or the `lines` keyword argument to `format_text()`. (#191, #1099, #1100)
6+
57
# v2.6.15
68

79
Fixed a bug where the combination of `always_use_return` and `short_circuit_to_if` would silently change the meaning of a programme. (#887, #1096)

docs/src/cli.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ julia -m JuliaFormatter [<options>] <path>...
2525
```
2626

2727
!!! note "Runic Compatibility"
28-
The CLI interface is designed to be compatible with [Runic.jl](https://github.com/fredrikekre/Runic.jl)'s CLI where possible, making it easier to switch between formatters.
29-
30-
!!! warning "Missing features"
31-
Note that the `--lines` option is not yet implemented.
28+
The CLI interface is designed to be compatible with [Runic.jl](https://github.com/fredrikekre/Runic.jl)'s CLI where possible, making it easier to switch between formatters. This includes the repeatable `--lines=<start>:<stop>` option for formatting only specific line ranges (e.g. `jlfmt --lines=1:10 --lines=42:47 src/file.jl`).
3229

3330
## Quick Start
3431

docs/src/skipping_formatting.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Note that the formatter expects `#! format: on` and `#! format: off` to be on it
3030
!!! note "Ignoring files"
3131
You can also ignore entire files and directories by supplying [the `ignore` option](@ref options-ignore) in `.JuliaFormatter.toml`.
3232

33-
### Preventing indentation
33+
## Preventing indentation
3434

3535
Sometimes you may wish for a block of code to not be indented.
3636
You can achieve this with a more targeted approach of `#! format: noindent`.
@@ -89,3 +89,61 @@ end
8989
```
9090

9191
`#! format: noindent` can also be nested.
92+
93+
## One-off range formatting
94+
95+
Sometimes it is quite useful to only format specific ranges of a file.
96+
This situation can occur e.g. when highlighting a block of code in an editor and formatting only that block.
97+
(Indeed, both LanguageServer.jl and JETLS.jl support this feature.)
98+
99+
It is also useful for minimising diffs: by formatting only the lines that have been newly changed, you can avoid introducing formatting-only diffs in other parts of a file.
100+
101+
Since version 2.7, JuliaFormatter allows you to do this with the `lines` keyword argument in `format_text(...; lines=[(start_line, end_line)], ...)`.
102+
Multiple ranges can be specified; each range must be a tuple of two (1-indexed) integers representing the start and end line of the range to format.
103+
104+
```@example lines
105+
s = """
106+
f(a , g(b ,
107+
h( 12 ),
108+
))"""
109+
110+
# Format only line 2.
111+
using JuliaFormatter
112+
format_text(s; lines=[(2, 2)]) |> println
113+
```
114+
115+
The second line (`h(12)`) is now formatted, but the extra spaces in the first line have not been collapsed.
116+
117+
With the `jlfmt` CLI, you can specify `--lines=2:2` to achieve the same effect.
118+
119+
Note, however, that this works best when the lines to be formatted are a self-contained block of code.
120+
This is because formatting is context-sensitive: it's not possible to format a single line in isolation without considering where it occurs!
121+
122+
This can sometimes lead to odd results, and indeed the example above was chosen to showcase this.
123+
**In general, formatting of partial expressions is performed only on a best-effort basis.**
124+
For example:
125+
126+
```@example lines
127+
# Format only line 2, but with a smaller margin.
128+
format_text(s; lines=[(2, 2)], margin=10) |> println
129+
```
130+
131+
Here, the formatter decided to indent the call `h(12)` by eight spaces, even though based on its position in the file it should have been indented by only four spaces.
132+
Furthermore, as a result of this increased indentation, the formatter also decided to break up `h(12)` into multiple lines, even though `h(12),` with a four-space indent would have fit within the margin.
133+
134+
To understand why, we need to look at what would happen if *all* the text were to be formatted:
135+
136+
```@example lines
137+
format_text(s; margin=10) |> println
138+
```
139+
140+
Because of the small margin, JuliaFormatter decided to break the calls to `f` and `g`, meaning that `h(12)` would now get two levels of indentation.
141+
When formatting only line 2, however, you don't see the changes to the surrounding code: the only visible change is that of `h(12)`.
142+
143+
## A note about other special comments
144+
145+
Note that any comment that begins with `#! __JuliaFormatter` is reserved for internal use.
146+
For example, the option to format only specific lines of a file uses such marker comments.
147+
If you have one of these in your source code, unexpected results may occur.
148+
149+
It is very unlikely that your source code will inadvertently include such a comment, but this is documented here for completeness!

src/JuliaFormatter.jl

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ include("nest_utils.jl")
152152
include("print.jl")
153153
include("markdown.jl")
154154
include("copied_from_documenter.jl")
155+
include("line_ranges.jl")
155156

156157
const UNIX_TO_WINDOWS = r"\r?\n" => "\r\n"
157158
const WINDOWS_TO_UNIX = "\r\n" => "\n"
@@ -168,30 +169,58 @@ normalize_line_ending(s::AbstractString, replacer = WINDOWS_TO_UNIX) = replace(s
168169
style::AbstractStyle = DefaultStyle(),
169170
indent::Int = 4,
170171
margin::Int = 92,
172+
lines::Union{Nothing,Vector{Tuple{Int,Int}}} = nothing,
171173
options...,
172174
)::String
173175
174176
Formats a Julia source passed in as a string, returning the formatted
175177
code as another string.
176178
177179
See [Formatting Options](@ref formatting-options) for details on available options.
180+
181+
# Line-range formatting
182+
183+
Pass `lines` to restrict formatting to a set of line ranges, emitting everything else
184+
verbatim. `lines` is a `Vector{Tuple{Int,Int}}` of inclusive, 1-based `(start, stop)` line
185+
ranges, e.g. `format_text(text; lines = [(1, 10), (42, 47)])`. Overlapping and adjacent
186+
ranges are merged. A range that begins or ends in the middle of a multi-line expression is
187+
formatted on a best-effort basis.
178188
"""
179189
function format_text(text::AbstractString; style::AbstractStyle = DefaultStyle(), kwargs...)
180190
return format_text(text, style; kwargs...)
181191
end
182192

183-
function format_text(text::AbstractString, style::AbstractStyle; kwargs...)
193+
function format_text(
194+
text::AbstractString,
195+
style::AbstractStyle;
196+
lines::Union{Nothing,Vector{Tuple{Int,Int}}} = nothing,
197+
kwargs...,
198+
)
184199
if isempty(text)
185200
return text
186201
end
202+
if lines !== nothing
203+
# Restrict formatting to the given line ranges (see `line_ranges.jl`). This re-enters
204+
# `format_text` without `lines` for the actual formatting, so it works for any style.
205+
return format_line_ranges(text, style, lines; kwargs...)
206+
end
187207
opts = Options(; merge(options(style), kwargs)...)
188208
return format_text(text, style, opts)
189209
end
190210

191-
function format_text(text::AbstractString, style::SciMLStyle; maxiters = 3, kwargs...)
211+
function format_text(
212+
text::AbstractString,
213+
style::SciMLStyle;
214+
maxiters = 3,
215+
lines::Union{Nothing,Vector{Tuple{Int,Int}}} = nothing,
216+
kwargs...,
217+
)
192218
if isempty(text)
193219
return text
194220
end
221+
if lines !== nothing
222+
return format_line_ranges(text, style, lines; maxiters, kwargs...)
223+
end
195224
opts = Options(; merge(options(style), kwargs)...)
196225
# We need to iterate to a fixpoint because the result of short to long
197226
# form isn't properly formatted
@@ -309,7 +338,7 @@ function _format_file(
309338
format_markdown::Bool = false,
310339
format_options...,
311340
)::Bool
312-
path, ext = splitext(filename)
341+
_, ext = splitext(filename)
313342
shebang_pattern = r"^#!\s*/.*\bjulia[0-9.-]*\b"
314343
formatted_str = if match(r"^\.[jq]*md$", ext) nothing
315344
if !(format_markdown)

0 commit comments

Comments
 (0)