1#[cfg(feature = "cfxlua")]
2use crate::LuaVersion;
3use crate::{
4 context::{
5 create_indent_trivia, create_newline_trivia, line_ending_character, Context, FormatNode,
6 },
7 formatters::{
8 trivia::{FormatTriviaType, UpdateLeadingTrivia, UpdateTrailingTrivia, UpdateTrivia},
9 trivia_util::{
10 self, punctuated_inline_comments, CommentSearch, GetLeadingTrivia, GetTrailingTrivia,
11 HasInlineComments,
12 },
13 },
14 shape::Shape,
15 QuoteStyle,
16};
17use full_moon::node::Node;
18use full_moon::tokenizer::{StringLiteralQuoteType, Token, TokenKind, TokenReference, TokenType};
19use full_moon::{
20 ast::{
21 punctuated::{Pair, Punctuated},
22 span::ContainedSpan,
23 },
24 ShortString,
25};
26
27#[derive(Debug, Clone, Copy)]
28pub enum FormatTokenType {
29 Token,
30 LeadingTrivia,
31 TrailingTrivia,
32}
33
34#[derive(Debug)]
36pub enum EndTokenType {
37 IndentComments,
40 InlineComments,
42}
43
44#[macro_export]
45macro_rules! fmt_symbol {
46 ($ctx:expr, $token:expr, $x:expr, $shape:expr) => {
47 $crate::formatters::general::format_symbol(
48 $ctx,
49 $token,
50 &TokenReference::symbol_specific_lua_version($x, $ctx.config().syntax.into()).unwrap(),
51 $shape,
52 )
53 };
54}
55
56fn get_quote_to_use(ctx: &Context, literal: &str) -> StringLiteralQuoteType {
57 match ctx.config().quote_style {
58 QuoteStyle::ForceDouble => StringLiteralQuoteType::Double,
59 QuoteStyle::ForceSingle => StringLiteralQuoteType::Single,
60 _ => {
61 let preferred = match ctx.config().quote_style {
62 QuoteStyle::AutoPreferDouble => StringLiteralQuoteType::Double,
63 QuoteStyle::AutoPreferSingle => StringLiteralQuoteType::Single,
64 _ => unreachable!("have other quote styles we haven't looked into yet"),
65 };
66
67 if literal.contains('\'') || literal.contains('"') {
69 let num_single_quotes = literal.matches('\'').count();
70 let num_double_quotes = literal.matches('"').count();
71
72 match num_single_quotes.cmp(&num_double_quotes) {
73 std::cmp::Ordering::Equal => preferred,
74 std::cmp::Ordering::Greater => StringLiteralQuoteType::Double,
75 std::cmp::Ordering::Less => StringLiteralQuoteType::Single,
76 }
77 } else {
78 preferred
79 }
80 }
81 }
82}
83
84fn format_single_line_comment_string(comment: &str) -> &str {
85 comment.trim_end()
87}
88
89pub fn trivia_to_vec(
90 (token, leading, trailing): (Token, Option<Vec<Token>>, Option<Vec<Token>>),
91) -> Vec<Token> {
92 let mut output = leading.unwrap_or_default();
93 output.push(token);
94 output.append(&mut trailing.unwrap_or_default());
95
96 output
97}
98
99fn format_string_literal(
100 ctx: &Context,
101 literal: &ShortString,
102 multi_line_depth: &usize,
103 quote_type: &StringLiteralQuoteType,
104) -> TokenType {
105 #[cfg(feature = "cfxlua")]
107 if ctx.config().syntax == LuaVersion::CfxLua
108 && matches!(quote_type, StringLiteralQuoteType::Backtick)
109 {
110 return TokenType::StringLiteral {
111 literal: literal.clone(),
112 multi_line_depth: *multi_line_depth,
113 quote_type: StringLiteralQuoteType::Backtick,
114 };
115 }
116
117 if let StringLiteralQuoteType::Brackets = quote_type {
119 let literal = literal
122 .replace("\r\n", "\n")
123 .replace('\n', &line_ending_character(ctx.config().line_endings));
124
125 TokenType::StringLiteral {
126 literal: literal.into(),
127 multi_line_depth: *multi_line_depth,
128 quote_type: StringLiteralQuoteType::Brackets,
129 }
130 } else {
131 lazy_static::lazy_static! {
134 static ref RE: regex::Regex = regex::Regex::new(r#"\\?(["'])|\\([\S\s])"#).unwrap();
135 static ref UNNECESSARY_ESCAPES: regex::Regex = regex::Regex::new(r#"^[^\n\r"'0-9\\abfnrtuvxz]$"#).unwrap();
136 }
137 let quote_to_use = get_quote_to_use(ctx, literal);
138 let literal = RE
139 .replace_all(literal, |caps: ®ex::Captures| {
140 let quote = caps.get(1);
141 let escaped = caps.get(2);
142
143 match quote {
144 Some(quote) => {
145 match quote.as_str() {
148 "'" => {
149 if let StringLiteralQuoteType::Single = quote_to_use {
151 String::from("\\'")
152 } else {
153 String::from("'")
154 }
155 }
156 "\"" => {
157 if let StringLiteralQuoteType::Double = quote_to_use {
159 String::from("\\\"")
160 } else {
161 String::from("\"")
162 }
163 }
164 other => unreachable!("unknown quote type {:?}", other),
165 }
166 }
167 None => {
168 let text = escaped
171 .expect("have a match which was neither an escape or a quote")
172 .as_str();
173 if UNNECESSARY_ESCAPES.is_match(text) {
174 text.to_owned()
175 } else {
176 format!("\\{}", text.to_owned())
177 }
178 }
179 }
180 })
181 .into();
182
183 TokenType::StringLiteral {
184 literal,
185 multi_line_depth: *multi_line_depth,
186 quote_type: quote_to_use,
187 }
188 }
189}
190
191pub fn format_token(
195 ctx: &Context,
196 token: &Token,
197 format_type: FormatTokenType,
198 shape: Shape,
199) -> (Token, Option<Vec<Token>>, Option<Vec<Token>>) {
200 let mut leading_trivia: Option<Vec<Token>> = None;
201 let mut trailing_trivia: Option<Vec<Token>> = None;
202
203 let token_type = match token.token_type() {
204 TokenType::Number { text } => {
205 let text = if text.starts_with('.') {
206 String::from("0") + text.as_str()
207 } else if text.starts_with("-.") {
208 String::from("-0") + text.get(1..).expect("unknown number literal")
209 } else {
210 text.to_string()
211 }
212 .into();
213
214 TokenType::Number { text }
215 }
216 TokenType::StringLiteral {
217 literal,
218 multi_line_depth,
219 quote_type,
220 } => format_string_literal(ctx, literal, multi_line_depth, quote_type),
221 TokenType::Shebang { line } => {
222 let line = format_single_line_comment_string(line).into();
223
224 debug_assert!(matches!(format_type, FormatTokenType::LeadingTrivia));
226 trailing_trivia = Some(vec![create_newline_trivia(ctx)]);
227
228 TokenType::Shebang { line }
229 }
230 TokenType::SingleLineComment { comment } => {
231 let comment = format_single_line_comment_string(comment).into();
232
233 match format_type {
234 FormatTokenType::LeadingTrivia => {
235 leading_trivia = Some(vec![create_indent_trivia(ctx, shape)]);
236 trailing_trivia = Some(vec![create_newline_trivia(ctx)]);
237 }
238 FormatTokenType::TrailingTrivia => {
239 leading_trivia = Some(vec![Token::new(TokenType::spaces(1))]);
241 }
242 _ => (),
243 }
244
245 TokenType::SingleLineComment { comment }
246 }
247 TokenType::MultiLineComment { blocks, comment } => {
248 if let FormatTokenType::LeadingTrivia = format_type {
249 leading_trivia = Some(vec![create_indent_trivia(ctx, shape)]);
250 trailing_trivia = Some(vec![create_newline_trivia(ctx)]);
252 }
253
254 let comment = comment
257 .replace("\r\n", "\n")
258 .replace('\n', &line_ending_character(ctx.config().line_endings));
259
260 TokenType::MultiLineComment {
261 blocks: *blocks,
262 comment: comment.into(),
263 }
264 }
265 TokenType::Whitespace { characters } => TokenType::Whitespace {
266 characters: characters.to_owned(),
267 }, #[cfg(feature = "luau")]
269 TokenType::InterpolatedString { literal, kind } => TokenType::InterpolatedString {
270 literal: literal.to_owned(),
271 kind: kind.to_owned(),
272 },
273 _ => token.token_type().to_owned(),
274 };
275
276 (Token::new(token_type), leading_trivia, trailing_trivia)
277}
278
279fn load_token_trivia(
284 ctx: &Context,
285 current_trivia: Vec<&Token>,
286 format_token_type: FormatTokenType,
287 shape: Shape,
288) -> Vec<Token> {
289 let mut token_trivia = Vec::new();
290
291 let mut newline_count_in_succession = 0;
292 let mut trivia_iter = current_trivia.iter().peekable();
293
294 while let Some(trivia) = trivia_iter.next() {
295 match trivia.token_type() {
296 TokenType::Whitespace { characters } => {
297 match format_token_type {
303 FormatTokenType::LeadingTrivia if characters.contains('\n') => {
304 newline_count_in_succession += 1;
305 if newline_count_in_succession == 1 {
306 token_trivia.push(create_newline_trivia(ctx));
308 }
309 }
310 FormatTokenType::TrailingTrivia => {
311 if let Some(next_trivia) = trivia_iter.peek() {
314 if let TokenType::MultiLineComment { .. } = next_trivia.token_type() {
315 if !characters.contains('\n') {
316 token_trivia.push(Token::new(TokenType::spaces(1)))
317 }
318 }
319 }
320 }
321 _ => (),
322 }
323
324 continue;
326 }
327 TokenType::Shebang { .. }
328 | TokenType::SingleLineComment { .. }
329 | TokenType::MultiLineComment { .. } => {
330 if let FormatTokenType::LeadingTrivia = format_token_type {
333 if let Some(next_trivia) = trivia_iter.peek() {
334 if let TokenType::Whitespace { characters } = next_trivia.token_type() {
335 if characters.contains('\n') {
336 trivia_iter.next();
338 }
339 }
340 }
341 }
342 newline_count_in_succession = 0;
344 }
345 _ => {
346 newline_count_in_succession = 0;
348 }
349 }
350
351 let (token, leading_trivia, trailing_trivia) =
352 format_token(ctx, trivia.to_owned(), format_token_type, shape);
353 if let Some(mut trivia) = leading_trivia {
354 token_trivia.append(&mut trivia);
355 }
356
357 token_trivia.push(token);
358
359 if let Some(mut trivia) = trailing_trivia {
360 token_trivia.append(&mut trivia)
361 }
362 }
363
364 token_trivia
365}
366
367pub fn format_token_reference(
368 ctx: &Context,
369 token_reference: &TokenReference,
370 shape: Shape,
371) -> TokenReference {
372 let formatted_leading_trivia: Vec<Token> = load_token_trivia(
374 ctx,
375 token_reference.leading_trivia().collect(),
376 FormatTokenType::LeadingTrivia,
377 shape,
378 );
379 let formatted_trailing_trivia: Vec<Token> = load_token_trivia(
380 ctx,
381 token_reference.trailing_trivia().collect(),
382 FormatTokenType::TrailingTrivia,
383 shape,
384 );
385
386 let (token, _leading_trivia, _trailing_trivia) =
387 format_token(ctx, token_reference.token(), FormatTokenType::Token, shape);
388
389 TokenReference::new(formatted_leading_trivia, token, formatted_trailing_trivia)
390}
391
392pub fn format_punctuated<T, F>(
396 ctx: &Context,
397 old: &Punctuated<T>,
398 shape: Shape,
399 value_formatter: F,
400) -> Punctuated<T>
401where
402 T: std::fmt::Display,
403 F: Fn(&Context, &T, Shape) -> T,
404{
405 let mut list: Punctuated<T> = Punctuated::new();
406 let mut shape = shape;
407
408 for pair in old.pairs() {
409 match pair {
410 Pair::Punctuated(value, punctuation) => {
411 let value = value_formatter(ctx, value, shape);
412 let punctuation = fmt_symbol!(ctx, punctuation, ",", shape).update_trailing_trivia(
413 FormatTriviaType::Append(vec![Token::new(TokenType::spaces(1))]),
414 );
415 shape = shape.take_last_line(&value) + 2; list.push(Pair::new(value, Some(punctuation)));
418 }
419 Pair::End(value) => {
420 let value = value_formatter(ctx, value, shape);
421 list.push(Pair::new(value, None));
422 }
423 }
424 }
425
426 list
427}
428
429pub fn format_punctuated_multiline<T, F>(
431 ctx: &Context,
432 old: &Punctuated<T>,
433 shape: Shape,
434 value_formatter: F,
435 hang_level: Option<usize>,
436) -> Punctuated<T>
437where
438 T: Node + GetLeadingTrivia + UpdateLeadingTrivia + GetTrailingTrivia + UpdateTrailingTrivia,
439 F: Fn(&Context, &T, Shape) -> T,
440{
441 let mut formatted: Punctuated<T> = Punctuated::new();
442
443 let hanging_shape = match hang_level {
445 Some(hang_level) => shape.with_indent(shape.indent().add_indent_level(hang_level)),
446 None => shape,
447 };
448
449 for (idx, pair) in old.pairs().enumerate() {
450 let shape = if idx == 0 {
452 shape
453 } else {
454 hanging_shape.reset()
455 };
456
457 match pair {
458 Pair::Punctuated(value, punctuation) => {
459 let value = value_formatter(ctx, value, shape);
460 let value = if idx == 0 {
461 value
462 } else {
463 trivia_util::prepend_newline_indent(ctx, &value, hanging_shape)
464 };
465
466 let mut trailing_comments = value.trailing_comments();
469 let value = value.update_trailing_trivia(FormatTriviaType::Replace(vec![]));
470
471 let punctuation = fmt_symbol!(ctx, punctuation, ",", shape);
472 trailing_comments.append(&mut punctuation.trailing_trivia().cloned().collect());
473 let punctuation = punctuation
474 .update_trailing_trivia(FormatTriviaType::Replace(trailing_comments));
475
476 formatted.push(Pair::new(value, Some(punctuation)));
477 }
478 Pair::End(value) => {
479 let value = value_formatter(ctx, value, shape);
480 let value = if idx == 0 {
481 value
482 } else {
483 trivia_util::prepend_newline_indent(ctx, &value, hanging_shape)
484 };
485 formatted.push(Pair::new(value, None));
486 }
487 }
488 }
489
490 formatted
491}
492
493pub fn try_format_punctuated<T, F>(
496 ctx: &Context,
497 old: &Punctuated<T>,
498 shape: Shape,
499 value_formatter: F,
500 hang_level: Option<usize>,
501) -> Punctuated<T>
502where
503 T: Node
504 + GetLeadingTrivia
505 + UpdateLeadingTrivia
506 + GetTrailingTrivia
507 + UpdateTrailingTrivia
508 + HasInlineComments
509 + std::fmt::Display,
510 F: Fn(&Context, &T, Shape) -> T,
511{
512 let format_multiline = punctuated_inline_comments(old, false);
515
516 if format_multiline {
517 format_punctuated_multiline(ctx, old, shape, value_formatter, hang_level)
518 } else {
519 format_punctuated(ctx, old, shape, value_formatter)
520 }
521}
522
523pub fn format_contained_punctuated_multiline<T, F1>(
526 ctx: &Context,
527 parentheses: &ContainedSpan,
528 arguments: &Punctuated<T>,
529 argument_formatter: F1, shape: Shape,
531) -> (ContainedSpan, Punctuated<T>)
532where
533 T: UpdateLeadingTrivia + GetTrailingTrivia + UpdateTrailingTrivia,
534 F1: Fn(&Context, &T, Shape) -> T,
535{
536 let (start_parens, end_parens) = parentheses.tokens();
538 let start_parens = format_token_reference(ctx, start_parens, shape)
539 .update_trailing_trivia(FormatTriviaType::Append(vec![create_newline_trivia(ctx)]));
540
541 let end_parens = format_end_token(ctx, end_parens, EndTokenType::IndentComments, shape)
542 .update_leading_trivia(FormatTriviaType::Append(vec![create_indent_trivia(
543 ctx, shape,
544 )]));
545
546 let parentheses = ContainedSpan::new(start_parens, end_parens);
547
548 let mut formatted_arguments = Punctuated::new();
549 let shape = shape.increment_additional_indent();
550
551 for argument in arguments.pairs() {
552 let shape = shape.reset(); let formatted_argument = argument_formatter(ctx, argument.value(), shape)
555 .update_leading_trivia(FormatTriviaType::Append(vec![create_indent_trivia(
556 ctx, shape,
557 )]));
558
559 let multiline_comments =
562 formatted_argument.trailing_comments_search(CommentSearch::Multiline);
563 let singleline_comments =
564 formatted_argument.trailing_comments_search(CommentSearch::Single);
565
566 let formatted_argument = formatted_argument
567 .update_trailing_trivia(FormatTriviaType::Replace(multiline_comments));
568
569 let punctuation = match argument.punctuation() {
570 Some(punctuation) => {
571 let symbol = fmt_symbol!(ctx, punctuation, ",", shape);
574
575 let mut trailing_trivia: Vec<_> = symbol
576 .leading_trivia()
577 .filter(|trivia| trivia_util::trivia_is_comment(trivia))
578 .cloned()
579 .flat_map(|x| {
580 vec![
582 create_newline_trivia(ctx),
583 create_indent_trivia(ctx, shape),
584 x,
585 ]
586 })
587 .chain(singleline_comments)
588 .collect();
589 trailing_trivia.push(create_newline_trivia(ctx));
590
591 let symbol = symbol.update_trivia(
592 FormatTriviaType::Replace(vec![]),
593 FormatTriviaType::Append(trailing_trivia),
594 );
595
596 Some(symbol)
597 }
598 None => Some(TokenReference::new(
602 singleline_comments,
603 create_newline_trivia(ctx),
604 vec![],
605 )),
606 };
607
608 formatted_arguments.push(Pair::new(formatted_argument, punctuation))
609 }
610
611 (parentheses, formatted_arguments)
612}
613
614pub fn format_contained_span(
615 ctx: &Context,
616 contained_span: &ContainedSpan,
617 shape: Shape,
618) -> ContainedSpan {
619 let (start_token, end_token) = contained_span.tokens();
620
621 ContainedSpan::new(
622 format_token_reference(ctx, start_token, shape),
623 format_token_reference(ctx, end_token, shape),
624 )
625}
626
627pub fn format_symbol(
630 ctx: &Context,
631 current_symbol: &TokenReference,
632 wanted_symbol: &TokenReference,
633 shape: Shape,
634) -> TokenReference {
635 let mut formatted_leading_trivia: Vec<Token> = load_token_trivia(
637 ctx,
638 current_symbol.leading_trivia().collect(),
639 FormatTokenType::LeadingTrivia,
640 shape,
641 );
642 let mut formatted_trailing_trivia: Vec<Token> = load_token_trivia(
643 ctx,
644 current_symbol.trailing_trivia().collect(),
645 FormatTokenType::TrailingTrivia,
646 shape,
647 );
648
649 let mut wanted_leading_trivia: Vec<Token> = wanted_symbol
654 .leading_trivia()
655 .map(|x| x.to_owned())
656 .collect();
657 let mut wanted_trailing_trivia: Vec<Token> = wanted_symbol
658 .trailing_trivia()
659 .map(|x| x.to_owned())
660 .collect();
661 wanted_trailing_trivia.append(&mut formatted_trailing_trivia);
662 formatted_leading_trivia.append(&mut wanted_leading_trivia);
663
664 TokenReference::new(
665 formatted_leading_trivia,
666 wanted_symbol.token().to_owned(),
667 wanted_trailing_trivia,
668 )
669}
670
671pub fn format_end_token(
674 ctx: &Context,
675 current_token: &TokenReference,
676 token_type: EndTokenType,
677 shape: Shape,
678) -> TokenReference {
679 let mut formatted_leading_trivia: Vec<Token> = load_token_trivia(
681 ctx,
682 current_token.leading_trivia().collect(),
683 FormatTokenType::LeadingTrivia,
684 if let EndTokenType::IndentComments = token_type {
687 shape.increment_additional_indent()
688 } else {
689 shape
690 },
691 );
692 let formatted_trailing_trivia: Vec<Token> = load_token_trivia(
693 ctx,
694 current_token.trailing_trivia().collect(),
695 FormatTokenType::TrailingTrivia,
696 shape,
697 );
698
699 if !ctx.should_preserve_trailing_block_newline_gaps() {
700 let original_leading_trivia = std::mem::take(&mut formatted_leading_trivia);
704 let mut iter = original_leading_trivia.iter().cloned().rev().peekable();
705
706 let mut stop_removal = false;
707 while let Some(x) = iter.next() {
708 match x.token_type() {
709 TokenType::Whitespace { ref characters } => {
710 if !stop_removal
711 && characters.contains('\n')
712 && !matches!(
713 iter.peek().map(|x| x.token_kind()),
714 Some(TokenKind::SingleLineComment) | Some(TokenKind::MultiLineComment)
715 )
716 {
717 continue;
718 } else {
719 formatted_leading_trivia.push(x.to_owned());
720 }
721 }
722 _ => {
723 formatted_leading_trivia.push(x.to_owned());
724 stop_removal = true; }
726 }
727 }
728
729 formatted_leading_trivia.reverse();
731 }
732
733 TokenReference::new(
734 formatted_leading_trivia,
735 Token::new(current_token.token_type().to_owned()),
736 formatted_trailing_trivia,
737 )
738}
739
740fn pop_until_no_whitespace(trivia: &mut Vec<Token>) {
742 if let Some(t) = trivia.pop() {
743 match t.token_kind() {
744 TokenKind::Whitespace => pop_until_no_whitespace(trivia), _ => trivia.push(t), }
747 }
748}
749
750pub fn format_eof(ctx: &Context, eof: &TokenReference, shape: Shape) -> TokenReference {
754 if ctx.should_format_node(eof) != FormatNode::Normal {
755 return eof.to_owned();
756 }
757
758 let mut formatted_leading_trivia: Vec<Token> = load_token_trivia(
760 ctx,
761 eof.leading_trivia().collect(),
762 FormatTokenType::LeadingTrivia,
763 shape,
764 );
765
766 let only_whitespace = formatted_leading_trivia
767 .iter()
768 .all(|x| x.token_kind() == TokenKind::Whitespace);
769 if only_whitespace {
770 TokenReference::new(Vec::new(), Token::new(TokenType::Eof), Vec::new())
772 } else {
773 pop_until_no_whitespace(&mut formatted_leading_trivia);
775 formatted_leading_trivia.push(create_newline_trivia(ctx));
776
777 TokenReference::new(
778 formatted_leading_trivia,
779 Token::new(TokenType::Eof),
780 Vec::new(),
781 )
782 }
783}