Skip to main content

rustidy_format/
whitespace.rs

1//! Whitespace impls
2
3// Imports
4use {
5	crate::{Format, FormatOutput, FormatTag, Formattable, WhitespaceConfig},
6	core::ops::ControlFlow,
7	itertools::Itertools,
8	rustidy_util::{AstStr, ast_str::AstStrRepr, whitespace::{Comment, Whitespace}},
9	std::sync::Arc,
10};
11
12#[extend::ext(name = WhitespaceFormat)]
13pub impl Whitespace {
14	const INDENT: WhitespaceConfig = WhitespaceConfig { format: Some(
15		WhitespaceFormatKind::Indent { use_prev: false, remove_if_pure: false, }
16	), };
17	const PRESERVE: WhitespaceConfig = WhitespaceConfig { format: None };
18	const INDENT_CLOSE: WhitespaceConfig = WhitespaceConfig { format: Some(
19		WhitespaceFormatKind::Indent { use_prev: true, remove_if_pure: false, }
20	), };
21	const INDENT_CLOSE_REMOVE_IF_PURE: WhitespaceConfig = WhitespaceConfig { format: Some(
22		WhitespaceFormatKind::Indent { use_prev: true, remove_if_pure: true, }
23	), };
24	const INDENT_REMOVE_IF_PURE: WhitespaceConfig = WhitespaceConfig { format: Some(
25		WhitespaceFormatKind::Indent { use_prev: false, remove_if_pure: true, }
26	), };
27	const REMOVE: WhitespaceConfig = WhitespaceConfig { format: Some(WhitespaceFormatKind::Remove), };
28	const SINGLE: WhitespaceConfig = WhitespaceConfig {
29		format: Some(WhitespaceFormatKind::Spaces { len: 1 }),
30	};
31
32	fn spaces(len: usize) -> WhitespaceConfig {
33		let len = u16::try_from(len)
34			.expect("Cannot format more than 2^16 spaces");
35		WhitespaceConfig {
36			format: Some(WhitespaceFormatKind::Spaces { len }),
37		}
38	}
39
40	fn indent(remove_if_pure: bool) -> WhitespaceConfig {
41		WhitespaceConfig { format: Some(
42			WhitespaceFormatKind::Indent { use_prev: false, remove_if_pure }
43		), }
44	}
45
46	fn prev_indent(remove_if_pure: bool) -> WhitespaceConfig {
47		WhitespaceConfig { format: Some(
48			WhitespaceFormatKind::Indent { use_prev: true, remove_if_pure }
49		), }
50	}
51
52	/// Returns if this whitespace is empty
53	fn is_empty(&mut self) -> bool {
54		self.0.first.0.is_empty() && self.0.rest.is_empty()
55	}
56
57	/// Returns if this whitespace only contains pure whitespace
58	fn is_pure(&mut self) -> bool {
59		self.0.rest.is_empty()
60	}
61
62	/// Joins `other` to this whitespace as a suffix
63	fn join_suffix(&mut self, other: Self) {
64		let lhs = &mut *self.0;
65		let mut rhs = other.0.take();
66
67		let lhs_last = match lhs.rest.last_mut() {
68			Some((_, last)) => last,
69			None => &mut lhs.first,
70		};
71
72		replace_with::replace_with_or_abort(
73			&mut lhs_last.0,
74			|lhs_last| AstStr::join(lhs_last, rhs.first.0)
75		);
76		lhs.rest.append(&mut rhs.rest);
77	}
78
79	/// Joins `other` to this whitespace as a prefix
80	fn join_prefix(&mut self, mut other: Self) {
81		replace_with::replace_with_or_abort(self, |this| {
82			other.join_suffix(this);
83			other
84		});
85	}
86}
87
88impl Formattable for Whitespace {
89	fn with_prefix_ws<O>(
90		&mut self,
91		ctx: &mut crate::Context,
92		f: &mut impl FnMut(&mut Self,&mut crate::Context) -> O,
93	) -> Result<O, ControlFlow<()>> {
94		Ok(f(self, ctx))
95	}
96
97	fn with_strings<O>(
98		&mut self,
99		ctx: &mut crate::Context,
100		exclude_prefix_ws: bool,
101		f: &mut impl FnMut(&mut AstStr,&mut crate::Context) -> ControlFlow<O>,
102	) -> ControlFlow<O, bool> {
103		let is_empty = self.0.first.0.is_empty() && self.0.rest.is_empty();
104
105		if !exclude_prefix_ws {
106			f(&mut self.0.first.0, ctx)?;
107			for (comment, pure) in &mut self.0.rest {
108				match comment {
109					Comment::Line(comment) => f(&mut comment.0, ctx)?,
110					Comment::Block(comment) => f(&mut comment.0, ctx)?,
111				}
112				f(&mut pure.0, ctx)?;
113			}
114		}
115
116		ControlFlow::Continue(is_empty)
117	}
118
119	fn format_output(&mut self, ctx: &mut crate::Context) -> FormatOutput {
120		let mut output = self.0.first.0.format_output(ctx);
121		for (comment, ws) in &mut self.0.rest {
122			match comment {
123				Comment::Line(comment) => comment.0
124					.format_output(ctx)
125					.append_to(&mut output),
126				Comment::Block(comment) => comment.0
127					.format_output(ctx)
128					.append_to(&mut output),
129			}
130
131			ws.0.format_output(ctx).append_to(&mut output);
132		}
133		output.prefix_ws_len = Some(output.len);
134		if let Some(multiline) = &mut output.multiline {
135			multiline.prefix_ws_len = Some(multiline.prefix_len);
136		}
137
138		output
139	}
140}
141
142// Note: This impl is useful for types that have whitespace within them,
143//       but require that whitespace to be empty.
144// TODO: Remove this impl and just make it so any place
145//       that skips whitespace during parsing instead
146//       does it at the type level.
147impl Format<(), ()> for Whitespace {
148	fn format(
149		&mut self,
150		_ctx: &mut crate::Context,
151		_prefix_ws: (),
152		_args: ()
153	) -> FormatOutput {
154		if !self.is_empty() {
155			tracing::warn!("Whitespace was not empty");
156		}
157		FormatOutput::default()
158	}
159}
160
161impl Format<WhitespaceConfig, ()> for Whitespace {
162	fn format(
163		&mut self,
164		ctx: &mut crate::Context,
165		prefix_ws: WhitespaceConfig,
166		_args: ()
167	) -> FormatOutput {
168		if let Some(format) = prefix_ws.format {
169			self::format(self, ctx, format);
170		}
171
172		self.format_output(ctx)
173	}
174}
175
176#[derive(Clone, Copy, Debug)]
177#[derive(strum::EnumIs)]
178#[doc(hidden)]
179pub enum WhitespaceFormatKind {
180	Remove,
181
182	Spaces {
183		/// Number of spaces
184		len: u16,
185	},
186
187	Indent {
188		/// Use the previous indentation
189		use_prev:       bool,
190
191		/// Remove if the whitespace is pure
192		remove_if_pure: bool,
193	},
194}
195
196impl WhitespaceFormatKind {
197	/// Returns the indentation string, with a newline *before*
198	// TODO: Should we be checking for multiple newlines?
199	fn indent_str_nl(
200		ctx: &mut crate::Context,
201		cur_str: &AstStr,
202		after_newline: bool
203	) -> AstStrRepr {
204		let min_newlines = ctx.config().min_empty_lines;
205		let max_newlines = ctx.config().max_empty_lines;
206		let (min_newlines, max_newlines) = match after_newline {
207			true => (min_newlines, max_newlines),
208			false => (min_newlines + 1, max_newlines + 1),
209		};
210		// Note: We try to use the input's number of newlines, since we might
211		//       have been changed.
212		let newlines = match cur_str.input() {
213			Some(input) => rustidy_util::str_count_newlines(input),
214			None => cur_str.count_newlines(),
215		};
216		let newlines = newlines.clamp(min_newlines, max_newlines);
217
218		AstStrRepr::Indentation {
219			indent: Arc::clone(&ctx.config().indent),
220			newlines,
221			depth: ctx.indent(),
222		}
223	}
224
225	/// Returns the prefix string
226	fn prefix_str(
227		self,
228		ctx: &mut crate::Context,
229		cur_str: &AstStr,
230		is_last: bool,
231		after_newline: bool
232	) -> AstStrRepr {
233		match self {
234			Self::Remove => "".into(),
235			Self::Spaces { len } => AstStrRepr::Spaces { len },
236			Self::Indent { use_prev, remove_if_pure } => match remove_if_pure && is_last {
237				true => "".into(),
238				false => ctx.with_indent_offset_if(
239					-1,
240					use_prev && is_last,
241					|ctx| Self::indent_str_nl(ctx, cur_str, after_newline)
242				),
243			},
244		}
245	}
246
247	/// Returns the string after a newline
248	fn after_newline_str(
249		self,
250		ctx: &mut crate::Context,
251		cur_str: &AstStr,
252		is_last: bool
253	) -> AstStrRepr {
254		match self {
255			Self::Remove | Self::Spaces { .. } => "".into(),
256			Self::Indent { use_prev, .. } => match is_last {
257				true => ctx.with_indent_offset_if(
258					-1,
259					use_prev,
260					|ctx| Self::indent_str_nl(ctx, cur_str, true)
261				),
262				false => Self::indent_str_nl(ctx, cur_str, true),
263			},
264		}
265	}
266
267	/// Returns the normal string
268	fn normal_str(
269		self,
270		ctx: &mut crate::Context,
271		cur_str: &AstStr,
272		is_last: bool
273	) -> AstStrRepr {
274		match self {
275			Self::Remove => "".into(),
276			Self::Spaces { len } => AstStrRepr::Spaces { len },
277			Self::Indent { use_prev, .. } => match is_last {
278				true => ctx.with_indent_offset_if(
279					-1,
280					use_prev,
281					|ctx| Self::indent_str_nl(ctx, cur_str, false)
282				),
283				false => Self::indent_str_nl(ctx, cur_str, false),
284			},
285		}
286	}
287}
288
289#[doc(hidden)]
290pub fn format(
291	ws: &mut Whitespace,
292	ctx: &mut crate::Context,
293	kind: WhitespaceFormatKind
294) {
295	// Note: If we're whitespace after a line doc comment, then we have a newline
296	//       prior to us that we need to take into account.
297	// TODO: We should do this even when we're preserving the whitespace
298	let after_newline = ctx.remove_tag(FormatTag::AfterNewline);
299
300	let prefix_str = kind.prefix_str(
301		ctx,
302		&ws.0.first.0,
303		ws.0.rest.is_empty(),
304		after_newline
305	);
306	ws.0.first.0.replace(prefix_str);
307
308	for (pos, (comment, ws)) in ws.0.rest.iter_mut().with_position() {
309		let is_last = matches!(pos, itertools::Position::Last | itertools::Position::Only);
310		let ws_str = match comment.is_line() {
311			true => kind.after_newline_str(ctx, &ws.0, is_last),
312			false => kind.normal_str(ctx, &ws.0, is_last),
313		};
314		ws.0.replace(ws_str);
315
316		if is_last && let Comment::Line(comment) = comment && !comment.0.has_newlines() {
317			let mut s = comment.0.str().into_owned();
318			s.push('\n');
319			comment.0
320				.replace(AstStrRepr::String(s.into()));
321		}
322	}
323}