forge_fmt/
buffer.rs

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