stylua_lib/
shape.rs

1use crate::context::Context;
2use crate::formatters::{
3    trivia::{FormatTriviaType, UpdateTrivia},
4    trivia_util::trivia_is_comment,
5};
6use full_moon::node::Node;
7use std::fmt::Display;
8use std::ops::Add;
9
10/// A struct representing indentation level of the current code
11#[derive(Clone, Copy, Debug)]
12pub struct Indent {
13    /// How many characters a single indent level represents. This is inferred from the configuration
14    indent_width: usize,
15    /// The current block indentation level. The base indentation level is 0. Note: this is not the indentation width
16    block_indent: usize,
17    /// Any additional indent level that we are in, excluding the block indent. For example, within a multiline table.
18    additional_indent: usize,
19}
20
21impl Indent {
22    /// Creates a new indentation at the base indent level, inferring indent_width from context.
23    pub fn new(ctx: &Context) -> Self {
24        Self {
25            block_indent: 0,
26            additional_indent: 0,
27            indent_width: ctx.config().indent_width,
28        }
29    }
30
31    /// The current block indentation level
32    pub fn block_indent(&self) -> usize {
33        self.block_indent
34    }
35
36    /// The current additional indentation level
37    pub fn additional_indent(&self) -> usize {
38        self.additional_indent
39    }
40
41    /// The configured width of a single indent
42    pub fn configured_indent_width(&self) -> usize {
43        self.indent_width
44    }
45
46    /// The current width (characters) taken up by indentation
47    pub fn indent_width(&self) -> usize {
48        (self.block_indent + self.additional_indent) * self.indent_width
49    }
50
51    /// Recreates an Indent struct with the given additional indent level
52    pub fn with_additional_indent(&self, additional_indent: usize) -> Self {
53        Self {
54            additional_indent,
55            ..*self
56        }
57    }
58
59    /// Increments the block indentation level by one
60    pub fn increment_block_indent(&self) -> Self {
61        Self {
62            block_indent: self.block_indent.saturating_add(1),
63            ..*self
64        }
65    }
66
67    // Decrements the block indentation level by one
68    // pub fn decrement_block_indent(&self) -> Self {
69    //     Self {
70    //         block_indent: self.block_indent.saturating_sub(1),
71    //         ..*self
72    //     }
73    // }
74
75    /// Increments the additional indentation level by one
76    pub fn increment_additional_indent(&self) -> Self {
77        Self {
78            additional_indent: self.additional_indent.saturating_add(1),
79            ..*self
80        }
81    }
82
83    // Decrements the additional indentation level by one
84    // pub fn decrement_additional_indent(&self) -> Self {
85    //     Self {
86    //         additional_indent: self.additional_indent.saturating_sub(1),
87    //         ..*self
88    //     }
89    // }
90
91    /// Increases the additional indentation level by amount specified
92    pub fn add_indent_level(&self, amount: usize) -> Self {
93        Self {
94            additional_indent: self.additional_indent.saturating_add(amount),
95            ..*self
96        }
97    }
98}
99
100#[derive(Clone, Copy, Debug)]
101pub struct Shape {
102    /// The current indentation level
103    indent: Indent,
104    /// The current width we have taken on the line, excluding any indentation.
105    offset: usize,
106    /// The maximum number of characters we want to fit on a line. This is inferred from the configuration
107    column_width: usize,
108    /// Whether we should use simple heuristic checking.
109    /// This is enabled when we are calling within a heuristic itself, to reduce the exponential blowup
110    simple_heuristics: bool,
111}
112
113impl Shape {
114    /// Creates a new shape at the base indentation level
115    #[must_use]
116    pub fn new(ctx: &Context) -> Self {
117        Self {
118            indent: Indent::new(ctx),
119            offset: 0,
120            column_width: ctx.config().column_width,
121            simple_heuristics: false,
122        }
123    }
124
125    /// Sets the column width to the provided width. Normally only used to set an infinite width when testing layouts
126    #[must_use]
127    pub fn with_column_width(&self, column_width: usize) -> Self {
128        Self {
129            column_width,
130            ..*self
131        }
132    }
133
134    /// Recreates the shape with the provided indentation
135    #[must_use]
136    pub fn with_indent(&self, indent: Indent) -> Self {
137        Self { indent, ..*self }
138    }
139
140    /// Recreates the shape with an infinite width. Useful when testing layouts and want to force code onto a single line
141    #[must_use]
142    pub fn with_infinite_width(&self) -> Self {
143        self.with_column_width(usize::MAX)
144    }
145
146    /// The current indentation of the shape
147    #[must_use]
148    pub fn indent(&self) -> Indent {
149        self.indent
150    }
151
152    /// Increments the block indentation level by one. Alias for `shape.with_indent(shape.indent().increment_block_indent())`
153    #[must_use]
154    pub fn increment_block_indent(&self) -> Self {
155        Self {
156            indent: self.indent.increment_block_indent(),
157            ..*self
158        }
159    }
160
161    /// Increments the additional indentation level by one. Alias for `shape.with_indent(shape.indent().increment_additional_indent())`
162    #[must_use]
163    pub fn increment_additional_indent(&self) -> Self {
164        Self {
165            indent: self.indent.increment_additional_indent(),
166            ..*self
167        }
168    }
169
170    /// The width currently taken up for this line
171    #[must_use]
172    pub fn used_width(&self) -> usize {
173        self.indent.indent_width() + self.offset
174    }
175
176    /// Check to see whether our current width is above the budget available
177    #[must_use]
178    pub fn over_budget(&self) -> bool {
179        self.used_width() > self.column_width
180    }
181
182    /// Adds a width offset to the current width total
183    #[must_use]
184    pub fn add_width(&self, width: usize) -> Shape {
185        Self {
186            offset: self.offset + width,
187            ..*self
188        }
189    }
190
191    /// Whether simple heuristics should be used when calculating formatting shape
192    /// This is to reduce the expontential blowup of discarded test formatting
193    #[must_use]
194    pub fn using_simple_heuristics(&self) -> bool {
195        self.simple_heuristics
196    }
197
198    #[must_use]
199    pub fn with_simple_heuristics(&self) -> Shape {
200        Self {
201            simple_heuristics: true,
202            ..*self
203        }
204    }
205
206    /// Resets the offset for the shape
207    #[must_use]
208    pub fn reset(&self) -> Shape {
209        Self { offset: 0, ..*self }
210    }
211
212    /// Takes the first line from an item which can be converted into a string, and sets that to the shape
213    #[must_use]
214    pub fn take_first_line<T: Display>(&self, item: &T) -> Shape {
215        let string = format!("{item}");
216        let mut lines = string.lines();
217        let width = lines.next().unwrap_or("").len();
218        self.add_width(width)
219    }
220
221    /// Takes an item which could possibly span multiple lines. If it spans multiple lines, the shape is reset
222    /// and the last line is added to the width. If it only takes a single line, we just continue adding to the current
223    /// width
224    #[must_use]
225    pub fn take_last_line<T: Display>(&self, item: &T) -> Shape {
226        let string = format!("{item}");
227        let mut lines = string.lines();
228        let last_item = lines.next_back().unwrap_or("");
229
230        // Check if we have any more lines remaining
231        if lines.count() > 0 {
232            // Reset the shape and add the last line
233            self.reset().add_width(last_item.len())
234        } else {
235            // Continue adding to the current shape
236            self.add_width(last_item.len())
237        }
238    }
239
240    /// Takes in a new node, and tests whether adding it in will force any lines over the budget.
241    /// This function attempts to ignore the impact of comments by removing them, which makes this function more expensive.
242    /// NOTE: This function does not update state/return a new shape
243    #[must_use]
244    pub fn test_over_budget<T: Node>(&self, item: &T) -> bool {
245        // Converts the node into a string, removing any comments present
246        // We strip leading/trailing comments of each token present, but keep whitespace
247        let string = item
248            .tokens()
249            .map(|token| {
250                token
251                    .update_trivia(
252                        FormatTriviaType::Replace(
253                            token
254                                .leading_trivia()
255                                .filter(|token| !trivia_is_comment(token))
256                                .map(|x| x.to_owned())
257                                .collect(),
258                        ),
259                        FormatTriviaType::Replace(
260                            token
261                                .trailing_trivia()
262                                .filter(|token| !trivia_is_comment(token))
263                                .map(|x| x.to_owned())
264                                .collect(),
265                        ),
266                    )
267                    .to_string()
268            })
269            .collect::<String>();
270
271        let lines = string.lines();
272
273        lines.enumerate().any(|(idx, line)| {
274            let shape = if idx == 0 { *self } else { self.reset() };
275            shape.add_width(line.len()).over_budget()
276        })
277    }
278}
279
280impl Add<usize> for Shape {
281    type Output = Shape;
282
283    fn add(self, rhs: usize) -> Shape {
284        self.add_width(rhs)
285    }
286}