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 +4 -4
- data/CHANGELOG.md +51 -0
- data/Cargo.lock +1 -1
- data/README.md +22 -18
- data/ext/rfmt/Cargo.toml +4 -1
- data/ext/rfmt/src/doc/builders.rs +528 -0
- data/ext/rfmt/src/doc/mod.rs +220 -0
- data/ext/rfmt/src/doc/printer.rs +721 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +250 -0
- data/ext/rfmt/src/format/mod.rs +35 -0
- data/ext/rfmt/src/format/registry.rs +195 -0
- data/ext/rfmt/src/format/rule.rs +726 -0
- data/ext/rfmt/src/format/rules/begin.rs +434 -0
- data/ext/rfmt/src/format/rules/body_end.rs +233 -0
- data/ext/rfmt/src/format/rules/call.rs +448 -0
- data/ext/rfmt/src/format/rules/case.rs +359 -0
- data/ext/rfmt/src/format/rules/class.rs +160 -0
- data/ext/rfmt/src/format/rules/def.rs +216 -0
- data/ext/rfmt/src/format/rules/fallback.rs +130 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +454 -0
- data/ext/rfmt/src/format/rules/loops.rs +325 -0
- data/ext/rfmt/src/format/rules/mod.rs +31 -0
- data/ext/rfmt/src/format/rules/module.rs +150 -0
- data/ext/rfmt/src/format/rules/singleton_class.rs +202 -0
- data/ext/rfmt/src/format/rules/statements.rs +122 -0
- data/ext/rfmt/src/format/rules/variable_write.rs +314 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- data/lib/rfmt/prism_bridge.rb +43 -12
- data/lib/rfmt/version.rb +1 -1
- data/lib/ruby_lsp/rfmt/formatter_runner.rb +2 -0
- metadata +23 -2
- data/ext/rfmt/src/emitter/mod.rs +0 -1844
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 327f57f105df77a0d6b77bda9ab87c4f234f03f23fb918d74679942456c5a8ec
|
|
4
|
+
data.tar.gz: 482a5406275e422fb306970ffb47708f37be0c3115bc92485996d2adde6c9463
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -361,30 +361,34 @@ end
|
|
|
361
361
|
|
|
362
362
|
## Editor Integration
|
|
363
363
|
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|