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}