Skip to content

Commit 96f2219

Browse files
committed
Add option to preserve newline gaps for blocks
1 parent 049bb7c commit 96f2219

File tree

12 files changed

+256
-36
lines changed

12 files changed

+256
-36
lines changed

src/cli/opt.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use clap::{ArgEnum, StructOpt};
22
use std::path::PathBuf;
3-
use stylua_lib::{CallParenType, CollapseSimpleStatement, IndentType, LineEndings, QuoteStyle};
3+
use stylua_lib::{
4+
CallParenType, CollapseSimpleStatement, IndentType, LineEndings, PreserveBlockNewlineGaps,
5+
QuoteStyle,
6+
};
47

58
lazy_static::lazy_static! {
69
static ref NUM_CPUS: String = num_cpus::get().to_string();
@@ -180,6 +183,9 @@ pub struct FormatOpts {
180183
/// Specify whether to collapse simple statements.
181184
#[structopt(long, arg_enum, ignore_case = true)]
182185
pub collapse_simple_statement: Option<ArgCollapseSimpleStatement>,
186+
/// Specify whether to preserve leading and trailing newline gaps for blocks.
187+
#[structopt(long, arg_enum, ignore_case = true)]
188+
pub preserve_block_newline_gaps: Option<ArgPreserveBlockNewlineGaps>,
183189
/// Enable requires sorting
184190
#[structopt(long)]
185191
pub sort_requires: bool,
@@ -250,6 +256,13 @@ convert_enum!(CollapseSimpleStatement, ArgCollapseSimpleStatement, {
250256
Always,
251257
});
252258

259+
convert_enum!(PreserveBlockNewlineGaps, ArgPreserveBlockNewlineGaps, {
260+
Never,
261+
AlwaysLeading,
262+
AlwaysTrailing,
263+
Always,
264+
});
265+
253266
#[cfg(test)]
254267
mod tests {
255268
use super::Opt;

src/context.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
shape::Shape, CallParenType, CollapseSimpleStatement, Config, IndentType, LineEndings,
3-
Range as FormatRange,
3+
PreserveBlockNewlineGaps, Range as FormatRange,
44
};
55
use full_moon::{
66
node::Node,
@@ -154,6 +154,20 @@ impl Context {
154154
CollapseSimpleStatement::ConditionalOnly | CollapseSimpleStatement::Always
155155
)
156156
}
157+
158+
pub fn should_preserve_leading_block_newline_gaps(&self) -> bool {
159+
matches!(
160+
self.config().preserve_block_newline_gaps,
161+
PreserveBlockNewlineGaps::Always | PreserveBlockNewlineGaps::AlwaysLeading
162+
)
163+
}
164+
165+
pub fn should_preserve_trailing_block_newline_gaps(&self) -> bool {
166+
matches!(
167+
self.config().preserve_block_newline_gaps,
168+
PreserveBlockNewlineGaps::Always | PreserveBlockNewlineGaps::AlwaysTrailing
169+
)
170+
}
157171
}
158172

159173
/// 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
@@ -452,7 +452,7 @@ fn last_stmt_remove_leading_newlines(last_stmt: LastStmt) -> LastStmt {
452452
pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
453453
let mut ctx = *ctx;
454454
let mut formatted_statements: Vec<(Stmt, Option<TokenReference>)> = Vec::new();
455-
let mut found_first_stmt = false;
455+
let mut remove_next_stmt_leading_newlines = !ctx.should_preserve_leading_block_newline_gaps();
456456
let mut stmt_iterator = block.stmts_with_semicolon().peekable();
457457

458458
while let Some((stmt, semi)) = stmt_iterator.next() {
@@ -461,12 +461,12 @@ pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
461461
let shape = shape.reset();
462462
let mut stmt = format_stmt(&ctx, stmt, shape);
463463

464-
// If this is the first stmt, then remove any leading newlines
465-
if !found_first_stmt {
464+
// If this is the first stmt, and leading newlines should be removed, then remove them
465+
if remove_next_stmt_leading_newlines {
466466
if let FormatNode::Normal = ctx.should_format_node(&stmt) {
467467
stmt = stmt_remove_leading_newlines(stmt);
468468
}
469-
found_first_stmt = true;
469+
remove_next_stmt_leading_newlines = false;
470470
}
471471

472472
// Need to check next statement if it is a function call, with a parameters expression as the prefix
@@ -563,8 +563,9 @@ pub fn format_block(ctx: &Context, block: &Block, shape: Shape) -> Block {
563563

564564
let shape = shape.reset();
565565
let mut last_stmt = format_last_stmt(&ctx, last_stmt, shape);
566-
// If this is the first stmt, then remove any leading newlines
567-
if !found_first_stmt && matches!(ctx.should_format_node(&last_stmt), FormatNode::Normal)
566+
// If this is the first stmt, and leading newlines should be removed, then remove them
567+
if remove_next_stmt_leading_newlines
568+
&& matches!(ctx.should_format_node(&last_stmt), FormatNode::Normal)
568569
{
569570
last_stmt = last_stmt_remove_leading_newlines(last_stmt);
570571
}

src/formatters/general.rs

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ pub fn format_end_token(
643643
shape: Shape,
644644
) -> TokenReference {
645645
// Indent any comments leading a token, as these comments are technically part of the function body block
646-
let formatted_leading_trivia: Vec<Token> = load_token_trivia(
646+
let mut formatted_leading_trivia: Vec<Token> = load_token_trivia(
647647
ctx,
648648
current_token.leading_trivia().collect(),
649649
FormatTokenType::LeadingTrivia,
@@ -662,37 +662,39 @@ pub fn format_end_token(
662662
shape,
663663
);
664664

665-
// Special case for block end tokens:
666-
// We will reverse the leading trivia, and keep removing any newlines we find, until we find something else, then we stop.
667-
// This is to remove unnecessary newlines at the end of the block.
668-
let mut iter = formatted_leading_trivia.iter().rev().peekable();
669-
670-
let mut formatted_leading_trivia = Vec::new();
671-
let mut stop_removal = false;
672-
while let Some(x) = iter.next() {
673-
match x.token_type() {
674-
TokenType::Whitespace { ref characters } => {
675-
if !stop_removal
676-
&& characters.contains('\n')
677-
&& !matches!(
678-
iter.peek().map(|x| x.token_kind()),
679-
Some(TokenKind::SingleLineComment) | Some(TokenKind::MultiLineComment)
680-
)
681-
{
682-
continue;
683-
} else {
665+
if !ctx.should_preserve_trailing_block_newline_gaps() {
666+
// Special case for block end tokens:
667+
// We will reverse the leading trivia, and keep removing any newlines we find, until we find something else, then we stop.
668+
// This is to remove unnecessary newlines at the end of the block.
669+
let original_leading_trivia = std::mem::take(&mut formatted_leading_trivia);
670+
let mut iter = original_leading_trivia.iter().cloned().rev().peekable();
671+
672+
let mut stop_removal = false;
673+
while let Some(x) = iter.next() {
674+
match x.token_type() {
675+
TokenType::Whitespace { ref characters } => {
676+
if !stop_removal
677+
&& characters.contains('\n')
678+
&& !matches!(
679+
iter.peek().map(|x| x.token_kind()),
680+
Some(TokenKind::SingleLineComment) | Some(TokenKind::MultiLineComment)
681+
)
682+
{
683+
continue;
684+
} else {
685+
formatted_leading_trivia.push(x.to_owned());
686+
}
687+
}
688+
_ => {
684689
formatted_leading_trivia.push(x.to_owned());
690+
stop_removal = true; // Stop removing newlines once we have seen some sort of comment
685691
}
686692
}
687-
_ => {
688-
formatted_leading_trivia.push(x.to_owned());
689-
stop_removal = true; // Stop removing newlines once we have seen some sort of comment
690-
}
691693
}
692-
}
693694

694-
// Need to reverse the vector since we reversed the iterator
695-
formatted_leading_trivia.reverse();
695+
// Need to reverse the vector since we reversed the iterator
696+
formatted_leading_trivia.reverse();
697+
}
696698

697699
TokenReference::new(
698700
formatted_leading_trivia,

src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,23 @@ pub enum CollapseSimpleStatement {
9494
Always,
9595
}
9696

97+
/// If blocks should be allowed to have leading and trailing newline gaps.
98+
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Deserialize)]
99+
#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen"), wasm_bindgen)]
100+
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
101+
#[cfg_attr(feature = "fromstr", derive(strum::EnumString))]
102+
pub enum PreserveBlockNewlineGaps {
103+
/// Never allow leading or trailing newline gaps
104+
#[default]
105+
Never,
106+
/// Always preserve leading newline gaps if present in input
107+
AlwaysLeading,
108+
/// Always preserve trailing newline gaps if present in input
109+
AlwaysTrailing,
110+
/// Always preserve both leading and trailing newline gaps if present in input
111+
Always,
112+
}
113+
97114
/// An optional formatting range.
98115
/// If provided, only content within these boundaries (inclusive) will be formatted.
99116
/// Both boundaries are optional, and are given as byte offsets from the beginning of the file.
@@ -176,6 +193,12 @@ pub struct Config {
176193
/// if set to [`CollapseSimpleStatement::None`] structures are never collapsed.
177194
/// if set to [`CollapseSimpleStatement::FunctionOnly`] then simple functions (i.e., functions with a single laststmt) can be collapsed
178195
pub collapse_simple_statement: CollapseSimpleStatement,
196+
/// Whether we should allow blocks to preserve leading and trailing newline gaps.
197+
/// if set to [`PreserveBlockNewlineGaps::Never`] then newline gaps are never allowed at the start or end of blocks.
198+
/// if set to [`PreserveBlockNewlineGaps::AlwaysLeading`] then newline gaps are preserved at the start blocks.
199+
/// if set to [`PreserveBlockNewlineGaps::AlwaysTrailing`] then newline gaps are preserved at the end of blocks.
200+
/// if set to [`PreserveBlockNewlineGaps::Always`] then newline gaps are preserved at the start and end of blocks.
201+
pub preserve_block_newline_gaps: PreserveBlockNewlineGaps,
179202
/// Configuration for the sort requires codemod
180203
pub sort_requires: SortRequiresConfig,
181204
}
@@ -346,6 +369,7 @@ impl Default for Config {
346369
call_parentheses: CallParenType::default(),
347370
collapse_simple_statement: CollapseSimpleStatement::default(),
348371
sort_requires: SortRequiresConfig::default(),
372+
preserve_block_newline_gaps: PreserveBlockNewlineGaps::default(),
349373
}
350374
}
351375
}
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)