#[cfg(feature = "luau")]
use full_moon::ast::types::TypeSpecifier;
use full_moon::tokenizer::{Token, TokenReference};
use full_moon::{
ast::{
punctuated::{Pair, Punctuated},
Assignment, Call, Expression, FunctionArgs, FunctionCall, LocalAssignment, Suffix, Value,
},
tokenizer::TokenType,
};
#[cfg(feature = "lua54")]
use crate::formatters::lua54::format_attribute;
#[cfg(feature = "luau")]
use crate::formatters::luau::format_type_specifier;
use crate::{
context::{create_indent_trivia, create_newline_trivia, Context},
fmt_symbol,
formatters::{
expression::{format_expression, format_var, hang_expression},
general::{
format_punctuated, format_punctuated_multiline, format_token_reference,
try_format_punctuated,
},
trivia::{
strip_leading_trivia, strip_trailing_trivia, strip_trivia, FormatTriviaType,
UpdateLeadingTrivia, UpdateTrailingTrivia, UpdateTrivia,
},
trivia_util::{
self, prepend_newline_indent, CommentSearch, GetLeadingTrivia, GetTrailingTrivia,
HasInlineComments,
},
},
shape::Shape,
};
pub fn calculate_hang_level(expression: &Expression) -> Option<usize> {
match expression {
Expression::Value { value, .. } => match **value {
Value::ParenthesesExpression(_) => None,
_ => Some(1),
},
Expression::UnaryOperator { expression, .. } => calculate_hang_level(expression),
_ => Some(1),
}
}
pub fn hang_punctuated_list(
ctx: &Context,
punctuated: &Punctuated<Expression>,
shape: Shape,
) -> Punctuated<Expression> {
assert!(punctuated.len() == 1);
let mut output = Punctuated::new();
for (idx, pair) in punctuated.pairs().enumerate() {
let mut value =
hang_expression(ctx, pair.value(), shape, calculate_hang_level(pair.value()));
if idx != 0 {
value =
value.update_leading_trivia(FormatTriviaType::Append(vec![create_indent_trivia(
ctx, shape,
)]));
}
output.push(Pair::new(
value,
pair.punctuation().map(|x| {
fmt_symbol!(ctx, x, ",", shape).update_trailing_trivia(FormatTriviaType::Append(
vec![create_newline_trivia(ctx)],
))
}),
));
}
output
}
pub fn hang_equal_token(
ctx: &Context,
equal_token: &TokenReference,
shape: Shape,
indent_first_item: bool,
) -> TokenReference {
let mut equal_token_trailing_trivia = vec![create_newline_trivia(ctx)];
if indent_first_item {
equal_token_trailing_trivia.push(create_indent_trivia(
ctx,
shape.increment_additional_indent(),
))
}
let equal_token_trailing_trivia = equal_token
.trailing_trivia()
.filter(|x| trivia_util::trivia_is_comment(x))
.flat_map(|x| vec![Token::new(TokenType::spaces(1)), x.to_owned()])
.chain(equal_token_trailing_trivia.iter().map(|x| x.to_owned()))
.collect();
equal_token.update_trailing_trivia(FormatTriviaType::Replace(equal_token_trailing_trivia))
}
fn is_complex_function_call(function_call: &FunctionCall) -> bool {
let test_function_args = |function_args: &FunctionArgs| match function_args {
FunctionArgs::Parentheses { arguments, .. } => {
let mut complexity_count = 0;
for argument in arguments {
if let Expression::Value { value, .. } = argument {
match &**value {
Value::Function(_) => return true,
Value::TableConstructor(_) => complexity_count += 1,
_ => (),
}
}
}
complexity_count > 1
}
_ => false,
};
function_call.suffixes().any(|suffix| match suffix {
Suffix::Call(Call::AnonymousCall(function_args)) => test_function_args(function_args),
Suffix::Call(Call::MethodCall(method_call)) => test_function_args(method_call.args()),
_ => false,
})
}
fn prevent_equals_hanging(expression: &Expression) -> bool {
match expression {
Expression::Value { value, .. } => match &**value {
Value::Function(_) => true,
Value::FunctionCall(function_call) => is_complex_function_call(function_call),
#[cfg(feature = "luau")]
Value::IfExpression(_) => true,
_ => false,
},
_ => false,
}
}
fn attempt_assignment_tactics(
ctx: &Context,
expressions: &Punctuated<Expression>,
shape: Shape,
equal_token: TokenReference,
) -> (Punctuated<Expression>, TokenReference) {
if expressions.len() > 1 {
let hanging_equal_token = hang_equal_token(ctx, &equal_token, shape, true);
let hanging_shape = shape.reset().increment_additional_indent();
let expr_list = format_punctuated(
ctx,
expressions,
hanging_shape.with_infinite_width(),
format_expression,
);
if trivia_util::punctuated_inline_comments(expressions, true)
|| hanging_shape
.take_first_line(&strip_trivia(&expr_list))
.over_budget()
{
let multiline_expr = format_punctuated_multiline(
ctx,
expressions,
hanging_shape,
format_expression,
None,
);
let mut output_expr = Punctuated::new();
for (idx, (formatted, original)) in
multiline_expr.into_pairs().zip(expressions).enumerate()
{
let shape = hanging_shape.reset();
if formatted.value().has_inline_comments()
|| shape
.take_first_line(&strip_leading_trivia(formatted.value()))
.over_budget()
{
output_expr.push(formatted.map(|_| {
let expression =
hang_expression(ctx, original, shape, calculate_hang_level(original));
if idx == 0 {
expression
} else {
trivia_util::prepend_newline_indent(ctx, &expression, shape)
}
}))
} else {
output_expr.push(formatted);
}
}
(output_expr, hanging_equal_token)
} else {
(expr_list, hanging_equal_token)
}
} else {
let expression = expressions.iter().next().unwrap();
if trivia_util::token_contains_comments(&equal_token)
|| expression.has_leading_comments(CommentSearch::Single)
{
let equal_token = hang_equal_token(ctx, &equal_token, shape, false);
let shape = shape.reset().increment_additional_indent();
let expression = if strip_trivia(expression).has_inline_comments() {
hang_expression(ctx, expression, shape, None)
} else {
format_expression(ctx, expression, shape)
};
let (expression, leading_comments) = trivia_util::take_leading_comments(&expression);
let leading_comments = leading_comments
.iter()
.flat_map(|x| {
vec![
create_indent_trivia(ctx, shape),
x.to_owned(),
create_newline_trivia(ctx),
]
})
.chain(std::iter::once(create_indent_trivia(ctx, shape)))
.collect();
let expression =
expression.update_leading_trivia(FormatTriviaType::Replace(leading_comments));
let expr_list = std::iter::once(Pair::new(expression, None)).collect();
return (expr_list, equal_token);
}
let expr_list = format_punctuated(ctx, expressions, shape, format_expression);
let formatting_shape = shape.take_first_line(&strip_trailing_trivia(&expr_list));
if trivia_util::can_hang_expression(expression) {
let hanging_expr_list = hang_punctuated_list(ctx, expressions, shape);
let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list));
if expression.has_inline_comments()
|| hanging_shape.used_width() < formatting_shape.used_width()
{
(hanging_expr_list, equal_token)
} else {
(expr_list, equal_token)
}
} else if prevent_equals_hanging(expression) {
(expr_list, equal_token)
} else {
let hanging_equal_token = hang_equal_token(ctx, &equal_token, shape, true);
let equal_token_shape = shape.reset().increment_additional_indent();
let hanging_equal_token_expr_list =
format_punctuated(ctx, expressions, equal_token_shape, format_expression);
let equal_token_shape = equal_token_shape
.take_first_line(&strip_trailing_trivia(&hanging_equal_token_expr_list));
if !equal_token_shape.over_budget()
&& format!("{hanging_equal_token_expr_list}").lines().count() + 1 < format!("{expr_list}").lines().count()
|| formatting_shape.over_budget()
{
(hanging_equal_token_expr_list, hanging_equal_token)
} else {
(expr_list, equal_token)
}
}
}
}
pub fn format_assignment_no_trivia(
ctx: &Context,
assignment: &Assignment,
mut shape: Shape,
) -> Assignment {
let contains_comments = trivia_util::token_contains_comments(assignment.equal_token())
|| trivia_util::punctuated_inline_comments(assignment.expressions(), true);
let mut var_list = try_format_punctuated(
ctx,
assignment.variables(),
shape.with_infinite_width(),
format_var,
Some(1),
);
let mut equal_token = fmt_symbol!(ctx, assignment.equal_token(), " = ", shape);
let mut expr_list = format_punctuated(
ctx,
assignment.expressions(),
shape.with_infinite_width(),
format_expression,
);
if var_list.has_trailing_comments(trivia_util::CommentSearch::Single) {
const EQUAL_TOKEN_LEN: usize = "= ".len();
shape = shape
.reset()
.increment_additional_indent()
.add_width(EQUAL_TOKEN_LEN);
equal_token = prepend_newline_indent(ctx, &equal_token, shape);
}
let singleline_shape = shape
+ (strip_leading_trivia(&var_list).to_string().len()
+ 3
+ strip_trailing_trivia(&expr_list).to_string().len());
if contains_comments || singleline_shape.over_budget() {
var_list = try_format_punctuated(ctx, assignment.variables(), shape, format_var, Some(1));
let shape = shape + (strip_leading_trivia(&var_list).to_string().len() + 3);
let (new_expr_list, new_equal_token) =
attempt_assignment_tactics(ctx, assignment.expressions(), shape, equal_token);
expr_list = new_expr_list;
equal_token = new_equal_token;
}
Assignment::new(var_list, expr_list).with_equal_token(equal_token)
}
pub fn format_assignment(ctx: &Context, assignment: &Assignment, shape: Shape) -> Assignment {
let leading_trivia = vec![create_indent_trivia(ctx, shape)];
let trailing_trivia = vec![create_newline_trivia(ctx)];
format_assignment_no_trivia(ctx, assignment, shape).update_trivia(
FormatTriviaType::Append(leading_trivia),
FormatTriviaType::Append(trailing_trivia),
)
}
fn format_local_no_assignment(
ctx: &Context,
assignment: &LocalAssignment,
shape: Shape,
) -> LocalAssignment {
let local_token = fmt_symbol!(ctx, assignment.local_token(), "local ", shape);
let shape = shape + 6; let name_list = try_format_punctuated(
ctx,
assignment.names(),
shape,
format_token_reference,
Some(1),
);
#[cfg(feature = "lua54")]
let attributes = assignment
.attributes()
.map(|x| x.map(|attribute| format_attribute(ctx, attribute, shape)))
.collect();
#[cfg(feature = "luau")]
let type_specifiers: Vec<Option<TypeSpecifier>> = assignment
.type_specifiers()
.map(|x| x.map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)))
.collect();
let local_assignment = LocalAssignment::new(name_list);
#[cfg(feature = "lua54")]
let local_assignment = local_assignment.with_attributes(attributes);
#[cfg(feature = "luau")]
let local_assignment = local_assignment.with_type_specifiers(type_specifiers);
local_assignment
.with_local_token(local_token)
.with_equal_token(None)
.with_expressions(Punctuated::new())
}
pub fn format_local_assignment_no_trivia(
ctx: &Context,
assignment: &LocalAssignment,
mut shape: Shape,
) -> LocalAssignment {
if assignment.expressions().is_empty() {
format_local_no_assignment(ctx, assignment, shape)
} else {
let contains_comments = assignment
.equal_token()
.map_or(false, trivia_util::token_contains_comments)
|| trivia_util::punctuated_inline_comments(assignment.expressions(), true);
let local_token = fmt_symbol!(ctx, assignment.local_token(), "local ", shape);
let mut name_list = try_format_punctuated(
ctx,
assignment.names(),
shape.with_infinite_width(),
format_token_reference,
Some(1),
);
let mut equal_token = fmt_symbol!(ctx, assignment.equal_token().unwrap(), " = ", shape);
let mut expr_list = format_punctuated(
ctx,
assignment.expressions(),
shape.with_infinite_width(),
format_expression,
);
#[cfg(feature = "lua54")]
let attributes: Vec<Option<_>> = assignment
.attributes()
.map(|x| x.map(|attribute| format_attribute(ctx, attribute, shape)))
.collect();
#[cfg(feature = "luau")]
let type_specifiers: Vec<Option<TypeSpecifier>> = assignment
.type_specifiers()
.map(|x| x.map(|type_specifier| format_type_specifier(ctx, type_specifier, shape)))
.collect();
#[allow(unused_mut)]
let mut type_specifier_len = 0;
#[cfg(feature = "lua54")]
{
type_specifier_len += attributes.iter().fold(0, |acc, x| {
acc + x.as_ref().map_or(0, |y| y.to_string().len())
});
}
#[cfg(feature = "luau")]
{
type_specifier_len += type_specifiers.iter().fold(0, |acc, x| {
acc + x.as_ref().map_or(0, |y| y.to_string().len())
});
}
if name_list.has_trailing_comments(trivia_util::CommentSearch::Single) {
const EQUAL_TOKEN_LEN: usize = "= ".len();
shape = shape
.reset()
.increment_additional_indent()
.add_width(EQUAL_TOKEN_LEN);
equal_token = prepend_newline_indent(ctx, &equal_token, shape);
}
let singleline_shape = shape
+ (strip_leading_trivia(&name_list).to_string().len()
+ 6 + 3 + type_specifier_len
+ strip_trailing_trivia(&expr_list).to_string().len());
if contains_comments || singleline_shape.over_budget() {
name_list = try_format_punctuated(
ctx,
assignment.names(),
shape,
format_token_reference,
Some(1),
);
let shape = shape
+ (strip_leading_trivia(&name_list).to_string().len() + 6 + 3 + type_specifier_len);
let (new_expr_list, new_equal_token) =
attempt_assignment_tactics(ctx, assignment.expressions(), shape, equal_token);
expr_list = new_expr_list;
equal_token = new_equal_token;
}
let local_assignment = LocalAssignment::new(name_list);
#[cfg(feature = "lua54")]
let local_assignment = local_assignment.with_attributes(attributes);
#[cfg(feature = "luau")]
let local_assignment = local_assignment.with_type_specifiers(type_specifiers);
local_assignment
.with_local_token(local_token)
.with_equal_token(Some(equal_token))
.with_expressions(expr_list)
}
}
pub fn format_local_assignment(
ctx: &Context,
assignment: &LocalAssignment,
shape: Shape,
) -> LocalAssignment {
let leading_trivia = vec![create_indent_trivia(ctx, shape)];
let trailing_trivia = vec![create_newline_trivia(ctx)];
format_local_assignment_no_trivia(ctx, assignment, shape).update_trivia(
FormatTriviaType::Append(leading_trivia),
FormatTriviaType::Append(trailing_trivia),
)
}