codize/
list.rs

1use crate::{Code, Concat, Format, FormatCode};
2
3/// A list of code segments separated by a separator
4#[derive(derivative::Derivative)]
5#[derivative(Debug, Clone, PartialEq)]
6pub struct List {
7    /// The items in the list
8    concat_body: Concat,
9    /// The separator between the items
10    pub separator: String,
11    /// The trailing mode
12    pub trailing: Trailing,
13    /// When to inline
14    #[derivative(Debug = "ignore", PartialEq = "ignore")]
15    inline_condition: Option<fn(&List) -> bool>,
16}
17
18/// Trailing mode for a code list
19#[derive(Debug, Clone, PartialEq)]
20pub enum Trailing {
21    /// Add trailing separator if the list is split into multiple lines
22    IfMultiLine,
23    /// Always add trailing separator
24    Always,
25    /// Never add trailing separator
26    Never,
27}
28
29impl List {
30    /// Create a new empty code list
31    pub fn empty<TSep: ToString>(sep: TSep) -> Self {
32        Self {
33            separator: sep.to_string(),
34            concat_body: Concat::empty(),
35            trailing: Trailing::IfMultiLine,
36            inline_condition: None,
37        }
38    }
39
40    /// Create a new code list
41    pub fn new<TSep, TBody>(sep: TSep, body: TBody) -> Self
42    where
43        TSep: ToString,
44        TBody: IntoIterator,
45        TBody::Item: Into<Code>,
46    {
47        Self {
48            separator: sep.to_string(),
49            concat_body: Concat::new(body),
50            trailing: Trailing::IfMultiLine,
51            inline_condition: None,
52        }
53    }
54
55    /// Create a new code list with no trailing separator
56    pub fn no_trail(mut self) -> Self {
57        self.trailing = Trailing::Never;
58        self
59    }
60
61    /// Create a new code list with trailing separator even if the list is in one line
62    pub fn always_trail(mut self) -> Self {
63        self.trailing = Trailing::Always;
64        self
65    }
66
67    /// Set a condition for displaying the block as one line
68    pub fn inline_when(mut self, condition: fn(&List) -> bool) -> Self {
69        self.inline_condition = Some(condition);
70        self
71    }
72
73    /// Set the inline condition to be always true
74    pub fn inlined(mut self) -> Self {
75        self.inline_condition = Some(|_| true);
76        self
77    }
78
79    /// Set the inline condition to be always false
80    pub fn never_inlined(mut self) -> Self {
81        self.inline_condition = Some(|_| false);
82        self
83    }
84
85    /// Get the body of the block
86    #[inline]
87    pub fn body(&self) -> &[Code] {
88        &self.concat_body
89    }
90
91    /// Get if the list will generate any code or not (empty = no code)
92    #[inline]
93    pub fn is_empty(&self) -> bool {
94        self.concat_body.is_empty()
95    }
96
97    /// Should the list be displayed in one line
98    pub fn should_inline(&self) -> bool {
99        if let Some(condition) = self.inline_condition {
100            condition(self)
101        } else {
102            self.should_inline_intrinsic()
103        }
104    }
105
106    /// Should intrinsicly inline the list
107    ///
108    /// This is used for lists that only contain one item
109    pub fn should_inline_intrinsic(&self) -> bool {
110        self.body().len() == 1 && self.body()[0].should_inline()
111    }
112}
113
114impl From<List> for Code {
115    #[inline]
116    fn from(x: List) -> Self {
117        Code::List(x)
118    }
119}
120
121impl std::fmt::Display for List {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}", self.format())
124    }
125}
126
127impl FormatCode for List {
128    fn size_hint(&self) -> usize {
129        self.concat_body.size_hint()
130    }
131
132    fn format_into_vec_with(
133        &self,
134        format: &Format,
135        out: &mut Vec<String>,
136        connect: bool,
137        indent: &str,
138    ) {
139        let should_inline = self.should_inline();
140
141        // if first item is appended
142        // used to check if separator should be added
143        let mut first_appended = false;
144        // should next item be connected to the previous one
145        let mut previous_allow_connect = connect;
146
147        let mut previous_size = out.len();
148        let initial_size = previous_size;
149
150        for code in self.body().iter().filter(|c| !c.is_empty()) {
151            // append separator if needed
152            if let Some(last) = out.last_mut() {
153                if first_appended {
154                    last.push_str(&self.separator);
155                }
156            }
157            let connect = if first_appended {
158                should_inline
159                    || (previous_allow_connect && {
160                        // allow connect if the item is first, not block, or is non-inline block
161                        match code {
162                            Code::Block(b) => !b.should_inline(),
163                            _ => true,
164                        }
165                    })
166            } else {
167                // for first, inline if connect
168                connect
169            };
170            // emit the next item to out
171            code.format_into_vec_with(format, out, connect, indent);
172            // check if next item can be connected
173            // only connect if the current is multi-line
174            let new_size = out.len();
175            previous_allow_connect = new_size > previous_size + 1;
176            previous_size = new_size;
177            first_appended = true;
178        }
179
180        let should_trail = match self.trailing {
181            Trailing::IfMultiLine => previous_size > initial_size + 1,
182            Trailing::Always => true,
183            Trailing::Never => false,
184        };
185        if should_trail {
186            if let Some(last) = out.last_mut() {
187                last.push_str(&self.separator);
188            }
189        }
190    }
191}
192
193/// Macro for creating [`List`]s
194///
195/// Note that spaces and newlines are automatically added between the items after the separator.
196/// You don't need to specify them as part of the separator.
197///
198/// The default trailing separator behavior is only trail if the list is split into multiple lines.
199/// You can use [`List::no_trail`] or [`List::always_trail`] to change the behavior.
200///
201/// # Examples
202///
203/// ```
204/// use codize::{clist, cblock};
205///
206/// let expected = "call_something( a, b, c )";
207/// let code = cblock!("call_something(", [
208///     clist!("," => ["a", "b", "c"]).inlined()
209/// ], ")");
210/// assert_eq!(expected, code.to_string());
211///
212/// let expected =
213/// "call_something(
214///     a,
215///     b,
216///     c,
217/// )";
218/// let code = cblock!("call_something(", [
219///     clist!("," => ["a", "b", "c"])
220/// ], ")");
221/// assert_eq!(expected, code.to_string());
222/// ```
223#[macro_export]
224macro_rules! clist {
225    ($sep:expr => []) => {
226        $crate::List::empty($sep)
227    };
228    ($sep:expr => [ $( $body:expr ),* $(,)? ]) => {
229        $crate::List::new($sep, [ $($crate::Code::from($body)),* ])
230    };
231    ($sep:expr => $body:expr) => {
232        $crate::List::new($sep, $body)
233    };
234}
235
236#[cfg(test)]
237mod test {
238    use indoc::indoc;
239
240    use crate::{cblock, Block, Code, List};
241
242    #[test]
243    fn empty() {
244        let code = clist!("," => []);
245        assert_eq!("", code.to_string());
246    }
247
248    #[test]
249    fn one() {
250        let code = clist!("," => ["hello"]);
251        assert_eq!("hello", code.to_string());
252    }
253
254    #[test]
255    fn one_trail() {
256        let code = clist!("," => ["hello"]).always_trail();
257        assert_eq!("hello,", code.to_string());
258    }
259
260    #[test]
261    fn many() {
262        let code = clist!("," => ["hello", "hello2"]);
263        assert_eq!("hello,\nhello2,", code.to_string());
264    }
265
266    #[test]
267    fn many_no_trail() {
268        let code = clist!("," => ["hello", "hello2"]).no_trail();
269        assert_eq!("hello,\nhello2", code.to_string());
270    }
271
272    #[test]
273    fn with_blocks() {
274        // the first block is inline, so next block cannot be connected
275        let expected = indoc! {"
276            { a },
277            {
278                hello,
279                hello2
280            }, {
281                foo,
282                bar,
283            },
284            { b },
285            { c },"};
286
287        let expected_inline = indoc! {"
288            { a }, {
289                hello,
290                hello2
291            }, {
292                foo,
293                bar,
294            }, { b }, { c },"};
295
296        fn should_inline_block(c: &Block) -> bool {
297            if let Some(Code::Line(s)) = c.body().first() {
298                s.len() == 1
299            } else {
300                false
301            }
302        }
303        let code = clist!("," => [
304            cblock!("{", ["a"], "}").inline_when(should_inline_block),
305            cblock!("{", [clist!("," => ["hello", "hello2"]).no_trail()], "}"),
306            cblock!("{", [clist!("," => ["foo", "bar"])], "}"),
307            cblock!("{", ["b"], "}").inline_when(should_inline_block),
308            cblock!("{", ["c"], "}").inline_when(should_inline_block),
309        ]);
310        assert_eq!(expected, code.to_string());
311        assert_eq!(expected_inline, code.inline_when(|_| true).to_string());
312    }
313
314    #[test]
315    fn multiple_levels() {
316        let expected = indoc! {"
317            a, b, c,
318            d, e, f,
319            x, y, z,"};
320        fn always(_: &List) -> bool {
321            true
322        }
323        let code = clist!("," => [
324            clist!("," => ["a", "b", "c"]).inline_when(always),
325            clist!("," => ["d", "e", "f"]).inline_when(always),
326            clist!("," => ["x", "y", "z"]).inline_when(always),
327        ]);
328        assert!(!code.should_inline());
329        assert_eq!(expected, code.to_string());
330    }
331}