Skip to main content

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}