diff --git a/src/formatters/assignment.rs b/src/formatters/assignment.rs index 3983e7c4..c284c9d2 100644 --- a/src/formatters/assignment.rs +++ b/src/formatters/assignment.rs @@ -18,7 +18,7 @@ use crate::{ context::{create_indent_trivia, create_newline_trivia, Context}, fmt_symbol, formatters::{ - expression::{format_expression, format_var, hang_expression}, + expression::{format_expression, format_var, hang_expression, is_brackets_string}, general::{ format_punctuated, format_punctuated_multiline, format_token_reference, try_format_punctuated, @@ -201,6 +201,29 @@ pub fn hang_at_equals_due_to_comments( (equal_token, expression) } +/// Whether the expression is a `..` chain whose leftmost operand is a multiline long string (`[[ ]]` / `[=[ ]=]`) +fn is_multiline_long_string_concat(expression: &Expression) -> bool { + fn leftmost(expression: &Expression) -> &Expression { + match expression { + Expression::BinaryOperator { lhs, .. } => leftmost(lhs), + _ => expression, + } + } + + matches!( + expression, + Expression::BinaryOperator { + binop: full_moon::ast::BinOp::TwoDots(_), + .. + } + ) && match leftmost(expression) { + Expression::String(token) => { + is_brackets_string(leftmost(expression)) && token.token().to_string().contains('\n') + } + _ => false, + } +} + /// Attempts different formatting tactics on an expression list being assigned (`= foo, bar`), to find the best /// formatting output. fn attempt_assignment_tactics( @@ -297,8 +320,14 @@ fn attempt_assignment_tactics( let hanging_expr_list = hang_punctuated_list(ctx, expressions, shape); let hanging_shape = shape.take_first_line(&strip_trivia(&hanging_expr_list)); + // A `..` chain led by a multiline long string (e.g. `[[ ]]`) has a short first line, so the + // comparison above never hangs it, even when a later line is over budget. Only hang if that fixes it if expression.has_inline_comments() || hanging_shape.used_width() < formatting_shape.used_width() + // Check the formatted expression (e.g. with parentheses removed), as that is what is output + || (is_multiline_long_string_concat(expr_list.iter().next().unwrap()) + && shape.test_over_budget(&strip_trailing_trivia(&expr_list)) + && !shape.test_over_budget(&strip_trivia(&hanging_expr_list))) { (hanging_expr_list, equal_token) } else { diff --git a/tests/inputs/concat-multiline-string-chain.lua b/tests/inputs/concat-multiline-string-chain.lua new file mode 100644 index 00000000..c2a2f42a --- /dev/null +++ b/tests/inputs/concat-multiline-string-chain.lua @@ -0,0 +1,30 @@ +local x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] +local y = "dropdown[4,0.75;6.4;contributor_grouping;" .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. ";true]" +local z = [[ +short]] .. foo .. [[bar]] +x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] +-- regression: function-led multiline RHS must be unchanged +local f = (function() + return 1 +end)() .. "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .. b +-- regression: call-led multiline RHS (reduced from jest-lua TestScheduler) must be unchanged +local function g() + local message = ("%sYour test suite is leaking memory. Please ensure all references are cleaned.\n"):format( + tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) + ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." +end +-- regression: over-long line inside the long string itself; hanging cannot help, so must be unchanged +local s = [[ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +]] .. a .. b +-- parenthesised long string lead: parentheses are removed, so the chain must hang in a single pass +local p = ([[ +foo +]]) .. "dropdown[4,0.75;6.4;contributor_grouping;" .. S("Group by name") .. "," .. S("Group by contribution") .. ";" .. mail.selected_idxs.contributor_grouping[name] .. [[;true] +]] diff --git a/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap new file mode 100644 index 00000000..bcf0c13e --- /dev/null +++ b/tests/snapshots/tests__standard@concat-multiline-string-chain.lua.snap @@ -0,0 +1,61 @@ +--- +source: tests/tests.rs +expression: "format(&contents, LuaVersion::Lua51)" +input_file: tests/inputs/concat-multiline-string-chain.lua +--- +local x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] +local y = "dropdown[4,0.75;6.4;contributor_grouping;" + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. ";true]" +local z = [[ +short]] .. foo .. [[bar]] +x = [[ +box[0,0;1,1;] +dropdown[4,0.75;6.4;contributor_grouping;]] + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] +-- regression: function-led multiline RHS must be unchanged +local f = (function() + return 1 +end)() .. "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .. b +-- regression: call-led multiline RHS (reduced from jest-lua TestScheduler) must be unchanged +local function g() + local message = ("%sYour test suite is leaking memory. Please ensure all references are cleaned.\n"):format( + tostring(chalk.red:bold("EXPERIMENTAL FEATURE!\n")) + ) .. "\n" .. "There is a number of things that can leak memory:\n" .. " - Async operations that have not finished (e.g. fs.readFile).\n" .. " - Keeping references to the global scope." +end +-- regression: over-long line inside the long string itself; hanging cannot help, so must be unchanged +local s = [[ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +]] .. a .. b +-- parenthesised long string lead: parentheses are removed, so the chain must hang in a single pass +local p = [[ +foo +]] + .. "dropdown[4,0.75;6.4;contributor_grouping;" + .. S("Group by name") + .. "," + .. S("Group by contribution") + .. ";" + .. mail.selected_idxs.contributor_grouping[name] + .. [[;true] +]] + diff --git a/tests/test_concat_idempotence.rs b/tests/test_concat_idempotence.rs new file mode 100644 index 00000000..6bffaa77 --- /dev/null +++ b/tests/test_concat_idempotence.rs @@ -0,0 +1,10 @@ +use stylua_lib::{format_code, Config, OutputVerification}; + +#[test] +fn test_concat_multiline_string_chain_idempotent() { + let input = include_str!("inputs/concat-multiline-string-chain.lua"); + let format = + |code: &str| format_code(code, Config::default(), None, OutputVerification::None).unwrap(); + let first = format(input); + assert_eq!(format(&first), first); +}