rfmt 1.5.2 → 1.6.0

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.
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.2"
3
+ version = "1.6.0"
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
+ }