rfmt 1.5.3 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9257e8d3dca2d3c849ce3040387b7f0430bf110f70f4ca3951b24ada8365b43e
4
- data.tar.gz: c3a1a6ac54502ae6cce494534964f80078bca40fd7818b75b5b483010db91b2a
3
+ metadata.gz: 327f57f105df77a0d6b77bda9ab87c4f234f03f23fb918d74679942456c5a8ec
4
+ data.tar.gz: 482a5406275e422fb306970ffb47708f37be0c3115bc92485996d2adde6c9463
5
5
  SHA512:
6
- metadata.gz: 53dbf78b4b9e7bf20f52ddb430464bae9e449c874963665d9748f77ea6aa918e9c0160ef1f6358f8ed4cbe4e29b1e13e4d37a532cd43d401333746a12cd188d3
7
- data.tar.gz: b84e983d37f74f7c3bcb8ff98d4b382093cd47a29249bc8a4f50a4a2456d0b0a264f28defa76e203a55d62b376e0207351dbbd566d0eba327aa42ba46f4cd699
6
+ metadata.gz: 52982745650ed439f2ac7a5a1a9a26ae2dcf9e9df8015526278f58508929e15a1d72425d2d3c5963c0a4008968e20fcb6c374881122a3e99618d2cef15434eaf
7
+ data.tar.gz: 10e8282f05caacdffcbb2d7042e9f95a08c5d08c9b081307a12752840549fbb08b6bc557a256a9a46fd012986369fd7cfd0442a46d1d81146c2fb234d0b936f7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.6.1] - 2026-04-24
4
+
5
+ Follow-up release consolidating the 1.6.0 architecture work.
6
+
7
+ ### Rule-based formatter
8
+
9
+ The rule-based `format/` pipeline (`Formatter`, `Registry`, `Rule`) is the canonical formatting path, replacing the legacy monolithic emitter. Rules covering body indentation (`StatementsRule`), singleton classes (`SingletonClassRule`), and variable writes (`VariableWriteRule`) ship as part of the default registry. The Intermediate Representation (IR) module decouples parsing from emission for composability and testability.
10
+
11
+ ### Method chain reformatting
12
+
13
+ Multi-line method chains can be reformatted from aligned style (indented under the first dot) to indented style (one level beyond the receiver), preserving the source's base indent. The pass is wired into the fallback path for resilience.
14
+
15
+ ### Printer optimizations
16
+
17
+ The printer carries a pre-computed indent cache and inline hints for the hot path; `reformat_chain_lines` has been deduplicated across rules and uses `Cow<str>` to avoid allocations on pass-through.
18
+
19
+ ### Editor integration
20
+
21
+ Setup guides for VSCode, Neovim, Helix, Emacs, and Zed land in the repository; every editor uses the Ruby LSP addon system, so there are no editor-specific plugins to maintain. The README's Editor Integration section replaces the previous "Coming Soon" placeholder with a VSCode quick start.
22
+
23
+ ## [1.6.0] - 2026-04-23
24
+
25
+ ### Added
26
+ - **Rule-based formatter architecture**: new modular `format/` pipeline (`Formatter`, `Registry`, `Rule`) replacing the legacy monolithic emitter
27
+ - **Intermediate Representation (IR) module**: decouples parsing from emission for composability and testability
28
+ - New formatter rules: `StatementsRule` (body indentation), `SingletonClassRule`, `VariableWriteRule`
29
+ - Method chain reformatting: convert aligned style to indented style when lines exceed the configured width
30
+ - Chain reformatting wired into the fallback path for resilience
31
+ - `config` module exported for test consumption
32
+ - **Editor Integration Documentation**: comprehensive setup guides for VSCode, Neovim, Helix, Emacs, and Zed
33
+ - VSCode: Format on Save configuration with Ruby LSP, settings reference table, project-specific setup
34
+ - Zed: full configuration with `initialization_options` and `format_on_save`
35
+ - All editors work through the Ruby LSP addon system — no editor-specific plugins required
36
+ - README: Editor Integration section updated with VSCode quick start (replacing "Coming Soon")
37
+
38
+ ### Changed
39
+ - Printer optimized with indent cache and inline hints
40
+ - `reformat_chain_lines` deduplicated across rules and optimized with `Cow` to reduce allocations
41
+ - README: Neovim integration updated from CLI-based to Ruby LSP-based approach
42
+ - Removed Sublime Text section from editor documentation (replaced by Zed)
43
+
44
+ ### Fixed
45
+ - Prism comment JSON deserialization now accepts `comment_type` and `embdoc` fields (#97)
46
+ - BTreeMap range panic when computing comment indices on edge inputs
47
+ - Comment duplication during source extraction
48
+ - Empty source input handled gracefully by the formatter runner
49
+ - Clippy warnings: use `repeat_n`
50
+
51
+ ### Removed
52
+ - Legacy `Emitter` module (1844 LOC) — superseded by the new `Formatter`
53
+
3
54
  ## [1.5.3] - 2026-02-22
4
55
 
5
56
  ### Changed
data/Cargo.lock CHANGED
@@ -1251,7 +1251,7 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
1251
1251
 
1252
1252
  [[package]]
1253
1253
  name = "rfmt"
1254
- version = "1.5.3"
1254
+ version = "1.6.1"
1255
1255
  dependencies = [
1256
1256
  "anyhow",
1257
1257
  "clap",
data/README.md CHANGED
@@ -361,30 +361,34 @@ end
361
361
 
362
362
  ## Editor Integration
363
363
 
364
- ### Neovim
364
+ rfmt integrates with editors through [Ruby LSP](https://shopify.github.io/ruby-lsp/). For detailed setup instructions, see [Editor Integration Guide](docs/editors.md).
365
+
366
+ ### VSCode (Quick Start)
365
367
 
366
- Format Ruby files on save using autocmd:
368
+ 1. Install [Ruby LSP extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
369
+ 2. Add to your `settings.json`:
370
+
371
+ ```json
372
+ {
373
+ "rubyLsp.formatter": "rfmt",
374
+ "editor.formatOnSave": true,
375
+ "[ruby]": {
376
+ "editor.defaultFormatter": "Shopify.ruby-lsp"
377
+ }
378
+ }
379
+ ```
380
+
381
+ ### Neovim
367
382
 
368
383
  ```lua
369
- -- ~/.config/nvim/init.lua
370
-
371
- vim.api.nvim_create_autocmd("BufWritePre", {
372
- pattern = { "*.rb", "*.rake", "Gemfile", "Rakefile" },
373
- callback = function()
374
- local filepath = vim.fn.expand("%:p")
375
- local result = vim.fn.system({ "rfmt", filepath })
376
- if vim.v.shell_error == 0 then
377
- vim.cmd("edit!")
378
- end
379
- end,
384
+ require("lspconfig").ruby_lsp.setup({
385
+ init_options = {
386
+ formatter = "rfmt"
387
+ }
380
388
  })
381
389
  ```
382
390
 
383
- ### Coming Soon
384
-
385
- - **VS Code** - Extension in development
386
- - **RubyMine** - Plugin in development
387
- - **Zed** - Extension in development
391
+ See [Editor Integration Guide](docs/editors.md) for Helix, Emacs, Sublime Text, and more.
388
392
 
389
393
  ## Development
390
394
 
data/ext/rfmt/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rfmt"
3
- version = "1.5.3"
3
+ version = "1.6.1"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -57,3 +57,6 @@ tempfile = "3.8"
57
57
  [features]
58
58
  default = ["cli"]
59
59
  cli = ["clap"]
60
+
61
+ # Note: Benchmarks are included as part of the test suite due to Ruby FFI linking constraints.
62
+ # Run performance tests with: cargo test --release perf_
@@ -0,0 +1,528 @@
1
+ //! Builder functions for constructing Doc IR.
2
+ //!
3
+ //! These functions provide a convenient API for building Doc trees.
4
+ //! They are inspired by Prettier's document builders.
5
+ //!
6
+ //! # Example
7
+ //!
8
+ //! ```rust
9
+ //! use rfmt::doc::builders::*;
10
+ //!
11
+ //! // Build a simple Ruby method
12
+ //! let doc = concat(vec![
13
+ //! text("def foo"),
14
+ //! hardline(),
15
+ //! indent(concat(vec![
16
+ //! text("puts \"hello\""),
17
+ //! ])),
18
+ //! hardline(),
19
+ //! text("end"),
20
+ //! ]);
21
+ //! ```
22
+
23
+ use super::{Doc, GroupId};
24
+
25
+ /// Creates a text document.
26
+ ///
27
+ /// # Example
28
+ /// ```rust
29
+ /// let doc = text("hello");
30
+ /// ```
31
+ pub fn text<S: Into<String>>(s: S) -> Doc {
32
+ Doc::Text(s.into())
33
+ }
34
+
35
+ /// Concatenates multiple documents.
36
+ ///
37
+ /// # Example
38
+ /// ```rust
39
+ /// let doc = concat(vec![text("a"), text("b"), text("c")]);
40
+ /// // Prints: "abc"
41
+ /// ```
42
+ pub fn concat(docs: Vec<Doc>) -> Doc {
43
+ // Flatten nested Concats and remove Empty docs
44
+ let flattened: Vec<Doc> = docs
45
+ .into_iter()
46
+ .flat_map(|doc| match doc {
47
+ Doc::Concat(inner) => inner,
48
+ Doc::Empty => vec![],
49
+ other => vec![other],
50
+ })
51
+ .collect();
52
+
53
+ match flattened.len() {
54
+ 0 => Doc::Empty,
55
+ 1 => flattened.into_iter().next().unwrap(),
56
+ _ => Doc::Concat(flattened),
57
+ }
58
+ }
59
+
60
+ /// Creates a group that tries to fit on one line.
61
+ ///
62
+ /// If the contents fit within the line width, Line docs become spaces.
63
+ /// Otherwise, they become newlines.
64
+ ///
65
+ /// # Example
66
+ /// ```rust
67
+ /// let doc = group(concat(vec![
68
+ /// text("["),
69
+ /// softline(),
70
+ /// text("1, 2, 3"),
71
+ /// softline(),
72
+ /// text("]"),
73
+ /// ]));
74
+ /// // Short version: "[1, 2, 3]"
75
+ /// // Long version:
76
+ /// // [
77
+ /// // 1, 2, 3
78
+ /// // ]
79
+ /// ```
80
+ pub fn group(contents: Doc) -> Doc {
81
+ Doc::Group {
82
+ contents: Box::new(contents),
83
+ break_parent: false,
84
+ id: None,
85
+ }
86
+ }
87
+
88
+ /// Creates a group with a specific ID for IfBreak references.
89
+ pub fn group_with_id(contents: Doc, id: GroupId) -> Doc {
90
+ Doc::Group {
91
+ contents: Box::new(contents),
92
+ break_parent: false,
93
+ id: Some(id),
94
+ }
95
+ }
96
+
97
+ /// Creates a group that forces parent groups to break.
98
+ pub fn group_break(contents: Doc) -> Doc {
99
+ Doc::Group {
100
+ contents: Box::new(contents),
101
+ break_parent: true,
102
+ id: None,
103
+ }
104
+ }
105
+
106
+ /// Increases indentation for the contents.
107
+ ///
108
+ /// # Example
109
+ /// ```rust
110
+ /// let doc = concat(vec![
111
+ /// text("class Foo"),
112
+ /// hardline(),
113
+ /// indent(text("def bar; end")),
114
+ /// hardline(),
115
+ /// text("end"),
116
+ /// ]);
117
+ /// // Prints:
118
+ /// // class Foo
119
+ /// // def bar; end
120
+ /// // end
121
+ /// ```
122
+ pub fn indent(contents: Doc) -> Doc {
123
+ Doc::Indent(Box::new(contents))
124
+ }
125
+
126
+ /// A line break that becomes a space in flat mode.
127
+ ///
128
+ /// - In flat mode (group fits on one line): prints a space
129
+ /// - In break mode (group doesn't fit): prints a newline + indentation
130
+ ///
131
+ /// # Example
132
+ /// ```rust
133
+ /// group(concat(vec![text("a"), line(), text("b")]))
134
+ /// // Flat: "a b"
135
+ /// // Break: "a\n b"
136
+ /// ```
137
+ pub fn line() -> Doc {
138
+ Doc::Line {
139
+ soft: false,
140
+ hard: false,
141
+ literal: false,
142
+ }
143
+ }
144
+
145
+ /// A line break that disappears in flat mode.
146
+ ///
147
+ /// - In flat mode: prints nothing
148
+ /// - In break mode: prints a newline + indentation
149
+ ///
150
+ /// # Example
151
+ /// ```rust
152
+ /// group(concat(vec![text("["), softline(), text("1"), softline(), text("]")]))
153
+ /// // Flat: "[1]"
154
+ /// // Break: "[\n 1\n]"
155
+ /// ```
156
+ pub fn softline() -> Doc {
157
+ Doc::Line {
158
+ soft: true,
159
+ hard: false,
160
+ literal: false,
161
+ }
162
+ }
163
+
164
+ /// A line break that always prints a newline.
165
+ ///
166
+ /// This forces a line break regardless of whether the content fits.
167
+ ///
168
+ /// # Example
169
+ /// ```rust
170
+ /// concat(vec![text("line1"), hardline(), text("line2")])
171
+ /// // Always prints:
172
+ /// // line1
173
+ /// // line2
174
+ /// ```
175
+ pub fn hardline() -> Doc {
176
+ Doc::Line {
177
+ soft: false,
178
+ hard: true,
179
+ literal: false,
180
+ }
181
+ }
182
+
183
+ /// A literal line break without indentation.
184
+ ///
185
+ /// Used for heredocs where the content should not be indented.
186
+ ///
187
+ /// # Example
188
+ /// ```rust
189
+ /// concat(vec![text("<<~HEREDOC"), literalline(), text("content"), literalline(), text("HEREDOC")])
190
+ /// ```
191
+ pub fn literalline() -> Doc {
192
+ Doc::Line {
193
+ soft: false,
194
+ hard: true,
195
+ literal: true,
196
+ }
197
+ }
198
+
199
+ /// Conditional content based on group break state.
200
+ ///
201
+ /// # Arguments
202
+ /// - `break_contents`: Printed when the group is broken (multi-line)
203
+ /// - `flat_contents`: Printed when the group is flat (single line)
204
+ ///
205
+ /// # Example
206
+ /// ```rust
207
+ /// group(concat(vec![
208
+ /// text("["),
209
+ /// if_break(
210
+ /// concat(vec![hardline(), indent(text("1, 2, 3")), hardline()]),
211
+ /// text("1, 2, 3"),
212
+ /// ),
213
+ /// text("]"),
214
+ /// ]))
215
+ /// // Flat: "[1, 2, 3]"
216
+ /// // Break:
217
+ /// // [
218
+ /// // 1, 2, 3
219
+ /// // ]
220
+ /// ```
221
+ pub fn if_break(break_contents: Doc, flat_contents: Doc) -> Doc {
222
+ Doc::IfBreak {
223
+ break_contents: Box::new(break_contents),
224
+ flat_contents: Box::new(flat_contents),
225
+ group_id: None,
226
+ }
227
+ }
228
+
229
+ /// Conditional content referencing a specific group.
230
+ pub fn if_break_with_group(break_contents: Doc, flat_contents: Doc, group_id: GroupId) -> Doc {
231
+ Doc::IfBreak {
232
+ break_contents: Box::new(break_contents),
233
+ flat_contents: Box::new(flat_contents),
234
+ group_id: Some(group_id),
235
+ }
236
+ }
237
+
238
+ /// Joins documents with a separator.
239
+ ///
240
+ /// # Example
241
+ /// ```rust
242
+ /// join(text(", "), vec![text("a"), text("b"), text("c")])
243
+ /// // Prints: "a, b, c"
244
+ /// ```
245
+ pub fn join(separator: Doc, docs: Vec<Doc>) -> Doc {
246
+ if docs.is_empty() {
247
+ return Doc::Empty;
248
+ }
249
+
250
+ let mut result = Vec::with_capacity(docs.len() * 2 - 1);
251
+ let mut first = true;
252
+
253
+ for doc in docs {
254
+ if !first {
255
+ result.push(separator.clone());
256
+ }
257
+ result.push(doc);
258
+ first = false;
259
+ }
260
+
261
+ concat(result)
262
+ }
263
+
264
+ /// Joins documents with a line separator.
265
+ ///
266
+ /// In flat mode, uses spaces. In break mode, uses newlines.
267
+ ///
268
+ /// # Example
269
+ /// ```rust
270
+ /// group(join_line(vec![text("a"), text("b"), text("c")]))
271
+ /// // Flat: "a b c"
272
+ /// // Break:
273
+ /// // a
274
+ /// // b
275
+ /// // c
276
+ /// ```
277
+ pub fn join_line(docs: Vec<Doc>) -> Doc {
278
+ join(line(), docs)
279
+ }
280
+
281
+ /// Joins documents with a softline separator.
282
+ pub fn join_softline(docs: Vec<Doc>) -> Doc {
283
+ join(softline(), docs)
284
+ }
285
+
286
+ /// Joins documents with a hardline separator.
287
+ pub fn join_hardline(docs: Vec<Doc>) -> Doc {
288
+ join(hardline(), docs)
289
+ }
290
+
291
+ /// Creates an empty document.
292
+ pub fn empty() -> Doc {
293
+ Doc::Empty
294
+ }
295
+
296
+ /// Creates a trailing comment.
297
+ ///
298
+ /// # Example
299
+ /// ```rust
300
+ /// concat(vec![text("code"), trailing_comment("# comment")])
301
+ /// // Prints: "code # comment"
302
+ /// ```
303
+ pub fn trailing_comment<S: Into<String>>(text: S) -> Doc {
304
+ Doc::TrailingComment(text.into())
305
+ }
306
+
307
+ /// Creates a leading comment.
308
+ ///
309
+ /// # Example
310
+ /// ```rust
311
+ /// concat(vec![leading_comment("# comment", true), text("code")])
312
+ /// // Prints:
313
+ /// // # comment
314
+ /// // code
315
+ /// ```
316
+ pub fn leading_comment<S: Into<String>>(text: S, hard_line_after: bool) -> Doc {
317
+ Doc::LeadingComment {
318
+ text: text.into(),
319
+ hard_line_after,
320
+ }
321
+ }
322
+
323
+ /// Aligns content to a specific column offset.
324
+ pub fn align(n: usize, contents: Doc) -> Doc {
325
+ Doc::Align {
326
+ n,
327
+ contents: Box::new(contents),
328
+ }
329
+ }
330
+
331
+ /// Content to be appended at the end of the line.
332
+ pub fn line_suffix(contents: Doc) -> Doc {
333
+ Doc::LineSuffix(Box::new(contents))
334
+ }
335
+
336
+ /// Fill: packs content into lines as tightly as possible.
337
+ pub fn fill(docs: Vec<Doc>) -> Doc {
338
+ Doc::Fill(docs)
339
+ }
340
+
341
+ /// Creates multiple hardlines (blank lines).
342
+ ///
343
+ /// # Example
344
+ /// ```rust
345
+ /// blank_lines(2)
346
+ /// // Prints two newlines (one blank line between)
347
+ /// ```
348
+ pub fn blank_lines(count: usize) -> Doc {
349
+ if count == 0 {
350
+ return Doc::Empty;
351
+ }
352
+
353
+ let lines: Vec<Doc> = (0..count).map(|_| hardline()).collect();
354
+ concat(lines)
355
+ }
356
+
357
+ #[cfg(test)]
358
+ mod tests {
359
+ use super::*;
360
+
361
+ #[test]
362
+ fn test_text() {
363
+ let doc = text("hello");
364
+ assert_eq!(doc, Doc::Text("hello".to_string()));
365
+ }
366
+
367
+ #[test]
368
+ fn test_concat_flattens() {
369
+ let doc = concat(vec![
370
+ text("a"),
371
+ concat(vec![text("b"), text("c")]),
372
+ text("d"),
373
+ ]);
374
+
375
+ // Should flatten to a single Concat with 4 elements
376
+ if let Doc::Concat(docs) = doc {
377
+ assert_eq!(docs.len(), 4);
378
+ } else {
379
+ panic!("Expected Concat");
380
+ }
381
+ }
382
+
383
+ #[test]
384
+ fn test_concat_removes_empty() {
385
+ let doc = concat(vec![text("a"), empty(), text("b")]);
386
+
387
+ if let Doc::Concat(docs) = doc {
388
+ assert_eq!(docs.len(), 2);
389
+ } else {
390
+ panic!("Expected Concat");
391
+ }
392
+ }
393
+
394
+ #[test]
395
+ fn test_concat_single_element() {
396
+ let doc = concat(vec![text("a")]);
397
+ assert_eq!(doc, Doc::Text("a".to_string()));
398
+ }
399
+
400
+ #[test]
401
+ fn test_concat_empty() {
402
+ let doc = concat(vec![]);
403
+ assert_eq!(doc, Doc::Empty);
404
+ }
405
+
406
+ #[test]
407
+ fn test_group() {
408
+ let doc = group(text("hello"));
409
+ if let Doc::Group {
410
+ contents,
411
+ break_parent,
412
+ id,
413
+ } = doc
414
+ {
415
+ assert_eq!(*contents, Doc::Text("hello".to_string()));
416
+ assert!(!break_parent);
417
+ assert!(id.is_none());
418
+ } else {
419
+ panic!("Expected Group");
420
+ }
421
+ }
422
+
423
+ #[test]
424
+ fn test_indent() {
425
+ let doc = indent(text("body"));
426
+ if let Doc::Indent(contents) = doc {
427
+ assert_eq!(*contents, Doc::Text("body".to_string()));
428
+ } else {
429
+ panic!("Expected Indent");
430
+ }
431
+ }
432
+
433
+ #[test]
434
+ fn test_line_variants() {
435
+ let l = line();
436
+ assert_eq!(
437
+ l,
438
+ Doc::Line {
439
+ soft: false,
440
+ hard: false,
441
+ literal: false
442
+ }
443
+ );
444
+
445
+ let sl = softline();
446
+ assert_eq!(
447
+ sl,
448
+ Doc::Line {
449
+ soft: true,
450
+ hard: false,
451
+ literal: false
452
+ }
453
+ );
454
+
455
+ let hl = hardline();
456
+ assert_eq!(
457
+ hl,
458
+ Doc::Line {
459
+ soft: false,
460
+ hard: true,
461
+ literal: false
462
+ }
463
+ );
464
+
465
+ let ll = literalline();
466
+ assert_eq!(
467
+ ll,
468
+ Doc::Line {
469
+ soft: false,
470
+ hard: true,
471
+ literal: true
472
+ }
473
+ );
474
+ }
475
+
476
+ #[test]
477
+ fn test_join() {
478
+ let doc = join(text(", "), vec![text("a"), text("b"), text("c")]);
479
+
480
+ if let Doc::Concat(docs) = doc {
481
+ assert_eq!(docs.len(), 5); // a, ", ", b, ", ", c
482
+ } else {
483
+ panic!("Expected Concat");
484
+ }
485
+ }
486
+
487
+ #[test]
488
+ fn test_join_empty() {
489
+ let doc = join(text(", "), vec![]);
490
+ assert_eq!(doc, Doc::Empty);
491
+ }
492
+
493
+ #[test]
494
+ fn test_join_single() {
495
+ let doc = join(text(", "), vec![text("a")]);
496
+ assert_eq!(doc, Doc::Text("a".to_string()));
497
+ }
498
+
499
+ #[test]
500
+ fn test_if_break() {
501
+ let doc = if_break(text("broken"), text("flat"));
502
+ if let Doc::IfBreak {
503
+ break_contents,
504
+ flat_contents,
505
+ group_id,
506
+ } = doc
507
+ {
508
+ assert_eq!(*break_contents, Doc::Text("broken".to_string()));
509
+ assert_eq!(*flat_contents, Doc::Text("flat".to_string()));
510
+ assert!(group_id.is_none());
511
+ } else {
512
+ panic!("Expected IfBreak");
513
+ }
514
+ }
515
+
516
+ #[test]
517
+ fn test_blank_lines() {
518
+ let doc = blank_lines(0);
519
+ assert_eq!(doc, Doc::Empty);
520
+
521
+ let doc = blank_lines(2);
522
+ if let Doc::Concat(docs) = doc {
523
+ assert_eq!(docs.len(), 2);
524
+ } else {
525
+ panic!("Expected Concat");
526
+ }
527
+ }
528
+ }