1use std::borrow::Cow;
18
19use unicode_width::UnicodeWidthStr;
20
21#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum Doc {
27 Text(Cow<'static, str>, usize),
31 SourceCodeSlice(Cow<'static, str>, usize),
39 Line(LineKind),
41 Concat(Vec<Doc>),
43 BestFitting(Vec<Doc>),
47 Group {
57 content: Box<Doc>,
58 expand: bool,
59 must_break: bool,
60 },
61 IfBreak { broken: Box<Doc>, flat: Box<Doc> },
67 Indent(Box<Doc>),
69 LineSuffix(Box<Doc>),
72 BreakParent,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum LineKind {
80 Space,
82 Soft,
84 Hard,
86}
87
88pub fn text(s: impl Into<Cow<'static, str>>) -> Doc {
93 let s = s.into();
94 let width = text_width(&s);
95 Doc::Text(s, width)
96}
97
98pub fn source_code_slice(s: impl Into<Cow<'static, str>>) -> Doc {
102 let s = s.into();
103 let width = last_line_width(&s);
104 Doc::SourceCodeSlice(s, width)
105}
106
107pub fn concat(parts: Vec<Doc>) -> Doc {
109 Doc::Concat(parts)
110}
111
112pub fn group(inner: Doc) -> Doc {
114 let must_break = has_forced_break(&inner);
115 Doc::Group {
116 content: Box::new(inner),
117 expand: false,
118 must_break,
119 }
120}
121
122pub fn group_expanded(inner: Doc) -> Doc {
125 Doc::Group {
126 content: Box::new(inner),
127 expand: true,
128 must_break: true,
129 }
130}
131
132pub fn indent(inner: Doc) -> Doc {
134 Doc::Indent(Box::new(inner))
135}
136
137#[doc(hidden)]
140pub fn block_indent(inner: Doc) -> Doc {
141 concat(vec![indent(concat(vec![hard_line(), inner])), hard_line()])
142}
143
144#[doc(hidden)]
147pub fn if_group_breaks(broken: Doc, flat: Doc) -> Doc {
148 Doc::IfBreak {
149 broken: Box::new(broken),
150 flat: Box::new(flat),
151 }
152}
153
154pub fn line() -> Doc {
156 Doc::Line(LineKind::Space)
157}
158
159pub fn soft_line() -> Doc {
161 Doc::Line(LineKind::Soft)
162}
163
164pub fn hard_line() -> Doc {
166 Doc::Line(LineKind::Hard)
167}
168
169pub fn space() -> Doc {
171 Doc::Text(Cow::Borrowed(" "), 1)
172}
173
174pub fn empty() -> Doc {
176 Doc::Concat(Vec::new())
177}
178
179pub fn line_suffix(inner: Doc) -> Doc {
181 Doc::LineSuffix(Box::new(inner))
182}
183
184pub fn break_parent() -> Doc {
186 Doc::BreakParent
187}
188
189pub fn join(sep: Doc, items: Vec<Doc>) -> Doc {
191 let mut parts = Vec::with_capacity(items.len().saturating_mul(2));
192 for (i, item) in items.into_iter().enumerate() {
193 if i > 0 {
194 parts.push(sep.clone());
195 }
196 parts.push(item);
197 }
198 Doc::Concat(parts)
199}
200
201#[doc(hidden)]
204pub fn best_fitting(candidates: Vec<Doc>) -> Doc {
205 Doc::BestFitting(candidates)
206}
207
208#[doc(hidden)]
213#[derive(Default)]
214pub struct DocBuffer {
215 parts: Vec<Doc>,
216}
217
218impl DocBuffer {
219 pub fn new() -> Self {
221 DocBuffer { parts: Vec::new() }
222 }
223
224 pub fn write_element(&mut self, doc: Doc) {
226 self.parts.push(doc);
227 }
228
229 pub fn write_fmt_value<T: Format + ?Sized>(&mut self, value: &T) {
231 value.fmt(self);
232 }
233
234 pub fn write_with_rule<T: ?Sized, R: FormatRule<T>>(&mut self, rule: &R, item: &T) {
236 rule.fmt(item, self);
237 }
238
239 pub fn finish(mut self) -> Doc {
241 if self.parts.len() == 1 {
242 self.parts.pop().expect("len checked == 1")
243 } else {
244 Doc::Concat(self.parts)
245 }
246 }
247}
248
249#[doc(hidden)]
251pub trait Format {
252 fn fmt(&self, buffer: &mut DocBuffer);
254}
255
256#[doc(hidden)]
258pub trait FormatRule<T: ?Sized> {
259 fn fmt(&self, item: &T, buffer: &mut DocBuffer);
261}
262
263impl Format for Doc {
264 fn fmt(&self, buffer: &mut DocBuffer) {
265 buffer.write_element(self.clone());
266 }
267}
268
269impl<T: Format + ?Sized> Format for &T {
270 fn fmt(&self, buffer: &mut DocBuffer) {
271 (**self).fmt(buffer);
272 }
273}
274
275#[doc(hidden)]
277pub fn format_value<T: Format + ?Sized>(value: &T) -> Doc {
278 let mut buffer = DocBuffer::new();
279 buffer.write_fmt_value(value);
280 buffer.finish()
281}
282
283#[doc(hidden)]
285#[macro_export]
286macro_rules! doc_write {
287 ($buffer:expr, [ $( $element:expr ),* $(,)? ]) => {{
288 $( $buffer.write_fmt_value(&$element); )*
289 }};
290}
291
292#[doc(inline)]
293pub use crate::doc_write;
294
295#[derive(Clone, Copy, Debug)]
299pub struct PrintOptions {
300 pub line_width: usize,
302 pub indent_width: usize,
304}
305
306impl Default for PrintOptions {
307 fn default() -> Self {
308 PrintOptions {
309 line_width: 100,
310 indent_width: 4,
311 }
312 }
313}
314
315#[derive(Clone, Copy, PartialEq, Eq)]
316enum Mode {
317 Flat,
318 Break,
319}
320
321#[derive(Clone, Copy)]
322struct Cmd<'a> {
323 indent: usize,
324 mode: Mode,
325 doc: &'a Doc,
326}
327
328fn text_width(s: &str) -> usize {
335 if s.is_ascii() {
336 return s.len();
337 }
338 UnicodeWidthStr::width(s)
339}
340
341fn last_line_width(s: &str) -> usize {
344 match s.rfind('\n') {
345 Some(nl) => text_width(&s[nl + 1..]),
346 None => text_width(s),
347 }
348}
349
350fn has_forced_break(doc: &Doc) -> bool {
357 match doc {
358 Doc::Line(LineKind::Hard) | Doc::BreakParent => true,
359 Doc::Line(_) | Doc::Text(..) | Doc::SourceCodeSlice(..) => false,
360 Doc::LineSuffix(_) => false,
362 Doc::IfBreak { flat, .. } => has_forced_break(flat),
365 Doc::Concat(parts) => parts.iter().any(has_forced_break),
366 Doc::BestFitting(candidates) => {
367 !candidates.is_empty() && candidates.iter().all(has_forced_break)
368 }
369 Doc::Indent(inner) => has_forced_break(inner),
370 Doc::Group { must_break, .. } => *must_break,
374 }
375}
376
377fn fits(mut remaining: isize, rest: &[Cmd], next: Cmd, opts: &PrintOptions) -> bool {
381 if remaining < 0 {
382 return false;
383 }
384 let mut stack: Vec<Cmd> = vec![next];
385 let mut rest_idx = rest.len();
386 loop {
387 let cmd = match stack.pop() {
388 Some(cmd) => cmd,
389 None => {
390 if rest_idx == 0 {
391 return true;
392 }
393 rest_idx -= 1;
394 rest[rest_idx]
395 }
396 };
397 match cmd.doc {
398 Doc::Text(_, width) => {
399 remaining -= *width as isize;
400 if remaining < 0 {
401 return false;
402 }
403 }
404 Doc::SourceCodeSlice(s, _) => {
405 let mut pieces = s.split('\n');
406 let first = pieces.next().unwrap_or_default();
407 remaining -= text_width(first) as isize;
408 if remaining < 0 {
409 return false;
410 }
411 for piece in pieces {
412 remaining =
413 opts.line_width as isize - cmd.indent as isize - text_width(piece) as isize;
414 if remaining < 0 {
415 return false;
416 }
417 }
418 }
419 Doc::IfBreak { broken, flat } => {
420 let chosen = if cmd.mode == Mode::Break {
421 broken
422 } else {
423 flat
424 };
425 stack.push(Cmd { doc: chosen, ..cmd });
426 }
427 Doc::Concat(parts) => {
428 for part in parts.iter().rev() {
429 stack.push(Cmd { doc: part, ..cmd });
430 }
431 }
432 Doc::BestFitting(candidates) => {
433 if let Some(candidate) = candidates.first() {
434 stack.push(Cmd {
435 doc: candidate,
436 ..cmd
437 });
438 }
439 }
440 Doc::Indent(inner) => stack.push(Cmd {
441 indent: cmd.indent + opts.indent_width,
442 doc: inner,
443 mode: cmd.mode,
444 }),
445 Doc::Group {
446 content,
447 must_break,
448 ..
449 } => {
450 let mode = if *must_break { Mode::Break } else { Mode::Flat };
451 stack.push(Cmd {
452 indent: cmd.indent,
453 mode,
454 doc: content,
455 });
456 }
457 Doc::LineSuffix(_) | Doc::BreakParent => {}
460 Doc::Line(kind) => match cmd.mode {
461 Mode::Break => return true,
463 Mode::Flat => match kind {
464 LineKind::Hard => return true,
465 LineKind::Soft => {}
466 LineKind::Space => {
467 remaining -= 1;
468 if remaining < 0 {
469 return false;
470 }
471 }
472 },
473 },
474 }
475 }
476}
477
478pub fn print(doc: &Doc, opts: &PrintOptions) -> String {
481 let mut out = String::new();
482 let mut col = 0usize;
483 let mut cmds: Vec<Cmd> = vec![Cmd {
484 indent: 0,
485 mode: Mode::Break,
486 doc,
487 }];
488 let mut line_suffixes: Vec<Cmd> = Vec::new();
490
491 loop {
492 let cmd = match cmds.pop() {
493 Some(cmd) => cmd,
494 None if !line_suffixes.is_empty() => {
495 while let Some(suffix) = line_suffixes.pop() {
497 cmds.push(suffix);
498 }
499 continue;
500 }
501 None => break,
502 };
503 match cmd.doc {
504 Doc::Text(s, width) => {
505 out.push_str(s);
506 col += *width;
507 }
508 Doc::SourceCodeSlice(s, width) => {
509 let mut first = true;
510 for piece in s.split('\n') {
511 if !first {
512 out.push('\n');
513 for _ in 0..cmd.indent {
514 out.push(' ');
515 }
516 }
517 out.push_str(piece);
518 first = false;
519 }
520 col = if s.contains('\n') {
521 cmd.indent + *width
522 } else {
523 col + *width
524 };
525 }
526 Doc::IfBreak { broken, flat } => {
527 let chosen = if cmd.mode == Mode::Break {
528 broken
529 } else {
530 flat
531 };
532 cmds.push(Cmd { doc: chosen, ..cmd });
533 }
534 Doc::Concat(parts) => {
535 for part in parts.iter().rev() {
536 cmds.push(Cmd { doc: part, ..cmd });
537 }
538 }
539 Doc::BestFitting(candidates) => {
540 if candidates.is_empty() {
541 continue;
542 }
543 let mut chosen = candidates.last().expect("non-empty candidates");
544 for candidate in candidates {
545 if fits(
546 opts.line_width as isize - col as isize,
547 &cmds,
548 Cmd {
549 indent: cmd.indent,
550 mode: cmd.mode,
551 doc: candidate,
552 },
553 opts,
554 ) {
555 chosen = candidate;
556 break;
557 }
558 }
559 cmds.push(Cmd { doc: chosen, ..cmd });
560 }
561 Doc::LineSuffix(inner) => line_suffixes.push(Cmd { doc: inner, ..cmd }),
562 Doc::BreakParent => {}
563 Doc::Indent(inner) => cmds.push(Cmd {
564 indent: cmd.indent + opts.indent_width,
565 doc: inner,
566 mode: cmd.mode,
567 }),
568 Doc::Group {
569 content,
570 must_break,
571 ..
572 } => {
573 let mode = if *must_break {
574 Mode::Break
575 } else if fits(
576 opts.line_width as isize - col as isize,
577 &cmds,
578 Cmd {
579 indent: cmd.indent,
580 mode: Mode::Flat,
581 doc: content,
582 },
583 opts,
584 ) {
585 Mode::Flat
586 } else {
587 Mode::Break
588 };
589 cmds.push(Cmd {
590 indent: cmd.indent,
591 mode,
592 doc: content,
593 });
594 }
595 Doc::Line(kind) => {
596 let newline = match cmd.mode {
597 Mode::Break => true,
598 Mode::Flat => match kind {
599 LineKind::Hard => true,
600 LineKind::Space => {
601 out.push(' ');
602 col += 1;
603 false
604 }
605 LineKind::Soft => false,
606 },
607 };
608 if newline {
609 if !line_suffixes.is_empty() {
612 cmds.push(cmd);
613 while let Some(suffix) = line_suffixes.pop() {
614 cmds.push(suffix);
615 }
616 continue;
617 }
618 out.push('\n');
619 for _ in 0..cmd.indent {
620 out.push(' ');
621 }
622 col = cmd.indent;
623 }
624 }
625 }
626 }
627
628 finalize(out)
629}
630
631fn finalize(raw: String) -> String {
639 let mut out = String::with_capacity(raw.len());
640 let mut started = false;
641 let mut pending_blanks = 0usize;
642 for line in raw.lines() {
643 let line = line.trim_end();
644 if line.is_empty() {
645 if started {
646 pending_blanks += 1;
647 }
648 continue;
650 }
651 if started {
652 out.push('\n');
654 for _ in 0..pending_blanks {
655 out.push('\n');
656 }
657 }
658 pending_blanks = 0;
659 out.push_str(line);
660 started = true;
661 }
662 if started {
663 out.push('\n');
664 }
665 out
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 fn p(doc: &Doc, width: usize) -> String {
673 print(
674 doc,
675 &PrintOptions {
676 line_width: width,
677 indent_width: 4,
678 },
679 )
680 }
681
682 #[test]
683 fn text_and_concat() {
684 let doc = concat(vec![text("a"), space(), text("b")]);
685 assert_eq!(p(&doc, 80), "a b\n");
686 }
687
688 #[test]
689 fn cjk_text_is_measured_double_width() {
690 assert_eq!(text_width("abc"), 3);
691 assert_eq!(text_width("長芋"), 4); assert_eq!(text_width("a長"), 3); let doc = group(concat(vec![text("長"), line(), text("芋")]));
695 assert_eq!(p(&doc, 4), "長\n芋\n");
696 assert_eq!(p(&doc, 5), "長 芋\n");
697 }
698
699 #[test]
700 fn non_ascii_width_uses_unicode_sequence_width() {
701 assert_eq!(text_width("e\u{301}"), 1); assert_eq!(text_width("👩\u{200d}💻"), 2); let doc = group(concat(vec![text("e\u{301}"), line(), text("👩\u{200d}💻")]));
706 assert_eq!(p(&doc, 3), "e\u{301}\n👩\u{200d}💻\n");
707 assert_eq!(p(&doc, 4), "e\u{301} 👩\u{200d}💻\n");
708 }
709
710 #[test]
711 fn group_stays_flat_when_it_fits() {
712 let doc = group(concat(vec![text("a"), line(), text("b")]));
713 assert_eq!(p(&doc, 80), "a b\n");
714 }
715
716 #[test]
717 fn group_breaks_when_too_wide() {
718 let doc = group(concat(vec![
719 text("aaaa"),
720 indent(concat(vec![line(), text("bbbb")])),
721 ]));
722 assert_eq!(p(&doc, 5), "aaaa\n bbbb\n");
723 }
724
725 #[test]
726 fn hard_line_forces_enclosing_group_to_break() {
727 let doc = group(concat(vec![text("a"), hard_line(), text("b")]));
728 assert_eq!(p(&doc, 80), "a\nb\n");
729 }
730
731 #[test]
732 fn soft_line_is_nothing_when_flat() {
733 let doc = group(concat(vec![text("a"), soft_line(), text("b")]));
734 assert_eq!(p(&doc, 80), "ab\n");
735 }
736
737 #[test]
738 fn join_interleaves_separator() {
739 let doc = join(text(", "), vec![text("a"), text("b"), text("c")]);
740 assert_eq!(p(&doc, 80), "a, b, c\n");
741 }
742
743 #[test]
744 fn best_fitting_picks_first_candidate_that_fits() {
745 let doc = best_fitting(vec![
746 text("short"),
747 concat(vec![text("fallback"), hard_line(), text("layout")]),
748 ]);
749 assert_eq!(p(&doc, 10), "short\n");
750 }
751
752 #[test]
753 fn best_fitting_falls_back_to_last_candidate() {
754 let doc = best_fitting(vec![
755 text("too-wide"),
756 concat(vec![text("fallback"), hard_line(), text("layout")]),
757 ]);
758 assert_eq!(p(&doc, 4), "fallback\nlayout\n");
759 }
760
761 #[test]
762 fn nested_indent_accumulates() {
763 let doc = concat(vec![
764 text("a"),
765 indent(concat(vec![
766 hard_line(),
767 text("b"),
768 indent(concat(vec![hard_line(), text("c")])),
769 ])),
770 ]);
771 assert_eq!(p(&doc, 80), "a\n b\n c\n");
772 }
773
774 #[test]
775 fn expanded_group_breaks_even_when_it_fits() {
776 let doc = group_expanded(concat(vec![
777 text("("),
778 indent(concat(vec![soft_line(), text("a")])),
779 soft_line(),
780 text(")"),
781 ]));
782 assert_eq!(p(&doc, 80), "(\n a\n)\n");
783 }
784
785 #[test]
786 fn expanded_inner_group_forces_outer_to_break() {
787 let inner = group_expanded(concat(vec![
790 text("("),
791 indent(concat(vec![soft_line(), text("x")])),
792 soft_line(),
793 text(")"),
794 ]));
795 let outer = group(concat(vec![text("a"), line(), inner]));
796 assert_eq!(p(&outer, 80), "a\n(\n x\n)\n");
797 }
798
799 #[test]
800 fn line_suffix_defers_content_to_the_end_of_the_line() {
801 let doc = concat(vec![
803 line_suffix(text(" -- note")),
804 text("code"),
805 hard_line(),
806 text("next"),
807 ]);
808 assert_eq!(p(&doc, 80), "code -- note\nnext\n");
809 }
810
811 #[test]
812 fn line_suffix_flushes_at_end_of_document() {
813 let doc = concat(vec![line_suffix(text(" -- note")), text("code")]);
814 assert_eq!(p(&doc, 80), "code -- note\n");
815 }
816
817 #[test]
818 fn break_parent_forces_its_group_to_break() {
819 let doc = group(concat(vec![
820 text("a"),
821 break_parent(),
822 indent(concat(vec![line(), text("b")])),
823 ]));
824 assert_eq!(p(&doc, 80), "a\n b\n");
825 }
826
827 #[test]
828 fn empty_document_prints_nothing() {
829 assert_eq!(p(&empty(), 80), "");
830 }
831
832 #[test]
833 fn trailing_whitespace_is_trimmed() {
834 let doc = concat(vec![text("a"), indent(hard_line()), hard_line(), text("b")]);
837 assert_eq!(p(&doc, 80), "a\n\nb\n");
838 }
839
840 #[test]
841 fn text_caches_its_display_width() {
842 for s in ["select", "a, b, c", "長芋", "a長b", ""] {
845 match text(s) {
846 Doc::Text(stored, width) => {
847 assert_eq!(stored, s);
848 assert_eq!(width, text_width(s));
849 }
850 other => panic!("text() should build a Text node, got {other:?}"),
851 }
852 }
853 assert_eq!(
855 text_width("SELECT a, b"),
856 UnicodeWidthStr::width("SELECT a, b")
857 );
858 }
859
860 #[test]
861 fn group_precomputes_its_forced_break() {
862 match group(concat(vec![text("a"), line(), text("b")])) {
865 Doc::Group { must_break, .. } => assert!(!must_break),
866 other => panic!("expected a group, got {other:?}"),
867 }
868 match group(concat(vec![text("a"), hard_line(), text("b")])) {
869 Doc::Group { must_break, .. } => assert!(must_break),
870 other => panic!("expected a group, got {other:?}"),
871 }
872 match group_expanded(text("x")) {
874 Doc::Group { must_break, .. } => assert!(must_break),
875 other => panic!("expected a group, got {other:?}"),
876 }
877 }
878
879 #[test]
880 fn finalize_drops_leading_and_trailing_blank_lines_but_keeps_interior() {
881 let raw = String::from("\n \na \n\n\nb \n\n \n");
884 assert_eq!(finalize(raw), "a\n\n\nb\n");
885 assert_eq!(finalize(String::new()), "");
886 assert_eq!(finalize(String::from(" \n ")), "");
887 assert_eq!(finalize(String::from("only")), "only\n");
888 }
889
890 #[test]
891 fn source_code_slice_single_line_behaves_like_text() {
892 let doc = concat(vec![text("a "), source_code_slice("b = c"), text(" d")]);
893 assert_eq!(p(&doc, 80), "a b = c d\n");
894 }
895
896 #[test]
897 fn source_code_slice_caches_its_last_line_width() {
898 match source_code_slice("alpha\nbeta\nlong-tail") {
899 Doc::SourceCodeSlice(s, width) => {
900 assert_eq!(s, "alpha\nbeta\nlong-tail");
901 assert_eq!(width, "long-tail".len());
902 }
903 other => panic!("expected a source slice, got {other:?}"),
904 }
905 match source_code_slice("x\n") {
906 Doc::SourceCodeSlice(_, width) => assert_eq!(width, 0),
907 other => panic!("expected a source slice, got {other:?}"),
908 }
909 }
910
911 #[test]
912 fn source_code_slice_reindents_continuation_lines_under_indent() {
913 let doc = concat(vec![
914 text("header"),
915 indent(concat(vec![hard_line(), source_code_slice("v1\nv2\nv3")])),
916 hard_line(),
917 text("footer"),
918 ]);
919 assert_eq!(p(&doc, 80), "header\n v1\n v2\n v3\nfooter\n");
920 }
921
922 #[test]
923 fn source_code_slice_tail_width_participates_in_fits() {
924 let doc = group(concat(vec![
925 source_code_slice("aa\nbbbb"),
926 line(),
927 text("c"),
928 ]));
929 assert_eq!(p(&doc, 6), "aa\nbbbb c\n");
930 assert_eq!(p(&doc, 5), "aa\nbbbb\nc\n");
931 }
932
933 #[test]
934 fn if_group_breaks_selects_layout_by_group_mode() {
935 let doc = group(concat(vec![
936 text("a"),
937 if_group_breaks(text(","), empty()),
938 line(),
939 text("b"),
940 ]));
941 assert_eq!(p(&doc, 80), "a b\n");
942 assert_eq!(p(&doc, 1), "a,\nb\n");
943 }
944
945 #[test]
946 fn if_group_breaks_flat_arm_forced_break_propagates_but_broken_arm_does_not() {
947 assert!(has_forced_break(&if_group_breaks(empty(), hard_line())));
948 assert!(!has_forced_break(&if_group_breaks(hard_line(), empty())));
949 }
950
951 #[test]
952 fn block_indent_frames_content_on_its_own_indented_line() {
953 let doc = concat(vec![text("{"), block_indent(text("body")), text("}")]);
954 assert_eq!(p(&doc, 80), "{\n body\n}\n");
955 }
956
957 #[test]
958 fn doc_write_composes_format_values_in_order() {
959 let mut f = DocBuffer::new();
960 doc_write!(f, [text("a"), text(" = "), text("b")]);
961 assert_eq!(p(&group(f.finish()), 80), "a = b\n");
962 }
963
964 #[test]
965 fn format_rule_decouples_layout_from_data() {
966 struct AssignRule;
967 impl FormatRule<str> for AssignRule {
968 fn fmt(&self, item: &str, buffer: &mut DocBuffer) {
969 doc_write!(
970 buffer,
971 [text("x => \""), text(item.to_string()), text("\"")]
972 );
973 }
974 }
975
976 let mut f = DocBuffer::new();
977 f.write_with_rule(&AssignRule, "v");
978 assert_eq!(p(&f.finish(), 80), "x => \"v\"\n");
979 }
980}