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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/Cargo.lock +266 -92
- 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 +684 -0
- data/ext/rfmt/src/format/context.rs +448 -0
- data/ext/rfmt/src/format/formatter.rs +226 -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 +555 -0
- data/ext/rfmt/src/format/rules/begin.rs +295 -0
- data/ext/rfmt/src/format/rules/body_end.rs +109 -0
- data/ext/rfmt/src/format/rules/call.rs +409 -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 +116 -0
- data/ext/rfmt/src/format/rules/if_unless.rs +407 -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 +296 -0
- data/ext/rfmt/src/lib.rs +8 -5
- data/ext/rfmt/src/parser/prism_adapter.rs +157 -2
- 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 -1760
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
//! Printer converts Doc IR to a formatted string.
|
|
2
|
+
//!
|
|
3
|
+
//! The printer implements a Prettier-style algorithm that:
|
|
4
|
+
//! 1. Tries to fit groups on a single line
|
|
5
|
+
//! 2. Breaks groups into multiple lines when they don't fit
|
|
6
|
+
//! 3. Manages indentation automatically
|
|
7
|
+
//!
|
|
8
|
+
//! # Algorithm
|
|
9
|
+
//!
|
|
10
|
+
//! The printer uses a stack-based approach to process Doc nodes.
|
|
11
|
+
//! For each Group, it measures whether the contents fit in the remaining
|
|
12
|
+
//! line width. If they fit, Line docs become spaces (flat mode).
|
|
13
|
+
//! If they don't fit, Line docs become newlines (break mode).
|
|
14
|
+
|
|
15
|
+
use super::Doc;
|
|
16
|
+
use crate::config::{Config, IndentStyle};
|
|
17
|
+
|
|
18
|
+
/// Print mode for Line docs within a group.
|
|
19
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
20
|
+
pub enum Mode {
|
|
21
|
+
/// Group fits on one line. Line docs become spaces.
|
|
22
|
+
Flat,
|
|
23
|
+
/// Group doesn't fit. Line docs become newlines.
|
|
24
|
+
Break,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// A command in the print stack.
|
|
28
|
+
#[derive(Debug)]
|
|
29
|
+
struct PrintCommand<'a> {
|
|
30
|
+
/// Current indentation level (in spaces)
|
|
31
|
+
indent: usize,
|
|
32
|
+
/// Print mode (Flat or Break)
|
|
33
|
+
mode: Mode,
|
|
34
|
+
/// The Doc to print
|
|
35
|
+
doc: &'a Doc,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Converts Doc IR to a formatted string.
|
|
39
|
+
pub struct Printer<'a> {
|
|
40
|
+
/// Configuration for formatting
|
|
41
|
+
config: &'a Config,
|
|
42
|
+
/// Output buffer
|
|
43
|
+
output: String,
|
|
44
|
+
/// Current column position (0-indexed)
|
|
45
|
+
pos: usize,
|
|
46
|
+
/// Pre-computed indent strings by width (avoids allocation during print)
|
|
47
|
+
indent_cache: Vec<String>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MAX_PRECACHED_INDENT: usize = 32;
|
|
51
|
+
|
|
52
|
+
impl<'a> Printer<'a> {
|
|
53
|
+
/// Creates a new printer with the given configuration.
|
|
54
|
+
pub fn new(config: &'a Config) -> Self {
|
|
55
|
+
let indent_width = config.formatting.indent_width;
|
|
56
|
+
let indent_style = &config.formatting.indent_style;
|
|
57
|
+
|
|
58
|
+
let indent_cache = (0..=MAX_PRECACHED_INDENT)
|
|
59
|
+
.map(|width| Self::build_indent_string(width, indent_width, indent_style))
|
|
60
|
+
.collect();
|
|
61
|
+
|
|
62
|
+
Self {
|
|
63
|
+
config,
|
|
64
|
+
output: String::new(),
|
|
65
|
+
pos: 0,
|
|
66
|
+
indent_cache,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[inline]
|
|
71
|
+
fn build_indent_string(width: usize, indent_width: usize, style: &IndentStyle) -> String {
|
|
72
|
+
match style {
|
|
73
|
+
IndentStyle::Spaces => " ".repeat(width),
|
|
74
|
+
IndentStyle::Tabs if indent_width == 0 => String::new(),
|
|
75
|
+
IndentStyle::Tabs => {
|
|
76
|
+
let tabs = width / indent_width;
|
|
77
|
+
let spaces = width % indent_width;
|
|
78
|
+
if spaces == 0 {
|
|
79
|
+
"\t".repeat(tabs)
|
|
80
|
+
} else {
|
|
81
|
+
let mut result = String::with_capacity(tabs + spaces);
|
|
82
|
+
result.extend(std::iter::repeat_n('\t', tabs));
|
|
83
|
+
result.extend(std::iter::repeat_n(' ', spaces));
|
|
84
|
+
result
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Prints a Doc to a string.
|
|
91
|
+
#[inline]
|
|
92
|
+
pub fn print(&mut self, doc: &Doc) -> String {
|
|
93
|
+
self.output.clear();
|
|
94
|
+
self.pos = 0;
|
|
95
|
+
|
|
96
|
+
let mut commands: Vec<PrintCommand> = vec![PrintCommand {
|
|
97
|
+
indent: 0,
|
|
98
|
+
mode: Mode::Break, // Start in break mode
|
|
99
|
+
doc,
|
|
100
|
+
}];
|
|
101
|
+
|
|
102
|
+
while let Some(cmd) = commands.pop() {
|
|
103
|
+
self.process_command(cmd, &mut commands);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ensure trailing newline
|
|
107
|
+
if !self.output.is_empty() && !self.output.ends_with('\n') {
|
|
108
|
+
self.output.push('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
std::mem::take(&mut self.output)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Processes a single print command.
|
|
115
|
+
#[inline]
|
|
116
|
+
fn process_command<'b>(&mut self, cmd: PrintCommand<'b>, commands: &mut Vec<PrintCommand<'b>>) {
|
|
117
|
+
match cmd.doc {
|
|
118
|
+
Doc::Text(s) => {
|
|
119
|
+
self.output.push_str(s);
|
|
120
|
+
self.pos += s.chars().count();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Doc::Concat(docs) => {
|
|
124
|
+
// Push in reverse order so first doc is processed first
|
|
125
|
+
for doc in docs.iter().rev() {
|
|
126
|
+
commands.push(PrintCommand {
|
|
127
|
+
indent: cmd.indent,
|
|
128
|
+
mode: cmd.mode,
|
|
129
|
+
doc,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Doc::Group {
|
|
135
|
+
contents,
|
|
136
|
+
break_parent,
|
|
137
|
+
..
|
|
138
|
+
} => {
|
|
139
|
+
// Determine if the group fits on the remaining line
|
|
140
|
+
let remaining = self.config.formatting.line_length.saturating_sub(self.pos);
|
|
141
|
+
let fits = !*break_parent && self.fits(contents, cmd.indent, remaining, cmd.mode);
|
|
142
|
+
|
|
143
|
+
let mode = if fits { Mode::Flat } else { Mode::Break };
|
|
144
|
+
|
|
145
|
+
commands.push(PrintCommand {
|
|
146
|
+
indent: cmd.indent,
|
|
147
|
+
mode,
|
|
148
|
+
doc: contents,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Doc::Line {
|
|
153
|
+
soft,
|
|
154
|
+
hard,
|
|
155
|
+
literal,
|
|
156
|
+
} => match (cmd.mode, *hard) {
|
|
157
|
+
(_, true) => {
|
|
158
|
+
self.output.push('\n');
|
|
159
|
+
if !*literal {
|
|
160
|
+
let indent_str = self.get_indent(cmd.indent);
|
|
161
|
+
self.output.push_str(&indent_str);
|
|
162
|
+
self.pos = cmd.indent;
|
|
163
|
+
} else {
|
|
164
|
+
self.pos = 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
(Mode::Flat, false) if *soft => {}
|
|
168
|
+
(Mode::Flat, false) => {
|
|
169
|
+
self.output.push(' ');
|
|
170
|
+
self.pos += 1;
|
|
171
|
+
}
|
|
172
|
+
(Mode::Break, false) => {
|
|
173
|
+
self.output.push('\n');
|
|
174
|
+
let indent_str = self.get_indent(cmd.indent);
|
|
175
|
+
self.output.push_str(&indent_str);
|
|
176
|
+
self.pos = cmd.indent;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
Doc::Indent(contents) => {
|
|
181
|
+
let new_indent = cmd.indent + self.config.formatting.indent_width;
|
|
182
|
+
commands.push(PrintCommand {
|
|
183
|
+
indent: new_indent,
|
|
184
|
+
mode: cmd.mode,
|
|
185
|
+
doc: contents,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Doc::IfBreak {
|
|
190
|
+
break_contents,
|
|
191
|
+
flat_contents,
|
|
192
|
+
..
|
|
193
|
+
} => {
|
|
194
|
+
let doc = match cmd.mode {
|
|
195
|
+
Mode::Break => break_contents,
|
|
196
|
+
Mode::Flat => flat_contents,
|
|
197
|
+
};
|
|
198
|
+
commands.push(PrintCommand {
|
|
199
|
+
indent: cmd.indent,
|
|
200
|
+
mode: cmd.mode,
|
|
201
|
+
doc,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Doc::Empty => {}
|
|
206
|
+
|
|
207
|
+
Doc::TrailingComment(text) => {
|
|
208
|
+
self.output.push(' ');
|
|
209
|
+
self.output.push_str(text);
|
|
210
|
+
self.pos += 1 + text.chars().count();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Doc::LeadingComment {
|
|
214
|
+
text,
|
|
215
|
+
hard_line_after,
|
|
216
|
+
} => {
|
|
217
|
+
self.output.push_str(text);
|
|
218
|
+
self.pos += text.chars().count();
|
|
219
|
+
if *hard_line_after {
|
|
220
|
+
self.output.push('\n');
|
|
221
|
+
let indent_str = self.get_indent(cmd.indent);
|
|
222
|
+
self.output.push_str(&indent_str);
|
|
223
|
+
self.pos = cmd.indent;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Doc::Align { n, contents } => {
|
|
228
|
+
let new_indent = cmd.indent + n;
|
|
229
|
+
commands.push(PrintCommand {
|
|
230
|
+
indent: new_indent,
|
|
231
|
+
mode: cmd.mode,
|
|
232
|
+
doc: contents,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Doc::LineSuffix(contents) => {
|
|
237
|
+
// For now, just print inline. Proper line suffix handling
|
|
238
|
+
// would buffer these until end of line.
|
|
239
|
+
commands.push(PrintCommand {
|
|
240
|
+
indent: cmd.indent,
|
|
241
|
+
mode: cmd.mode,
|
|
242
|
+
doc: contents,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Doc::Fill(docs) => {
|
|
247
|
+
// Fill tries to fit as many items on each line as possible.
|
|
248
|
+
// For now, simple implementation: join with softline behavior.
|
|
249
|
+
for (i, doc) in docs.iter().rev().enumerate() {
|
|
250
|
+
if i > 0 {
|
|
251
|
+
commands.push(PrintCommand {
|
|
252
|
+
indent: cmd.indent,
|
|
253
|
+
mode: cmd.mode,
|
|
254
|
+
doc: &Doc::Line {
|
|
255
|
+
soft: true,
|
|
256
|
+
hard: false,
|
|
257
|
+
literal: false,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
commands.push(PrintCommand {
|
|
262
|
+
indent: cmd.indent,
|
|
263
|
+
mode: cmd.mode,
|
|
264
|
+
doc,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// Determines if a Doc fits within the remaining width.
|
|
272
|
+
///
|
|
273
|
+
/// Uses flat mode for inner groups and returns early when width is exceeded.
|
|
274
|
+
/// This is a hot path, so we pre-allocate a reasonable stack size.
|
|
275
|
+
#[inline]
|
|
276
|
+
fn fits(&self, doc: &Doc, indent: usize, remaining: usize, mode: Mode) -> bool {
|
|
277
|
+
let mut width = 0usize;
|
|
278
|
+
// Pre-allocate stack with reasonable capacity for typical nesting depth
|
|
279
|
+
let mut stack: Vec<(&Doc, usize, Mode)> = Vec::with_capacity(16);
|
|
280
|
+
stack.push((doc, indent, mode));
|
|
281
|
+
|
|
282
|
+
while let Some((doc, indent, mode)) = stack.pop() {
|
|
283
|
+
// Early exit: use >= for slightly earlier termination
|
|
284
|
+
if width >= remaining {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
match doc {
|
|
289
|
+
Doc::Text(s) => {
|
|
290
|
+
// Use len() for ASCII strings (common case), chars().count() for accuracy
|
|
291
|
+
width += if s.is_ascii() {
|
|
292
|
+
s.len()
|
|
293
|
+
} else {
|
|
294
|
+
s.chars().count()
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
Doc::Concat(docs) => {
|
|
299
|
+
for d in docs.iter().rev() {
|
|
300
|
+
stack.push((d, indent, mode));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Doc::Group { contents, .. } => {
|
|
305
|
+
// Assume nested groups stay flat during fit check
|
|
306
|
+
stack.push((contents, indent, Mode::Flat));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
Doc::Line { soft, hard, .. } => {
|
|
310
|
+
if *hard {
|
|
311
|
+
// Hard line forces a break - content after fits on new line
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
match mode {
|
|
315
|
+
Mode::Flat if *soft => {} // soft line: nothing
|
|
316
|
+
Mode::Flat => width += 1, // regular line: space
|
|
317
|
+
Mode::Break => return true, // break mode: newline ends measurement
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
Doc::Indent(contents) => {
|
|
322
|
+
let new_indent = indent + self.config.formatting.indent_width;
|
|
323
|
+
stack.push((contents, new_indent, mode));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
Doc::IfBreak {
|
|
327
|
+
flat_contents,
|
|
328
|
+
break_contents,
|
|
329
|
+
..
|
|
330
|
+
} => {
|
|
331
|
+
let contents = match mode {
|
|
332
|
+
Mode::Flat => flat_contents,
|
|
333
|
+
Mode::Break => break_contents,
|
|
334
|
+
};
|
|
335
|
+
stack.push((contents, indent, mode));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
Doc::Empty => {}
|
|
339
|
+
|
|
340
|
+
Doc::TrailingComment(s) => {
|
|
341
|
+
width += 1 + if s.is_ascii() {
|
|
342
|
+
s.len()
|
|
343
|
+
} else {
|
|
344
|
+
s.chars().count()
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
Doc::LeadingComment { text, .. } => {
|
|
349
|
+
width += if text.is_ascii() {
|
|
350
|
+
text.len()
|
|
351
|
+
} else {
|
|
352
|
+
text.chars().count()
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Doc::Align { n, contents } => {
|
|
357
|
+
stack.push((contents, indent + n, mode));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
Doc::LineSuffix(contents) => {
|
|
361
|
+
stack.push((contents, indent, mode));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
Doc::Fill(docs) => {
|
|
365
|
+
for d in docs.iter().rev() {
|
|
366
|
+
stack.push((d, indent, mode));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
width <= remaining
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn get_indent(&mut self, width: usize) -> String {
|
|
376
|
+
if width < self.indent_cache.len() {
|
|
377
|
+
return self.indent_cache[width].clone();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let indent_width = self.config.formatting.indent_width;
|
|
381
|
+
let indent_style = &self.config.formatting.indent_style;
|
|
382
|
+
while self.indent_cache.len() <= width {
|
|
383
|
+
let w = self.indent_cache.len();
|
|
384
|
+
self.indent_cache
|
|
385
|
+
.push(Self::build_indent_string(w, indent_width, indent_style));
|
|
386
|
+
}
|
|
387
|
+
self.indent_cache[width].clone()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[cfg(test)]
|
|
392
|
+
mod tests {
|
|
393
|
+
use super::*;
|
|
394
|
+
use crate::doc::builders::*;
|
|
395
|
+
|
|
396
|
+
/// Helper to print a doc with default config.
|
|
397
|
+
fn print_doc(doc: &Doc) -> String {
|
|
398
|
+
let config = Config::default();
|
|
399
|
+
let mut printer = Printer::new(&config);
|
|
400
|
+
printer.print(doc)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
fn test_print_text() {
|
|
405
|
+
let doc = text("hello");
|
|
406
|
+
let result = print_doc(&doc);
|
|
407
|
+
assert_eq!(result, "hello\n");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[test]
|
|
411
|
+
fn test_print_concat() {
|
|
412
|
+
let doc = concat(vec![text("a"), text("b"), text("c")]);
|
|
413
|
+
let result = print_doc(&doc);
|
|
414
|
+
assert_eq!(result, "abc\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#[test]
|
|
418
|
+
fn test_print_hardline() {
|
|
419
|
+
let doc = concat(vec![text("line1"), hardline(), text("line2")]);
|
|
420
|
+
let result = print_doc(&doc);
|
|
421
|
+
assert_eq!(result, "line1\nline2\n");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#[test]
|
|
425
|
+
fn test_print_indent() {
|
|
426
|
+
// Correct Doc structure: hardline inside indent
|
|
427
|
+
let doc = concat(vec![
|
|
428
|
+
text("def foo"),
|
|
429
|
+
indent(concat(vec![hardline(), text("body")])),
|
|
430
|
+
hardline(),
|
|
431
|
+
text("end"),
|
|
432
|
+
]);
|
|
433
|
+
let result = print_doc(&doc);
|
|
434
|
+
assert_eq!(result, "def foo\n body\nend\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn test_print_nested_indent() {
|
|
439
|
+
// Correct Doc structure: hardline inside indent
|
|
440
|
+
let doc = concat(vec![
|
|
441
|
+
text("class Foo"),
|
|
442
|
+
indent(concat(vec![
|
|
443
|
+
hardline(),
|
|
444
|
+
text("def bar"),
|
|
445
|
+
indent(concat(vec![hardline(), text("puts 'hello'")])),
|
|
446
|
+
hardline(),
|
|
447
|
+
text("end"),
|
|
448
|
+
])),
|
|
449
|
+
hardline(),
|
|
450
|
+
text("end"),
|
|
451
|
+
]);
|
|
452
|
+
let result = print_doc(&doc);
|
|
453
|
+
assert_eq!(
|
|
454
|
+
result,
|
|
455
|
+
"class Foo\n def bar\n puts 'hello'\n end\nend\n"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#[test]
|
|
460
|
+
fn test_print_group_fits() {
|
|
461
|
+
// Short content should stay on one line
|
|
462
|
+
let doc = group(concat(vec![text("short"), line(), text("text")]));
|
|
463
|
+
let result = print_doc(&doc);
|
|
464
|
+
assert_eq!(result, "short text\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[test]
|
|
468
|
+
fn test_print_group_breaks() {
|
|
469
|
+
// Create content that doesn't fit on one line
|
|
470
|
+
let long = "a".repeat(80);
|
|
471
|
+
let doc = group(concat(vec![text(&long), line(), text("more")]));
|
|
472
|
+
let result = print_doc(&doc);
|
|
473
|
+
assert!(result.contains('\n'));
|
|
474
|
+
// Should break: long\nmore
|
|
475
|
+
assert!(result.starts_with(&long));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#[test]
|
|
479
|
+
fn test_print_softline_flat() {
|
|
480
|
+
// Softline disappears in flat mode
|
|
481
|
+
let doc = group(concat(vec![
|
|
482
|
+
text("["),
|
|
483
|
+
softline(),
|
|
484
|
+
text("1"),
|
|
485
|
+
softline(),
|
|
486
|
+
text("]"),
|
|
487
|
+
]));
|
|
488
|
+
let result = print_doc(&doc);
|
|
489
|
+
assert_eq!(result, "[1]\n");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
#[test]
|
|
493
|
+
fn test_print_if_break_flat() {
|
|
494
|
+
let doc = group(concat(vec![
|
|
495
|
+
text("["),
|
|
496
|
+
if_break(text("BROKEN"), text("FLAT")),
|
|
497
|
+
text("]"),
|
|
498
|
+
]));
|
|
499
|
+
let result = print_doc(&doc);
|
|
500
|
+
assert_eq!(result, "[FLAT]\n");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[test]
|
|
504
|
+
fn test_print_if_break_broken() {
|
|
505
|
+
// Force break with group_break (break_parent = true)
|
|
506
|
+
let doc = group_break(concat(vec![
|
|
507
|
+
text("["),
|
|
508
|
+
line(),
|
|
509
|
+
if_break(text("BROKEN"), text("FLAT")),
|
|
510
|
+
text("]"),
|
|
511
|
+
]));
|
|
512
|
+
let result = print_doc(&doc);
|
|
513
|
+
assert!(
|
|
514
|
+
result.contains("BROKEN"),
|
|
515
|
+
"Expected BROKEN but got: {}",
|
|
516
|
+
result
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn test_print_trailing_comment() {
|
|
522
|
+
let doc = concat(vec![text("code"), trailing_comment("# comment")]);
|
|
523
|
+
let result = print_doc(&doc);
|
|
524
|
+
assert_eq!(result, "code # comment\n");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
#[test]
|
|
528
|
+
fn test_print_leading_comment() {
|
|
529
|
+
let doc = concat(vec![leading_comment("# comment", true), text("code")]);
|
|
530
|
+
let result = print_doc(&doc);
|
|
531
|
+
assert_eq!(result, "# comment\ncode\n");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#[test]
|
|
535
|
+
fn test_print_empty() {
|
|
536
|
+
let doc = empty();
|
|
537
|
+
let result = print_doc(&doc);
|
|
538
|
+
assert_eq!(result, "");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#[test]
|
|
542
|
+
fn test_print_join() {
|
|
543
|
+
let doc = join(text(", "), vec![text("a"), text("b"), text("c")]);
|
|
544
|
+
let result = print_doc(&doc);
|
|
545
|
+
assert_eq!(result, "a, b, c\n");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn test_print_ruby_class() {
|
|
550
|
+
// Correct Doc structure: hardline inside indent
|
|
551
|
+
let doc = concat(vec![
|
|
552
|
+
text("class Foo"),
|
|
553
|
+
indent(concat(vec![
|
|
554
|
+
hardline(),
|
|
555
|
+
text("def initialize"),
|
|
556
|
+
indent(concat(vec![hardline(), text("@value = 1")])),
|
|
557
|
+
hardline(),
|
|
558
|
+
text("end"),
|
|
559
|
+
])),
|
|
560
|
+
hardline(),
|
|
561
|
+
text("end"),
|
|
562
|
+
]);
|
|
563
|
+
let result = print_doc(&doc);
|
|
564
|
+
|
|
565
|
+
let expected = "\
|
|
566
|
+
class Foo
|
|
567
|
+
def initialize
|
|
568
|
+
@value = 1
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
";
|
|
572
|
+
assert_eq!(result, expected);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
fn test_print_ruby_array_fits() {
|
|
577
|
+
// Array that fits on one line
|
|
578
|
+
let doc = group(concat(vec![
|
|
579
|
+
text("["),
|
|
580
|
+
softline(),
|
|
581
|
+
join(
|
|
582
|
+
concat(vec![text(","), line()]),
|
|
583
|
+
vec![text("1"), text("2"), text("3")],
|
|
584
|
+
),
|
|
585
|
+
softline(),
|
|
586
|
+
text("]"),
|
|
587
|
+
]));
|
|
588
|
+
let result = print_doc(&doc);
|
|
589
|
+
assert_eq!(result, "[1, 2, 3]\n");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn test_print_literal_line() {
|
|
594
|
+
// Literal line should not add indentation
|
|
595
|
+
let doc = concat(vec![
|
|
596
|
+
text("<<~HEREDOC"),
|
|
597
|
+
indent(concat(vec![literalline(), text("content"), literalline()])),
|
|
598
|
+
text("HEREDOC"),
|
|
599
|
+
]);
|
|
600
|
+
let result = print_doc(&doc);
|
|
601
|
+
// literalline should not add indent despite being inside indent()
|
|
602
|
+
assert!(result.contains("\ncontent\n"));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Performance regression tests
|
|
606
|
+
// Run with: cargo test --release perf_
|
|
607
|
+
|
|
608
|
+
#[test]
|
|
609
|
+
fn perf_deep_nesting() {
|
|
610
|
+
let config = Config::default();
|
|
611
|
+
|
|
612
|
+
// Create 20-level deep nesting
|
|
613
|
+
let mut doc = text("deepest");
|
|
614
|
+
for _ in 0..20 {
|
|
615
|
+
doc = indent(concat(vec![hardline(), doc]));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let mut printer = Printer::new(&config);
|
|
619
|
+
let result = printer.print(&doc);
|
|
620
|
+
|
|
621
|
+
// Verify it produces valid output
|
|
622
|
+
assert!(result.lines().count() >= 20);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn perf_many_hardlines() {
|
|
627
|
+
let config = Config::default();
|
|
628
|
+
|
|
629
|
+
// Create 500 lines
|
|
630
|
+
let mut lines: Vec<Doc> = Vec::new();
|
|
631
|
+
for i in 0..500 {
|
|
632
|
+
if i > 0 {
|
|
633
|
+
lines.push(hardline());
|
|
634
|
+
}
|
|
635
|
+
lines.push(text("line"));
|
|
636
|
+
}
|
|
637
|
+
let doc = indent(concat(lines));
|
|
638
|
+
|
|
639
|
+
let mut printer = Printer::new(&config);
|
|
640
|
+
let result = printer.print(&doc);
|
|
641
|
+
|
|
642
|
+
assert_eq!(result.lines().count(), 500);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
#[test]
|
|
646
|
+
fn perf_nested_groups() {
|
|
647
|
+
let config = Config::default();
|
|
648
|
+
|
|
649
|
+
// Create nested group structure
|
|
650
|
+
let inner = group(concat(vec![
|
|
651
|
+
text("["),
|
|
652
|
+
softline(),
|
|
653
|
+
join(
|
|
654
|
+
concat(vec![text(","), line()]),
|
|
655
|
+
vec![text("1"), text("2"), text("3")],
|
|
656
|
+
),
|
|
657
|
+
softline(),
|
|
658
|
+
text("]"),
|
|
659
|
+
]));
|
|
660
|
+
|
|
661
|
+
let doc = group(concat(vec![
|
|
662
|
+
text("{"),
|
|
663
|
+
softline(),
|
|
664
|
+
text("a: "),
|
|
665
|
+
inner.clone(),
|
|
666
|
+
text(","),
|
|
667
|
+
line(),
|
|
668
|
+
text("b: "),
|
|
669
|
+
inner.clone(),
|
|
670
|
+
text(","),
|
|
671
|
+
line(),
|
|
672
|
+
text("c: "),
|
|
673
|
+
inner,
|
|
674
|
+
softline(),
|
|
675
|
+
text("}"),
|
|
676
|
+
]));
|
|
677
|
+
|
|
678
|
+
let mut printer = Printer::new(&config);
|
|
679
|
+
let result = printer.print(&doc);
|
|
680
|
+
|
|
681
|
+
// Should produce valid output
|
|
682
|
+
assert!(result.contains("[1, 2, 3]") || result.contains("[\n"));
|
|
683
|
+
}
|
|
684
|
+
}
|