Skip to content

Commit 17fea25

Browse files
committed
Add option to preserve newline gaps for blocks
1 parent 145a7c3 commit 17fea25

File tree

13 files changed

+242
-38
lines changed

13 files changed

+242
-38
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ StyLua only offers the following options:
293293
| `quote_style` | `AutoPreferDouble` | Quote style for string literals. Possible options: `AutoPreferDouble`, `AutoPreferSingle`, `ForceDouble`, `ForceSingle`. `AutoPrefer` styles will prefer the specified quote style, but fall back to the alternative if it has fewer string escapes. `Force` styles always use the specified style regardless of escapes. |
294294
| `call_parentheses` | `Always` | Whether parentheses should be applied on function calls with a single string/table argument. Possible options: `Always`, `NoSingleString`, `NoSingleTable`, `None`, `Input`. `Always` applies parentheses in all cases. `NoSingleString` omits parentheses on calls with a single string argument. Similarly, `NoSingleTable` omits parentheses on calls with a single table argument. `None` omits parentheses in both cases. Note: parentheses are still kept in situations where removal can lead to obscurity (e.g. `foo "bar".setup -> foo("bar").setup`, since the index is on the call result, not the string). `Input` removes all automation and preserves parentheses only if they were present in input code: consistency is not enforced. |
295295
| `space_after_function_names` | `Never` | Specify whether to add a space between the function name and parentheses. Possible options: `Never`, `Definitions`, `Calls`, or `Always` |
296+
| `block_newline_gaps` | `Never` | Specify whether to preserve leading and trailing newline gaps for blocks. Possible options: `Never`, `Preserve` |
296297
| `collapse_simple_statement` | `Never` | Specify whether to collapse simple statements. Possible options: `Never`, `FunctionOnly`, `ConditionalOnly`, or `Always` |
297298

298299
Default `stylua.toml`, note you do not need to explicitly specify each option if you want to use the defaults:
@@ -307,6 +308,7 @@ quote_style = "AutoPreferDouble"
307308
call_parentheses = "Always"
308309
collapse_simple_statement = "Never"
309310
space_after_function_names = "Never"
311+
block_newline_gaps = "Never"
310312

311313
[sort_requires]
312314
enabled = false

src/cli/opt.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use clap::{ArgEnum, StructOpt};
22
use std::path::PathBuf;
33
use stylua_lib::{
4-
CallParenType, CollapseSimpleStatement, IndentType, LineEndings, LuaVersion, QuoteStyle,
5-
SpaceAfterFunctionNames,
4+
BlockNewlineGaps, CallParenType, CollapseSimpleStatement, IndentType, LineEndings, LuaVersion,
5+
QuoteStyle, SpaceAfterFunctionNames,
66
};
77

88
lazy_static::lazy_static! {
@@ -186,6 +186,9 @@ pub struct FormatOpts {
186186
/// Specify whether to collapse simple statements.
187187
#[structopt(long, arg_enum, ignore_case = true)]
188188
pub collapse_simple_statement: Option<ArgCollapseSimpleStatement>,
189+
/// Specify whether to preserve leading and trailing newline gaps for blocks.
190+
#[structopt(long, arg_enum, ignore_case = true)]
191+
pub preserve_block_newline_gaps: Option<ArgBlockNewlineGaps>,
189192
/// Enable requires sorting
190193
#[structopt(long)]
191194
pub sort_requires: bool,
@@ -272,6 +275,11 @@ convert_enum!(CollapseSimpleStatement, ArgCollapseSimpleStatement, {
272275
Always,
273276
});
274277

278+
convert_enum!(BlockNewlineGaps, ArgBlockNewlineGaps, {
279+
Never,
280+
Preserve,
281+
});
282+
275283
convert_enum!(SpaceAfterFunctionNames, ArgSpaceAfterFunctionNames, {
276284
Never,
277285
Definitions,

src/context.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
2-
shape::Shape, CallParenType, CollapseSimpleStatement, Config, IndentType, LineEndings,
3-
Range as FormatRange, SpaceAfterFunctionNames,
2+
shape::Shape, BlockNewlineGaps, CallParenType, CollapseSimpleStatement, Config, IndentType,
3+
LineEndings, Range as FormatRange, SpaceAfterFunctionNames,
44
};
55
use full_moon::{
66
node::Node,
@@ -154,6 +154,14 @@ impl Context {
154154
CollapseSimpleStatement::ConditionalOnly | CollapseSimpleStatement::Always
155155
)
156156
}
157+
158+
pub fn should_preserve_leading_block_newline_gaps(&self) -> bool {
159+
matches!(self.config().block_newline_gaps, BlockNewlineGaps::Preserve)
160+
}
161+
162+
pub fn should_preserve_trailing_block_newline_gaps(&self) -> bool {
163+
matches!(self.config().block_newline_gaps, BlockNewlineGaps::Preserve)
164+
}
157165
}
158166

159167
/// Returns the relevant line ending string from the [`LineEndings`] enum

src/formatters/block.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ fn check_stmt_requires_semicolon(
535535
pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
536536
let mut ctx = *ctx;
537537
let mut formatted_statements: Vec<(Stmt, Option<TokenReference>)> = Vec::new();
538-
let mut found_first_stmt = false;
538+
let mut remove_next_stmt_leading_newlines = !ctx.should_preserve_leading_block_newline_gaps();
539539
let mut stmt_iterator = block.stmts_with_semicolon().peekable();
540540

541541
while let Some((stmt, semi)) = stmt_iterator.next() {
@@ -544,12 +544,12 @@ pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
544544
let shape = shape.reset();
545545
let mut stmt = format_stmt(&ctx, stmt, shape);
546546

547-
// If this is the first stmt, then remove any leading newlines
548-
if !found_first_stmt {
547+
// If this is the first stmt, and leading newlines should be removed, then remove them
548+
if remove_next_stmt_leading_newlines {
549549
if let FormatNode::Normal = ctx.should_format_node(&stmt) {
550550
stmt = stmt_remove_leading_newlines(stmt);
551551
}
552-
found_first_stmt = true;
552+
remove_next_stmt_leading_newlines = false;
553553
}
554554

555555
// If we have a semicolon, we need to push all the trailing trivia from the statement
@@ -611,8 +611,9 @@ pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
611611

612612
let shape = shape.reset();
613613
let mut last_stmt = format_last_stmt(&ctx, last_stmt, shape);
614-
// If this is the first stmt, then remove any leading newlines
615-
if !found_first_stmt && matches!(ctx.should_format_node(&last_stmt), FormatNode::Normal)
614+
// If this is the first stmt, and leading newlines should be removed, then remove them
615+
if remove_next_stmt_leading_newlines
616+
&& matches!(ctx.should_format_node(&last_stmt), FormatNode::Normal)
616617
{
617618
last_stmt = last_stmt_remove_leading_newlines(last_stmt);
618619
}

src/formatters/general.rs

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ pub fn format_end_token(
679679
shape: Shape,
680680
) -> TokenReference {
681681
// Indent any comments leading a token, as these comments are technically part of the function body block
682-
let formatted_leading_trivia: Vec<Token> = load_token_trivia(
682+
let mut formatted_leading_trivia: Vec<Token> = load_token_trivia(
683683
ctx,
684684
current_token.leading_trivia().collect(),
685685
FormatTokenType::LeadingTrivia,
@@ -698,37 +698,39 @@ pub fn format_end_token(
698698
shape,
699699
);
700700

701-
// Special case for block end tokens:
702-
// We will reverse the leading trivia, and keep removing any newlines we find, until we find something else, then we stop.
703-
// This is to remove unnecessary newlines at the end of the block.
704-
let mut iter = formatted_leading_trivia.iter().rev().peekable();
705-
706-
let mut formatted_leading_trivia = Vec::new();
707-
let mut stop_removal = false;
708-
while let Some(x) = iter.next() {
709-
match x.token_type() {
710-
TokenType::Whitespace { ref characters } => {
711-
if !stop_removal
712-
&& characters.contains('\n')
713-
&& !matches!(
714-
iter.peek().map(|x| x.token_kind()),
715-
Some(TokenKind::SingleLineComment) | Some(TokenKind::MultiLineComment)
716-
)
717-
{
718-
continue;
719-
} else {
701+
if !ctx.should_preserve_trailing_block_newline_gaps() {
702+
// Special case for block end tokens:
703+
// We will reverse the leading trivia, and keep removing any newlines we find, until we find something else, then we stop.
704+
// This is to remove unnecessary newlines at the end of the block.
705+
let original_leading_trivia = std::mem::take(&mut formatted_leading_trivia);
706+
let mut iter = original_leading_trivia.iter().cloned().rev().peekable();
707+
708+
let mut stop_removal = false;
709+
while let Some(x) = iter.next() {
710+
match x.token_type() {
711+
TokenType::Whitespace { ref characters } => {
712+
if !stop_removal
713+
&& characters.contains('\n')
714+
&& !matches!(
715+
iter.peek().map(|x| x.token_kind()),
716+
Some(TokenKind::SingleLineComment) | Some(TokenKind::MultiLineComment)
717+
)
718+
{
719+
continue;
720+
} else {
721+
formatted_leading_trivia.push(x.to_owned());
722+
}
723+
}
724+
_ => {
720725
formatted_leading_trivia.push(x.to_owned());
726+
stop_removal = true; // Stop removing newlines once we have seen some sort of comment
721727
}
722728
}
723-
_ => {
724-
formatted_leading_trivia.push(x.to_owned());
725-
stop_removal = true; // Stop removing newlines once we have seen some sort of comment
726-
}
727729
}
728-
}
729730

730-
// Need to reverse the vector since we reversed the iterator
731-
formatted_leading_trivia.reverse();
731+
// Need to reverse the vector since we reversed the iterator
732+
formatted_leading_trivia.reverse();
733+
}
732734

733735
TokenReference::new(
734736
formatted_leading_trivia,

src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ pub enum CollapseSimpleStatement {
153153
Always,
154154
}
155155

156+
/// If blocks should be allowed to have leading and trailing newline gaps.
157+
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
158+
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
159+
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
160+
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
161+
pub enum BlockNewlineGaps {
162+
/// Never allow leading or trailing newline gaps
163+
#[default]
164+
Never,
165+
/// Preserve both leading and trailing newline gaps if present in input
166+
Preserve,
167+
}
168+
156169
/// An optional formatting range.
157170
/// If provided, only content within these boundaries (inclusive) will be formatted.
158171
/// Both boundaries are optional, and are given as byte offsets from the beginning of the file.
@@ -254,6 +267,10 @@ pub struct Config {
254267
/// if set to [`CollapseSimpleStatement::None`] structures are never collapsed.
255268
/// if set to [`CollapseSimpleStatement::FunctionOnly`] then simple functions (i.e., functions with a single laststmt) can be collapsed
256269
pub collapse_simple_statement: CollapseSimpleStatement,
270+
/// Whether we should allow blocks to preserve leading and trailing newline gaps.
271+
/// if set to [`BlockNewlineGaps::Never`] then newline gaps are never allowed at the start or end of blocks.
272+
/// if set to [`BlockNewlineGaps::Preserve`] then newline gaps are preserved at the start and end of blocks.
273+
pub block_newline_gaps: BlockNewlineGaps,
257274
/// Configuration for the sort requires codemod
258275
pub sort_requires: SortRequiresConfig,
259276
/// Whether we should include a space between the function name and arguments.
@@ -287,6 +304,7 @@ impl Default for Config {
287304
collapse_simple_statement: CollapseSimpleStatement::default(),
288305
sort_requires: SortRequiresConfig::default(),
289306
space_after_function_names: SpaceAfterFunctionNames::default(),
307+
block_newline_gaps: BlockNewlineGaps::default(),
290308
}
291309
}
292310
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function foo()
2+
3+
local x = 1
4+
5+
6+
return true
7+
8+
end
9+
10+
function bar()
11+
12+
13+
return
14+
15+
16+
end
17+
18+
do
19+
20+
-- comment
21+
local x = 1
22+
23+
24+
local foo = bar
25+
26+
-- comment
27+
28+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
local function noop() -- comment
2+
end
3+
4+
function noop()
5+
-- comment
6+
end
7+
8+
call(function()
9+
-- comment
10+
11+
end)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
if
2+
this_is == very_long_variable_name
3+
and to_ensure_that == it_is_broken_into
4+
and multiple_lines == in_order_to_see_how_that_looks
5+
then
6+
return false
7+
elseif
8+
this_is == very_long_variable_name
9+
and to_ensure_that == it_is_broken_into
10+
and multiple_lines == in_order_to_see_how_that_looks
11+
then
12+
local only_a_gap_of_one_newline_is_preserved_below = 1
13+
local hurray = true
14+
15+
16+
17+
18+
elseif
19+
this_is == very_long_variable_name
20+
and to_ensure_that == it_is_broken_into
21+
and multiple_lines == in_order_to_see_how_that_looks
22+
then
23+
24+
25+
26+
27+
28+
local only_a_gap_of_one_newline_is_preserved_above = 1
29+
local hurray = true
30+
else
31+
32+
return also_preserved_in_else_blocks
33+
end

tests/snapshots/[email protected]

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: tests/tests.rs
3+
expression: "format_code(&contents,\n Config {\n preserve_block_newline_gaps: PreserveBlockNewlineGaps::Always,\n ..Config::default()\n }, None, OutputVerification::None).unwrap()"
4+
input_file: tests/inputs-preserve-block-newline-gaps/block-empty-lines.lua
5+
---
6+
function foo()
7+
8+
local x = 1
9+
10+
return true
11+
12+
end
13+
14+
function bar()
15+
16+
return
17+
18+
end
19+
20+
do
21+
22+
-- comment
23+
local x = 1
24+
25+
local foo = bar
26+
27+
-- comment
28+
29+
end

tests/snapshots/[email protected]

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: tests/tests.rs
3+
expression: "format_code(&contents,\n Config {\n preserve_block_newline_gaps: PreserveBlockNewlineGaps::Always,\n ..Config::default()\n }, None, OutputVerification::None).unwrap()"
4+
input_file: tests/inputs-preserve-block-newline-gaps/empty-function.lua
5+
---
6+
local function noop() -- comment
7+
end
8+
9+
function noop()
10+
-- comment
11+
end
12+
13+
call(function()
14+
-- comment
15+
16+
end)

tests/snapshots/[email protected]

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
source: tests/tests.rs
3+
expression: "format_code(&contents,\n Config {\n preserve_block_newline_gaps: PreserveBlockNewlineGaps::Always,\n ..Config::default()\n }, None, OutputVerification::None).unwrap()"
4+
input_file: tests/inputs-preserve-block-newline-gaps/long-elseif-chain.lua
5+
---
6+
if
7+
this_is == very_long_variable_name
8+
and to_ensure_that == it_is_broken_into
9+
and multiple_lines == in_order_to_see_how_that_looks
10+
then
11+
return false
12+
elseif
13+
this_is == very_long_variable_name
14+
and to_ensure_that == it_is_broken_into
15+
and multiple_lines == in_order_to_see_how_that_looks
16+
then
17+
local only_a_gap_of_one_newline_is_preserved_below = 1
18+
local hurray = true
19+
20+
elseif
21+
this_is == very_long_variable_name
22+
and to_ensure_that == it_is_broken_into
23+
and multiple_lines == in_order_to_see_how_that_looks
24+
then
25+
26+
local only_a_gap_of_one_newline_is_preserved_above = 1
27+
local hurray = true
28+
else
29+
30+
return also_preserved_in_else_blocks
31+
end

0 commit comments

Comments
 (0)