gruel_fmt/printer.rs
1//! Output buffer + trivia weaving for the formatter (ADR-0093).
2//!
3//! Owns the indent state, resolves interned identifiers, and (Phase 4) weaves
4//! `//` line comments and blank lines back into the output by consulting a
5//! [`TriviaTable`] built over the raw source.
6
7use lasso::{Spur, ThreadedRodeo};
8
9use crate::trivia::{LineIndex, TriviaKind, TriviaTable};
10
11/// Width of one indent level, in spaces.
12pub const INDENT_WIDTH: usize = 4;
13
14/// Output buffer driven by the emit functions in [`crate::emit`].
15pub struct Printer<'a> {
16 out: String,
17 interner: &'a ThreadedRodeo,
18 indent_level: usize,
19 /// True iff the next `write_str` call should be preceded by indent
20 /// whitespace (i.e. the cursor is at column 0 of a new line).
21 pending_indent: bool,
22
23 // Trivia weaving (Phase 4).
24 src: &'a str,
25 trivia: TriviaTable,
26 /// Index into `trivia.entries`: the next entry not yet emitted.
27 trivia_cursor: usize,
28 line_index: LineIndex,
29 /// Source byte offset of the end of the last emitted span, or 0 if
30 /// nothing has been emitted yet. Used to detect trailing same-line
31 /// comments.
32 last_emitted_end: u32,
33}
34
35impl<'a> Printer<'a> {
36 pub fn new(interner: &'a ThreadedRodeo, src: &'a str) -> Self {
37 Self {
38 out: String::new(),
39 interner,
40 indent_level: 0,
41 pending_indent: false,
42 src,
43 trivia: TriviaTable::scan(src),
44 trivia_cursor: 0,
45 line_index: LineIndex::new(src),
46 last_emitted_end: 0,
47 }
48 }
49
50 /// Write `s` at the current cursor; emits pending indent first.
51 pub fn write_str(&mut self, s: &str) {
52 if s.is_empty() {
53 return;
54 }
55 self.flush_indent();
56 self.out.push_str(s);
57 }
58
59 fn flush_indent(&mut self) {
60 if self.pending_indent {
61 for _ in 0..(self.indent_level * INDENT_WIDTH) {
62 self.out.push(' ');
63 }
64 self.pending_indent = false;
65 }
66 }
67
68 /// Resolve and emit an interned identifier.
69 pub fn write_ident(&mut self, spur: Spur) {
70 let s = self.interner.resolve(&spur);
71 self.write_str(s);
72 }
73
74 /// Resolve an interned string without emitting it; callers re-escape
75 /// before writing (e.g. string literals).
76 pub fn resolve(&self, spur: Spur) -> &str {
77 self.interner.resolve(&spur)
78 }
79
80 /// Begin a new line. Subsequent writes will be preceded by indent
81 /// whitespace.
82 pub fn newline(&mut self) {
83 self.out.push('\n');
84 self.pending_indent = true;
85 }
86
87 /// Emit a blank line between sibling items. Idempotent — repeated calls
88 /// without intervening writes produce at most one blank line, matching the
89 /// "at most one consecutive blank line" rule. Also no-ops when the cursor
90 /// is directly inside a freshly-opened brace (style rule: "No blank line
91 /// at the start of a block").
92 pub fn blank_line(&mut self) {
93 if self.out.is_empty() || self.out.ends_with("\n\n") || self.out.ends_with("{\n") {
94 self.pending_indent = true;
95 return;
96 }
97 if !self.out.ends_with('\n') {
98 self.out.push('\n');
99 }
100 self.out.push('\n');
101 self.pending_indent = true;
102 }
103
104 pub fn indent(&mut self) {
105 self.indent_level += 1;
106 }
107
108 pub fn dedent(&mut self) {
109 debug_assert!(self.indent_level > 0, "dedent below zero");
110 self.indent_level -= 1;
111 }
112
113 /// Drain every trivia entry whose `start` is strictly before `offset` —
114 /// these are the comments and blank-line runs that fall *before* the
115 /// next AST node.
116 pub fn drain_trivia_before(&mut self, offset: u32) {
117 while self.trivia_cursor < self.trivia.entries.len() {
118 let entry = self.trivia.entries[self.trivia_cursor];
119 if entry.start >= offset {
120 break;
121 }
122 self.trivia_cursor += 1;
123 self.emit_trivia(entry);
124 }
125 }
126
127 /// Drain every remaining trivia entry — used once at end-of-AST to flush
128 /// any trailing comments past the last item.
129 pub fn drain_trivia_remaining(&mut self) {
130 while self.trivia_cursor < self.trivia.entries.len() {
131 let entry = self.trivia.entries[self.trivia_cursor];
132 self.trivia_cursor += 1;
133 self.emit_trivia(entry);
134 }
135 }
136
137 fn emit_trivia(&mut self, entry: crate::trivia::TriviaEntry) {
138 match entry.kind {
139 TriviaKind::Blank => self.blank_line(),
140 TriviaKind::Comment => {
141 let text = &self.src[entry.start as usize..entry.end as usize];
142 self.write_str(text);
143 self.newline();
144 }
145 }
146 }
147
148 /// Drain `// comment` trivia that begins on the same source line as the
149 /// most recently emitted node. The comment is appended inline with two
150 /// leading spaces; the caller emits the terminating newline.
151 ///
152 /// Call this immediately *after* `mark_emitted_end` and *before* the
153 /// `newline()` that ends the line — that ordering keeps the comment glued
154 /// to the statement/item it follows in source.
155 pub fn drain_trailing_comment_on_line(&mut self) {
156 if self.last_emitted_end == 0 {
157 return;
158 }
159 let prev_line = self
160 .line_index
161 .line_of(self.last_emitted_end.saturating_sub(1));
162 while self.trivia_cursor < self.trivia.entries.len() {
163 let entry = self.trivia.entries[self.trivia_cursor];
164 if entry.kind != TriviaKind::Comment {
165 break;
166 }
167 let line = self.line_index.line_of(entry.start);
168 if line != prev_line {
169 break;
170 }
171 self.trivia_cursor += 1;
172 let text = &self.src[entry.start as usize..entry.end as usize];
173 // Two spaces, then the comment. Don't call `write_str` because
174 // pending_indent is irrelevant here — we're chaining onto the
175 // already-emitted line.
176 self.out.push_str(" ");
177 self.out.push_str(text);
178 }
179 }
180
181 /// Record that the AST node ending at `byte` has just been emitted. Used
182 /// to decide whether a following comment is a trailing same-line one.
183 pub fn mark_emitted_end(&mut self, byte: u32) {
184 self.last_emitted_end = byte;
185 }
186
187 /// Consume the printer and return the formatted source. Guarantees exactly
188 /// one trailing newline at EOF.
189 pub fn finish(mut self) -> String {
190 while self.out.ends_with("\n\n") {
191 self.out.pop();
192 }
193 if !self.out.ends_with('\n') {
194 self.out.push('\n');
195 }
196 self.out
197 }
198}