forge_fmt/
buffer.rs

1//! Format buffer.
2
3use crate::{
4    comments::{CommentState, CommentStringExt},
5    string::{QuoteState, QuotedStringExt},
6};
7use std::fmt::Write;
8
9/// An indent group. The group may optionally skip the first line
10#[derive(Clone, Debug, Default)]
11struct IndentGroup {
12    skip_line: bool,
13}
14
15#[derive(Clone, Copy, Debug)]
16enum WriteState {
17    LineStart(CommentState),
18    WriteTokens(CommentState),
19    WriteString(char),
20}
21
22impl WriteState {
23    fn comment_state(&self) -> CommentState {
24        match self {
25            Self::LineStart(state) => *state,
26            Self::WriteTokens(state) => *state,
27            Self::WriteString(_) => CommentState::None,
28        }
29    }
30}
31
32impl Default for WriteState {
33    fn default() -> Self {
34        Self::LineStart(CommentState::default())
35    }
36}
37
38/// A wrapper around a `std::fmt::Write` interface. The wrapper keeps track of indentation as well
39/// as information about the last `write_str` command if available. The formatter may also be
40/// restricted to a single line, in which case it will throw an error on a newline
41#[derive(Clone, Debug)]
42pub struct FormatBuffer<W> {
43    pub w: W,
44    indents: Vec<IndentGroup>,
45    base_indent_len: usize,
46    tab_width: usize,
47    last_char: Option<char>,
48    current_line_len: usize,
49    restrict_to_single_line: bool,
50    state: WriteState,
51}
52
53impl<W> FormatBuffer<W> {
54    pub fn new(w: W, tab_width: usize) -> Self {
55        Self {
56            w,
57            tab_width,
58            base_indent_len: 0,
59            indents: vec![],
60            current_line_len: 0,
61            last_char: None,
62            restrict_to_single_line: false,
63            state: WriteState::default(),
64        }
65    }
66
67    /// Create a new temporary buffer based on an existing buffer which retains information about
68    /// the buffer state, but has a blank String as its underlying `Write` interface
69    pub fn create_temp_buf(&self) -> FormatBuffer<String> {
70        let mut new = FormatBuffer::new(String::new(), self.tab_width);
71        new.base_indent_len = self.total_indent_len();
72        new.current_line_len = self.current_line_len();
73        new.last_char = self.last_char;
74        new.restrict_to_single_line = self.restrict_to_single_line;
75        new.state = match self.state {
76            WriteState::WriteTokens(state) | WriteState::LineStart(state) => {
77                WriteState::LineStart(state)
78            }
79            WriteState::WriteString(ch) => WriteState::WriteString(ch),
80        };
81        new
82    }
83
84    /// Restrict the buffer to a single line
85    pub fn restrict_to_single_line(&mut self, restricted: bool) {
86        self.restrict_to_single_line = restricted;
87    }
88
89    /// Indent the buffer by delta
90    pub fn indent(&mut self, delta: usize) {
91        self.indents.extend(std::iter::repeat_n(IndentGroup::default(), delta));
92    }
93
94    /// Dedent the buffer by delta
95    pub fn dedent(&mut self, delta: usize) {
96        self.indents.truncate(self.indents.len() - delta);
97    }
98
99    /// Get the current level of the indent. This is multiplied by the tab width to get the
100    /// resulting indent
101    fn level(&self) -> usize {
102        self.indents.iter().filter(|i| !i.skip_line).count()
103    }
104
105    /// Check if the last indent group is being skipped
106    pub fn last_indent_group_skipped(&self) -> bool {
107        self.indents.last().map(|i| i.skip_line).unwrap_or(false)
108    }
109
110    /// Set whether the last indent group should be skipped
111    pub fn set_last_indent_group_skipped(&mut self, skip_line: bool) {
112        if let Some(i) = self.indents.last_mut() {
113            i.skip_line = skip_line
114        }
115    }
116
117    /// Get the current indent size (level * tab_width)
118    pub fn current_indent_len(&self) -> usize {
119        self.level() * self.tab_width
120    }
121
122    /// Get the total indent size
123    pub fn total_indent_len(&self) -> usize {
124        self.current_indent_len() + self.base_indent_len
125    }
126
127    /// Get the current written position (this does not include the indent size)
128    pub fn current_line_len(&self) -> usize {
129        self.current_line_len
130    }
131
132    /// Check if the buffer is at the beginning of a new line
133    pub fn is_beginning_of_line(&self) -> bool {
134        matches!(self.state, WriteState::LineStart(_))
135    }
136
137    /// Start a new indent group (skips first indent)
138    pub fn start_group(&mut self) {
139        self.indents.push(IndentGroup { skip_line: true });
140    }
141
142    /// End the last indent group
143    pub fn end_group(&mut self) {
144        self.indents.pop();
145    }
146
147    /// Get the last char written to the buffer
148    pub fn last_char(&self) -> Option<char> {
149        self.last_char
150    }
151
152    /// When writing a newline apply state changes
153    fn handle_newline(&mut self, mut comment_state: CommentState) {
154        if comment_state == CommentState::Line {
155            comment_state = CommentState::None;
156        }
157        self.current_line_len = 0;
158        self.set_last_indent_group_skipped(false);
159        self.last_char = Some('\n');
160        self.state = WriteState::LineStart(comment_state);
161    }
162}
163
164impl<W: Write> FormatBuffer<W> {
165    /// Write a raw string to the buffer. This will ignore indents and remove the indents of the
166    /// written string to match the current base indent of this buffer if it is a temp buffer
167    pub fn write_raw(&mut self, s: impl AsRef<str>) -> std::fmt::Result {
168        self._write_raw(s.as_ref())
169    }
170
171    fn _write_raw(&mut self, s: &str) -> std::fmt::Result {
172        let mut lines = s.lines().peekable();
173        let mut comment_state = self.state.comment_state();
174        while let Some(line) = lines.next() {
175            // remove the whitespace that covered by the base indent length (this is normally the
176            // case with temporary buffers as this will be re-added by the underlying IndentWriter
177            // later on
178            let (new_comment_state, line_start) = line
179                .comment_state_char_indices()
180                .with_state(comment_state)
181                .take(self.base_indent_len)
182                .take_while(|(_, _, ch)| ch.is_whitespace())
183                .last()
184                .map(|(state, idx, _)| (state, idx + 1))
185                .unwrap_or((comment_state, 0));
186            comment_state = new_comment_state;
187            let trimmed_line = &line[line_start..];
188            if !trimmed_line.is_empty() {
189                self.w.write_str(trimmed_line)?;
190                self.current_line_len += trimmed_line.len();
191                self.last_char = trimmed_line.chars().next_back();
192                self.state = WriteState::WriteTokens(comment_state);
193            }
194            if lines.peek().is_some() || s.ends_with('\n') {
195                if self.restrict_to_single_line {
196                    return Err(std::fmt::Error)
197                }
198                self.w.write_char('\n')?;
199                self.handle_newline(comment_state);
200            }
201        }
202        Ok(())
203    }
204}
205
206impl<W: Write> Write for FormatBuffer<W> {
207    fn write_str(&mut self, mut s: &str) -> std::fmt::Result {
208        if s.is_empty() {
209            return Ok(())
210        }
211
212        let mut indent = " ".repeat(self.current_indent_len());
213
214        loop {
215            match self.state {
216                WriteState::LineStart(mut comment_state) => {
217                    match s.find(|b| b != '\n') {
218                        // No non-empty lines in input, write the entire string (only newlines)
219                        None => {
220                            if !s.is_empty() {
221                                self.w.write_str(s)?;
222                                self.handle_newline(comment_state);
223                            }
224                            break
225                        }
226
227                        // We can see the next non-empty line. Write up to the
228                        // beginning of that line, then insert an indent, then
229                        // continue.
230                        Some(len) => {
231                            let (head, tail) = s.split_at(len);
232                            self.w.write_str(head)?;
233                            self.w.write_str(&indent)?;
234                            self.current_line_len = 0;
235                            self.last_char = Some(' ');
236                            // a newline has been inserted
237                            if len > 0 {
238                                if self.last_indent_group_skipped() {
239                                    indent = " ".repeat(self.current_indent_len() + self.tab_width);
240                                    self.set_last_indent_group_skipped(false);
241                                }
242                                if comment_state == CommentState::Line {
243                                    comment_state = CommentState::None;
244                                }
245                            }
246                            s = tail;
247                            self.state = WriteState::WriteTokens(comment_state);
248                        }
249                    }
250                }
251                WriteState::WriteTokens(comment_state) => {
252                    if s.is_empty() {
253                        break
254                    }
255
256                    // find the next newline or non-comment string separator (e.g. ' or ")
257                    let mut len = 0;
258                    let mut new_state = WriteState::WriteTokens(comment_state);
259                    for (state, idx, ch) in s.comment_state_char_indices().with_state(comment_state)
260                    {
261                        len = idx;
262                        if ch == '\n' {
263                            if self.restrict_to_single_line {
264                                return Err(std::fmt::Error)
265                            }
266                            new_state = WriteState::LineStart(state);
267                            break
268                        } else if state == CommentState::None && (ch == '\'' || ch == '"') {
269                            new_state = WriteState::WriteString(ch);
270                            break
271                        } else {
272                            new_state = WriteState::WriteTokens(state);
273                        }
274                    }
275
276                    if matches!(new_state, WriteState::WriteTokens(_)) {
277                        // No newlines or strings found, write the entire string
278                        self.w.write_str(s)?;
279                        self.current_line_len += s.len();
280                        self.last_char = s.chars().next_back();
281                        self.state = new_state;
282                        break
283                    } else {
284                        // A newline or string has been found. Write up to that character and
285                        // continue on the tail
286                        let (head, tail) = s.split_at(len + 1);
287                        self.w.write_str(head)?;
288                        s = tail;
289                        match new_state {
290                            WriteState::LineStart(comment_state) => {
291                                self.handle_newline(comment_state)
292                            }
293                            new_state => {
294                                self.current_line_len += head.len();
295                                self.last_char = head.chars().next_back();
296                                self.state = new_state;
297                            }
298                        }
299                    }
300                }
301                WriteState::WriteString(quote) => {
302                    match s.quoted_ranges().with_state(QuoteState::String(quote)).next() {
303                        // No end found, write the rest of the string
304                        None => {
305                            self.w.write_str(s)?;
306                            self.current_line_len += s.len();
307                            self.last_char = s.chars().next_back();
308                            break
309                        }
310                        // String end found, write the string and continue to add tokens after
311                        Some((_, _, len)) => {
312                            let (head, tail) = s.split_at(len + 1);
313                            self.w.write_str(head)?;
314                            if let Some((_, last)) = head.rsplit_once('\n') {
315                                self.set_last_indent_group_skipped(false);
316                                self.current_line_len = last.len();
317                            } else {
318                                self.current_line_len += head.len();
319                            }
320                            self.last_char = Some(quote);
321                            s = tail;
322                            self.state = WriteState::WriteTokens(CommentState::None);
323                        }
324                    }
325                }
326            }
327        }
328
329        Ok(())
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    const TAB_WIDTH: usize = 4;
338
339    #[test]
340    fn test_buffer_indents() -> std::fmt::Result {
341        let delta = 1;
342
343        let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH);
344        assert_eq!(buf.indents.len(), 0);
345        assert_eq!(buf.level(), 0);
346        assert_eq!(buf.current_indent_len(), 0);
347
348        buf.indent(delta);
349        assert_eq!(buf.indents.len(), delta);
350        assert_eq!(buf.level(), delta);
351        assert_eq!(buf.current_indent_len(), delta * TAB_WIDTH);
352
353        buf.indent(delta);
354        buf.set_last_indent_group_skipped(true);
355        assert!(buf.last_indent_group_skipped());
356        assert_eq!(buf.indents.len(), delta * 2);
357        assert_eq!(buf.level(), delta);
358        assert_eq!(buf.current_indent_len(), delta * TAB_WIDTH);
359        buf.dedent(delta);
360
361        buf.dedent(delta);
362        assert_eq!(buf.indents.len(), 0);
363        assert_eq!(buf.level(), 0);
364        assert_eq!(buf.current_indent_len(), 0);
365
366        // panics on extra dedent
367        let res = std::panic::catch_unwind(|| buf.clone().dedent(delta));
368        assert!(res.is_err());
369
370        Ok(())
371    }
372
373    #[test]
374    fn test_identical_temp_buf() -> std::fmt::Result {
375        let content = "test string";
376        let multiline_content = "test\nmultiline\nmultiple";
377        let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH);
378
379        // create identical temp buf
380        let mut temp = buf.create_temp_buf();
381        writeln!(buf, "{content}")?;
382        writeln!(temp, "{content}")?;
383        assert_eq!(buf.w, format!("{content}\n"));
384        assert_eq!(temp.w, buf.w);
385        assert_eq!(temp.current_line_len, buf.current_line_len);
386        assert_eq!(temp.base_indent_len, buf.total_indent_len());
387
388        let delta = 1;
389        buf.indent(delta);
390
391        let mut temp_indented = buf.create_temp_buf();
392        assert!(temp_indented.w.is_empty());
393        assert_eq!(temp_indented.base_indent_len, buf.total_indent_len());
394        assert_eq!(temp_indented.level() + delta, buf.level());
395
396        let indent = " ".repeat(delta * TAB_WIDTH);
397
398        let mut original_buf = buf.clone();
399        write!(buf, "{multiline_content}")?;
400        let expected_content = format!(
401            "{}\n{}{}",
402            content,
403            indent,
404            multiline_content.lines().collect::<Vec<_>>().join(&format!("\n{indent}"))
405        );
406        assert_eq!(buf.w, expected_content);
407
408        write!(temp_indented, "{multiline_content}")?;
409
410        // write temp buf to original and assert the result
411        write!(original_buf, "{}", temp_indented.w)?;
412        assert_eq!(buf.w, original_buf.w);
413
414        Ok(())
415    }
416
417    #[test]
418    fn test_preserves_original_content_with_default_settings() -> std::fmt::Result {
419        let contents = [
420            "simple line",
421            r"
422            some 
423                    multiline
424    content",
425            "// comment",
426            "/* comment */",
427            r"mutliline
428            content
429            // comment1
430            with comments
431            /* comment2 */ ",
432        ];
433
434        for content in &contents {
435            let mut buf = FormatBuffer::new(String::new(), TAB_WIDTH);
436            write!(buf, "{content}")?;
437            assert_eq!(&buf.w, content);
438        }
439
440        Ok(())
441    }
442}