Skip to content

Commit 2303933

Browse files
authored
Format a blank line around multi-line module items (#8961)
* Format a blank line around module items that span several lines Why: Dense code, often written by AI agents, runs multi-line modules and functions together, which makes a file hard to scan (#8960). The formatter already collapses several blank lines into one; it now also separates an item that prints on more than one line from its neighbours in structures and signatures. Blocks inside expressions keep their layout. Format modified (structure items): Example: module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed Before (rescript): module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed After (rescript): module Details = { let make = () => React.string("details") } module Output = Tool.TextResult.Output let failed = Tool.TextResult.failed Format unchanged (one-line items with comments): Example: let a = 1 // about b let b = 2 // trailing let c = 3 After (rescript): let a = 1 // about b let b = 2 // trailing let c = 3 Review: Is "multi-line" decided on the source or on the output? On the output: the new Res_doc.item_separator measures the items on each side with the printer's own layout decisions, stopping at the first line break, so an item that only breaks at the line width counts and formatting twice gives the same text. The source line span (`let x = someLongCall(a, b, c, d)` is one source line but prints on several) and Doc.will_break (a record that wraps by width is force-broken only on the second pass) would both add the blank line only on the second format. The measurement costs at most one output line per item. The roundtrip test formats every printer fixture twice and gets the same text. Review: Why a Res_doc.comment marker? Leading comments print inside the item's document (a value binding carries them), so a `//` line above a one-line item would make it multi-line, even when the item is measured before print_list adds its own comments. The marker keeps comments out of the measurement; doc comments are attributes and still count. Review: Why move own-line trailing comments below the blank line? A comment inside the previous item's source, such as `f(a // c => 1)`, prints on its own line after the item. Formatting again makes it a leading comment of the next item, after the blank line, so the first pass puts it there too. Review: How are the measurement's branches tested? tests/syntax_tests/res_test.ml prints documents built for each case (line breaks, groups that fit and break, if_breaks, comments, trailing comments) and checks the output, as the printer fixtures do not reach every branch first. Review: What else prints through it? The editor's code actions and `rescript-tools format-codeblocks` for code blocks in docstrings, so the Xform and FormatDocstringsTest2 snapshots gain a blank line. Review: Do attributes count? Yes, an attribute or doc comment on its own line makes an item print on two lines, so runs of `@genType` or documented one-line items are spaced; an inline `@val external` is not. The fixtures cover both, and nested module bodies. Review: Does a string literal with a line break count? Yes: it prints on several lines, like a template literal, so the line-ending test in res_test.ml separates such items with a blank line, in the file's own line endings. Review: Is it opt-in? No: every `rescript format` uses it. The next commit formats this repository with it. Signed-off-by: Roland Peelen <roland@magic.dev> * Format the repository with multi-line item spacing The previous commit makes the formatter put a blank line around module items that span several lines. `make format` applies it to the ReScript sources it covers: 353 files gain 1179 blank lines and lose nothing. rewatch/testrepo/packages/dep02/src/Array.res gains one too: the rewatch format test expects exactly the testrepo's 8 deliberately unformatted files to change. The outputs that record source line numbers follow: the super_errors snapshots, the `assert` locations in the generated test JavaScript, the doc-extraction snapshots of tests/tools_tests, and the locations in React.res and JsxEvent.res that the completion and reanalyze snapshots of tests/analysis_tests point to. Signed-off-by: Roland Peelen <roland@magic.dev> --------- Signed-off-by: Roland Peelen <roland@magic.dev>
1 parent 6ea63e5 commit 2303933

627 files changed

Lines changed: 3623 additions & 1406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Format a blank line before and after a top-level or module-level item that spans several lines.

‎compiler/syntax/src/res_doc.ml‎

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type t =
2323
| Group of {mutable should_break: bool; doc: t}
2424
| CustomLayout of t list
2525
| BreakParent
26+
| ItemSeparator of {prev: t; next: t} (* see [item_separator] *)
27+
| Comment of t (* see [comment] *)
2628

2729
let nil = Nil
2830
let line = LineBreak Classic
@@ -52,6 +54,8 @@ let group d = Group {should_break = false; doc = d}
5254
let breakable_group ~force_break d = Group {should_break = force_break; doc = d}
5355
let custom_layout gs = CustomLayout gs
5456
let break_parent = BreakParent
57+
let item_separator ~prev ~next = ItemSeparator {prev; next}
58+
let comment d = Comment d
5559

5660
let space = Text " "
5761
let comma = Text ","
@@ -75,9 +79,9 @@ let propagate_forced_breaks doc =
7579
match doc with
7680
| Text _ | Nil | LineSuffix _ -> false
7781
| BreakParent -> true
78-
| LineBreak (Hard | Literal) -> true
82+
| LineBreak (Hard | Literal) | ItemSeparator _ -> true
7983
| LineBreak (Classic | Soft) -> false
80-
| Indent children ->
84+
| Indent children | Comment children ->
8185
let child_forces_break = walk children in
8286
child_forces_break
8387
| IfBreaks ({yes = true_doc; no = false_doc} as ib) ->
@@ -115,9 +119,12 @@ let propagate_forced_breaks doc =
115119
(* See documentation in interface file *)
116120
let rec will_break doc =
117121
match doc with
118-
| LineBreak (Hard | Literal) | BreakParent | Group {should_break = true} ->
122+
| LineBreak (Hard | Literal)
123+
| BreakParent | ItemSeparator _
124+
| Group {should_break = true} ->
119125
true
120-
| Group {doc} | Indent doc | CustomLayout (doc :: _) -> will_break doc
126+
| Group {doc} | Indent doc | Comment doc | CustomLayout (doc :: _) ->
127+
will_break doc
121128
| Concat docs -> List.exists will_break docs
122129
| IfBreaks {yes; no} -> will_break yes || will_break no
123130
| _ -> false
@@ -129,9 +136,9 @@ let has_pending_line_suffix doc =
129136
let rec walk ~broken pending doc =
130137
match doc with
131138
| LineSuffix _ -> true
132-
| LineBreak _ -> pending && not broken
139+
| LineBreak _ | ItemSeparator _ -> pending && not broken
133140
| Nil | Text _ | BreakParent -> pending
134-
| Indent doc -> walk ~broken pending doc
141+
| Indent doc | Comment doc -> walk ~broken pending doc
135142
| Concat docs -> List.fold_left (walk ~broken) pending docs
136143
| Group {should_break; doc} ->
137144
walk ~broken:(should_break || will_break doc) pending doc
@@ -170,10 +177,12 @@ let fits w stack =
170177
| _, Nil | _, LineSuffix _ | _, BreakParent -> ()
171178
| _, Text txt -> width := width.contents - String.length txt
172179
| _, Indent doc -> calculate (indent + 2) mode doc
180+
| _, Comment doc -> calculate indent mode doc
173181
| Flat, LineBreak Hard | Flat, LineBreak Literal -> result := Some true
174182
| Flat, LineBreak Classic -> width := width.contents - 1
175183
| Flat, LineBreak Soft -> ()
176184
| Break, LineBreak _ -> result := Some true
185+
| _, ItemSeparator _ -> result := Some true
177186
| _, Group {should_break = true; doc} -> calculate indent Break doc
178187
| _, Group {doc} -> calculate indent mode doc
179188
| _, IfBreaks {yes = break_doc; broken = true} ->
@@ -203,6 +212,59 @@ let fits w stack =
203212
in
204213
calculate_all stack
205214

215+
let select_custom_layout ~width ~pos ind rest docs =
216+
let rec find_group_that_fits groups =
217+
match groups with
218+
| [] -> Nil
219+
| [last_group] -> last_group
220+
| doc :: docs ->
221+
if fits (width - pos) ((ind, Flat, doc) :: rest) then doc
222+
else find_group_that_fits docs
223+
in
224+
find_group_that_fits docs
225+
226+
(* Whether [doc], printed from the start of a line indented by [ind], spans more
227+
than one line. Takes the layout decisions of [to_string] up to the first line
228+
break. Comments and line suffixes do not count. *)
229+
let spans_lines ~width ind mode doc =
230+
let rec loop ~pos stack =
231+
match stack with
232+
| [] -> false
233+
| (ind, mode, doc) :: rest -> (
234+
match doc with
235+
| Nil | BreakParent | LineSuffix _ | Comment _ -> loop ~pos rest
236+
| Text txt ->
237+
String.contains txt '\n' || loop ~pos:(pos + String.length txt) rest
238+
| Concat docs ->
239+
loop ~pos (List.map (fun doc -> (ind, mode, doc)) docs @ rest)
240+
| Indent doc -> loop ~pos ((ind + 2, mode, doc) :: rest)
241+
| IfBreaks {yes; no; broken} ->
242+
let doc = if broken || mode = Break then yes else no in
243+
loop ~pos ((ind, mode, doc) :: rest)
244+
| LineBreak (Hard | Literal) | ItemSeparator _ -> true
245+
| LineBreak Classic -> mode = Break || loop ~pos:(pos + 1) rest
246+
| LineBreak Soft -> mode = Break || loop ~pos rest
247+
| Group {should_break; doc} ->
248+
let flat = (ind, Flat, doc) :: rest in
249+
if should_break || not (fits (width - pos) flat) then
250+
loop ~pos ((ind, Break, doc) :: rest)
251+
else loop ~pos flat
252+
| CustomLayout docs ->
253+
let doc = select_custom_layout ~width ~pos ind rest docs in
254+
loop ~pos ((ind, Flat, doc) :: rest))
255+
in
256+
loop ~pos:ind [(ind, mode, doc)]
257+
258+
(* An own-line trailing comment is a line suffix that starts with a hard line:
259+
returns it without its leading hard lines. *)
260+
let rec own_line_suffix doc =
261+
match doc with
262+
| LineBreak Hard -> Some Nil
263+
| Concat (LineBreak Hard :: docs) ->
264+
let rest = Concat docs in
265+
Some (Option.value (own_line_suffix rest) ~default:rest)
266+
| _ -> None
267+
206268
let to_string ?(newline = "\n") ~width doc =
207269
propagate_forced_breaks doc;
208270
let buffer = Mini_buffer.create 1000 in
@@ -220,6 +282,7 @@ let to_string ?(newline = "\n") ~width doc =
220282
let ops = List.map (fun doc -> (ind, mode, doc)) docs in
221283
process ~pos line_suffices (List.append ops rest)
222284
| Indent doc -> process ~pos line_suffices ((ind + 2, mode, doc) :: rest)
285+
| Comment doc -> process ~pos line_suffices ((ind, mode, doc) :: rest)
223286
| IfBreaks {yes = break_doc; broken = true} ->
224287
process ~pos line_suffices ((ind, mode, break_doc) :: rest)
225288
| IfBreaks {yes = break_doc; no = flat_doc} ->
@@ -262,16 +325,30 @@ let to_string ?(newline = "\n") ~width doc =
262325
then process ~pos line_suffices ((ind, Break, doc) :: rest)
263326
else process ~pos line_suffices ((ind, Flat, doc) :: rest)
264327
| CustomLayout docs ->
265-
let rec find_group_that_fits groups =
266-
match groups with
267-
| [] -> Nil
268-
| [last_group] -> last_group
269-
| doc :: docs ->
270-
if fits (width - pos) ((ind, Flat, doc) :: rest) then doc
271-
else find_group_that_fits docs
272-
in
273-
let doc = find_group_that_fits docs in
274-
process ~pos line_suffices ((ind, Flat, doc) :: rest))
328+
let doc = select_custom_layout ~width ~pos ind rest docs in
329+
process ~pos line_suffices ((ind, Flat, doc) :: rest)
330+
| ItemSeparator {prev; next} ->
331+
if spans_lines ~width ind mode prev || spans_lines ~width ind mode next
332+
then
333+
(* Own-line trailing comments of [prev] go below the blank line, where
334+
they print once they are leading comments of [next]. *)
335+
let separator = (ind, mode, hard_line) in
336+
let same_line, below =
337+
List.partition_map
338+
(fun ((ind, mode, doc) as suffix) ->
339+
match own_line_suffix doc with
340+
| None -> Either.Left suffix
341+
| Some comment -> Either.Right ((ind, mode, comment), suffix))
342+
(List.rev line_suffices)
343+
in
344+
let rest =
345+
match below with
346+
| [] -> rest
347+
| (first, _) :: below ->
348+
(first :: List.map snd below) @ (separator :: rest)
349+
in
350+
process ~pos (List.rev same_line) (separator :: separator :: rest)
351+
else process ~pos line_suffices ((ind, mode, hard_line) :: rest))
275352
| [] -> (
276353
match line_suffices with
277354
| [] -> ()
@@ -351,6 +428,8 @@ let debug t =
351428
| Literal -> "Literal"
352429
in
353430
text ("LineBreak(" ^ break_txt ^ ")")
431+
| ItemSeparator _ -> text "ItemSeparator"
432+
| Comment doc -> concat [text "comment("; to_doc doc; text ")"]
354433
| Group {should_break; doc} ->
355434
group
356435
(concat
@@ -375,5 +454,5 @@ let is_nil doc =
375454
match doc with
376455
| Nil -> true
377456
| Text _ | LineSuffix _ | Concat _ | Indent _ | IfBreaks _ | LineBreak _
378-
| Group _ | CustomLayout _ | BreakParent ->
457+
| Group _ | CustomLayout _ | BreakParent | ItemSeparator _ | Comment _ ->
379458
false

‎compiler/syntax/src/res_doc.mli‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ val breakable_group : force_break:bool -> t -> t
1818
* will be checked until one fits. *)
1919
val custom_layout : t list -> t
2020
val break_parent : t
21+
22+
val item_separator : prev:t -> next:t -> t
23+
(** A hard line, and a blank line when [prev] or [next], the documents printed
24+
on each side of it, span several lines. It measures them but does not print
25+
them. *)
26+
27+
val comment : t -> t
28+
(** Prints like its argument, which [item_separator] does not measure. *)
29+
2130
val join : sep:t -> t list -> t
2231

2332
(* [(doc1, sep1); (doc2,sep2)] joins as doc1 sep1 doc2 *)

‎compiler/syntax/src/res_printer.ml‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,8 @@ let print_leading_comments node tbl loc =
337337
else if diff > 1 then Doc.concat [Doc.hard_line; Doc.hard_line]
338338
else Doc.hard_line
339339
in
340-
let doc =
341-
Doc.group
342-
(Doc.concat [Doc.concat (List.rev (cmt_doc :: acc)); separator; node])
343-
in
340+
let comments = Doc.concat (List.rev (separator :: cmt_doc :: acc)) in
341+
let doc = Doc.group (Doc.concat [Doc.comment comments; node]) in
344342
doc
345343
| comment :: (next_comment :: _comments as rest) ->
346344
let cmt_doc = print_leading_comment ~next_comment comment in
@@ -398,7 +396,9 @@ let print_comments_before_marker doc (tbl : Comment_table.t) loc marker =
398396
Hashtbl.replace tbl.trailing loc after_marker;
399397
print_trailing_comments doc tbl.trailing loc
400398

401-
let print_list ~get_loc ~nodes ~print ?(force_break = false) t =
399+
(* [~space_multiline] adds a blank line next to nodes printed on several lines *)
400+
let print_list ~get_loc ~nodes ~print ?(force_break = false)
401+
?(space_multiline = false) t =
402402
let rec loop first_loc_opt prev_loc_opt acc nodes =
403403
match nodes with
404404
| [] -> (first_loc_opt, prev_loc_opt, Doc.concat (List.rev acc))
@@ -414,15 +414,17 @@ let print_list ~get_loc ~nodes ~print ?(force_break = false) t =
414414
else
415415
let doc = print_comments raw_doc t loc in
416416
let acc =
417-
match prev_loc_opt with
418-
| None -> [doc]
419-
| Some prev_loc ->
417+
match (prev_loc_opt, acc) with
418+
| Some prev_loc, prev_doc :: _ ->
420419
let sep =
421420
if start_pos.pos_lnum - prev_loc.Location.loc_end.pos_lnum > 1
422421
then Doc.concat [Doc.hard_line; Doc.hard_line]
422+
else if space_multiline then
423+
Doc.item_separator ~prev:prev_doc ~next:doc
423424
else Doc.hard_line
424425
in
425426
doc :: sep :: acc
427+
| _ -> [doc]
426428
in
427429
let first_loc_opt =
428430
match first_loc_opt with
@@ -701,7 +703,7 @@ let rec print_structure ~state (s : Parsetree.structure) t =
701703
| [] -> print_comments_inside_file t
702704
| structure ->
703705
let previous_can_continue = ref false in
704-
print_list
706+
print_list ~space_multiline:true
705707
~get_loc:(fun s -> s.Parsetree.pstr_loc)
706708
~nodes:structure
707709
~print:(fun item cmt_tbl ->
@@ -1119,7 +1121,7 @@ and print_signature ~state signature cmt_tbl =
11191121
match signature with
11201122
| [] -> print_comments_inside_file cmt_tbl
11211123
| signature ->
1122-
print_list
1124+
print_list ~space_multiline:true
11231125
~get_loc:(fun s -> s.Parsetree.psig_loc)
11241126
~nodes:signature
11251127
~print:(print_signature_item ~state)

0 commit comments

Comments
 (0)