rsn_fmt/
lib.rs

1//! Formatter for [`rsn`]
2#![warn(clippy::pedantic, missing_docs)]
3use std::fmt::{Display, Write};
4use std::ops::Range;
5
6use rsn::tokenizer::{self, Balanced, Token, TokenKind, Tokenizer};
7use thiserror::Error;
8
9/// Configuration for `rsnfmt`
10pub mod config;
11pub use config::Config;
12mod utils;
13#[allow(clippy::wildcard_imports)]
14use utils::*;
15
16type Result<T, E = Error> = std::result::Result<T, E>;
17
18#[derive(Error, Debug)]
19/// Error returned from [`format_str`]
20pub enum Error {
21    /// Error Originating from Tokenization
22    #[error("tokenizer error: {_0:?}")]
23    Tokenizer(#[from] tokenizer::Error),
24    /// Missmatched delimiter e.g. `( ... ]` or `... }`
25    #[error("missmatched delimiter at {_0:?}")]
26    MissmatchedDelimiter(Range<usize>),
27}
28
29/// Unwrapping write, because we only write to [`String`]
30// Tried to shadow `std::write` but ra doesn't like: https://github.com/rust-lang/rust-analyzer/issues/13683
31macro_rules! w {
32    ($($tt:tt)*) => {
33        { write!($($tt)*).unwrap(); }
34    };
35}
36
37struct Indent {
38    level: usize,
39    hard_tab: bool,
40    width: usize,
41}
42
43impl Indent {
44    fn inc(&mut self) {
45        self.level += 1;
46    }
47
48    fn dec(&mut self) {
49        self.level -= 1;
50    }
51}
52
53impl Display for Indent {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        if self.hard_tab {
56            write!(f, "{:1$\t}", "", self.level)
57        } else {
58            write!(f, "{:1$}", "", self.width * self.level)
59        }
60    }
61}
62
63/// # Errors
64/// Errors on syntactically invalid rsn.
65pub fn format_str(source: &str, config: &Config) -> Result<String> {
66    let mut tokenizer = Tokenizer::full(source);
67    let mut f = String::new();
68    let mut indent = config.indent();
69    let mut opened = Vec::new();
70    let mut nled = false;
71    let mut spaced = false;
72    let nl = config.line_ending(source);
73    while let Some(token) = tokenizer.next() {
74        let Token { location, kind } = token?;
75        match kind {
76            TokenKind::Integer(_)
77            | TokenKind::Float(_)
78            | TokenKind::Bool(_)
79            | TokenKind::Character(_)
80            | TokenKind::Byte(_)
81            | TokenKind::String(_) // TODO align escaped newline
82            | TokenKind::Bytes(_)  // TODO align escaped newline
83            | TokenKind::Identifier(_)
84            | TokenKind::Comment(_) // TODO indent
85            => {
86                if nled {
87                    w!(f, "{indent}{}", &source[location]);
88                } else {
89                    if spaced {
90                        w!(f, " ");
91                    }
92                    w!(f, "{}", &source[location]);
93                }
94            }
95            TokenKind::Colon => w!(f, ":"),
96            TokenKind::Comma => w!(f, ",{nl}"),
97            TokenKind::Open(delimiter) => {
98                let tmp = tokenizer.clone();
99                match format_single_line(source, &mut tokenizer, delimiter, config)? {
100                    Some(single_line) if f.lines().last().unwrap_or_default().len() + single_line.len() < config.max_width =>  {
101                        if spaced || delimiter == Balanced::Brace {
102                            w!(f, " ");
103                        }
104                        w!(f, "{single_line}");
105                    }
106                    _ => {
107                        if nled {
108                            w!(f, "{indent}{}{nl}", delimiter.open());
109                        } else if spaced || delimiter.is_brace() {
110                            w!(f, " {}{nl}", delimiter.open());
111                        } else {
112                            w!(f, "{}{nl}", delimiter.open());
113                        }
114                        opened.push(delimiter);
115                        indent.inc();
116                        tokenizer = tmp;
117                    }
118                }
119            }
120            TokenKind::Close(delimiter) => {
121                indent.dec();
122                if nled {
123                    w!(f, "{indent}{}", delimiter.close());
124                } else {
125                    w!(f, "{nl}{indent}{}", delimiter.close());
126                }
127                if opened.is_empty() || delimiter != opened.pop().expect("opened is not empty") {
128                    return Err(Error::MissmatchedDelimiter(location));
129                }
130            }
131            TokenKind::Whitespace(ws) => {
132                match config.preserve_empty_lines {
133                    config::PreserveEmptyLines::One => {
134                        if ws.chars().filter(|c|*c=='\n').count() > 1 {
135                            if !nled {
136                                w!(f, "{nl}");
137                            }
138                            w!(f, "{nl}");
139                            nled = true;
140                            spaced = false;
141                        }
142                    },
143                    config::PreserveEmptyLines::All => for _ in 0..ws.chars().filter(|c|*c=='\n').count().saturating_sub(usize::from(nled)) {
144                        w!(f, "{nl}");
145                        nled = true;
146                        spaced = false;
147                    },
148                    config::PreserveEmptyLines::None => {},
149                }
150            }
151        }
152        if !matches!(kind, TokenKind::Whitespace(_)) {
153            nled = matches!(kind, TokenKind::Comma | TokenKind::Open(_));
154            spaced = kind == TokenKind::Colon;
155        }
156    }
157    Ok(f)
158}
159
160fn format_single_line(
161    source: &str,
162    tokenizer: &mut Tokenizer<true>,
163    delimiter: Balanced,
164    config: &Config,
165) -> Result<Option<String>> {
166    let mut f = String::new();
167    let mut opened = vec![delimiter];
168    let mut spaced = delimiter == Balanced::Brace;
169    let mut comma = false;
170    let mut empty = true;
171    let mut unspaced = true;
172    w!(f, "{}", delimiter.open());
173    for token in tokenizer {
174        let Token { location, kind } = token?;
175        if comma {
176            match kind {
177                TokenKind::Integer(_)
178                | TokenKind::Float(_)
179                | TokenKind::Bool(_)
180                | TokenKind::Character(_)
181                | TokenKind::Byte(_)
182                | TokenKind::String(_)
183                | TokenKind::Bytes(_)
184                | TokenKind::Identifier(_)
185                | TokenKind::Open(_) => {
186                    w!(f, ",");
187                    comma = false;
188                }
189                TokenKind::Close(_) => comma = false,
190                _ => {}
191            }
192        }
193        match kind {
194            TokenKind::Byte(_) | TokenKind::String(_)
195                if source[location.clone()].contains('\n') =>
196            {
197                return Ok(None);
198            }
199            TokenKind::Open(_) if opened.len() > config.max_inline_level => {
200                return Ok(None);
201            }
202            TokenKind::Close(_) if !empty && opened.len() > config.max_inline_level => {
203                return Ok(None);
204            }
205            TokenKind::Integer(_)
206            | TokenKind::Float(_)
207            | TokenKind::Bool(_)
208            | TokenKind::Character(_)
209            | TokenKind::Byte(_)
210            | TokenKind::String(_)
211            | TokenKind::Bytes(_)
212            | TokenKind::Identifier(_) => {
213                if spaced {
214                    w!(f, " ");
215                }
216                w!(f, "{}", &source[location]);
217            }
218            TokenKind::Comment(_) => {
219                // TODO inline comment
220                return Ok(None);
221            }
222            TokenKind::Colon => {
223                w!(f, ":");
224            }
225            TokenKind::Comma => comma = true,
226            TokenKind::Open(delimiter) => {
227                opened.push(delimiter);
228                if (spaced || delimiter == Balanced::Brace) && !unspaced {
229                    w!(f, " ");
230                }
231                w!(f, "{}", delimiter.open());
232            }
233            TokenKind::Close(delimiter) => {
234                if delimiter == Balanced::Brace {
235                    w!(f, " {}", delimiter.close());
236                } else {
237                    w!(f, "{}", delimiter.close());
238                }
239                if opened.is_empty() || delimiter != opened.pop().expect("opened is not empty") {
240                    return Err(Error::MissmatchedDelimiter(location));
241                }
242                if opened.is_empty() {
243                    return Ok(Some(f));
244                }
245            }
246            TokenKind::Whitespace(ws) => {
247                if ws.chars().filter(|c| *c == '\n').count() > 1
248                    && !config.preserve_empty_lines.is_none()
249                {
250                    return Ok(None);
251                }
252            }
253        }
254        if !kind.is_white_space() {
255            spaced = matches!(
256                kind,
257                TokenKind::Colon | TokenKind::Comma | TokenKind::Open(Balanced::Brace)
258            );
259            unspaced = kind.is_open();
260        }
261        empty |= !(kind.is_value() || kind.is_comment() || kind.is_close());
262    }
263    Ok(Some(f))
264}