Skip to content

Commit 4b7e90d

Browse files
authored
Configuration option to preserve blank lines at start of block (#4303)
* Configuration option to preserve blank lines at start of block Adds the option for - Functions - Control flow blocks - Expression statements (i.e. `let x = { ... };`) - Traits - Trait Impls - Mods - Foreign Mods If I am missing a kind of item, let me know :) Closes #2868 * fixup! Configuration option to preserve blank lines at start of block * Simplify `advance_to_first_block_item` * fixup! Simplify `advance_to_first_block_item`
1 parent 98efebd commit 4b7e90d

File tree

8 files changed

+938
-23
lines changed

8 files changed

+938
-23
lines changed

Configurations.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,35 @@ pub enum Foo {}
18091809
pub enum Foo {}
18101810
```
18111811

1812+
## `preserve_block_start_blank_lines`
1813+
1814+
Preserves blanks lines at the start of the block. Note that this will preserve newlines, but strip
1815+
any other whitespace on those lines.
1816+
1817+
- **Default value**: `false`
1818+
- **Possible values**: `true`, `false`
1819+
- **Stable**: No
1820+
1821+
#### `false` (default):
1822+
1823+
```rust
1824+
fn say_hi() {
1825+
println!("hi");
1826+
}
1827+
```
1828+
1829+
#### `true`:
1830+
1831+
```rust
1832+
fn say_hi() {
1833+
1834+
1835+
println!("hi");
1836+
}
1837+
```
1838+
1839+
###
1840+
18121841
## `overflow_delimited_expr`
18131842

18141843
When structs, slices, arrays, and block/array-like macros are used as the last

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ create_config! {
130130
"Write an item and its attribute on the same line \
131131
if their combined width is below a threshold";
132132
format_generated_files: bool, false, false, "Format generated files";
133+
preserve_block_start_blank_lines: bool, false, false, "Preserve blank lines at the start of \
134+
blocks.";
133135

134136
// Options that can change the source code beyond whitespace/blocks (somewhat linty things)
135137
merge_derives: bool, true, true, "Merge multiple `#[derive(...)]` into a single one";
@@ -617,6 +619,7 @@ blank_lines_lower_bound = 0
617619
edition = "2018"
618620
inline_attribute_width = 0
619621
format_generated_files = false
622+
preserve_block_start_blank_lines = false
620623
merge_derives = true
621624
use_try_shorthand = false
622625
use_field_init_shorthand = false

src/formatting/items.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ enum BodyElement<'a> {
229229
ForeignItem(&'a ast::ForeignItem),
230230
}
231231

232+
impl BodyElement<'_> {
233+
pub(crate) fn span(&self) -> Span {
234+
match self {
235+
BodyElement::ForeignItem(fi) => fi.span(),
236+
}
237+
}
238+
}
239+
232240
/// Represents a fn's signature.
233241
pub(crate) struct FnSig<'a> {
234242
decl: &'a ast::FnDecl,
@@ -325,6 +333,11 @@ impl<'a> FmtVisitor<'a> {
325333
let indent_str = self.block_indent.to_string(self.config);
326334
self.push_str(&indent_str);
327335
} else {
336+
let first_non_ws = item.body.first().map(|s| s.span().lo());
337+
if let Some(opening_nls) = self.advance_to_first_block_item(first_non_ws) {
338+
self.push_str(&opening_nls);
339+
}
340+
328341
for item in &item.body {
329342
self.format_body_element(item);
330343
}
@@ -851,6 +864,14 @@ pub(crate) fn format_impl(
851864
visitor.block_indent = item_indent;
852865
visitor.last_pos = lo + BytePos(open_pos as u32);
853866

867+
let first_non_ws = inner_attributes(&item.attrs)
868+
.first()
869+
.map(|a| a.span.lo())
870+
.or_else(|| items.first().map(|i| i.span().lo()));
871+
if let Some(opening_nls) = visitor.advance_to_first_block_item(first_non_ws) {
872+
result.push_str(&opening_nls);
873+
}
874+
854875
visitor.visit_attrs(&item.attrs, ast::AttrStyle::Inner);
855876
visitor.visit_impl_items(items);
856877

@@ -1210,6 +1231,12 @@ pub(crate) fn format_trait(
12101231
visitor.block_indent = offset.block_only().block_indent(context.config);
12111232
visitor.last_pos = block_span.lo() + BytePos(open_pos as u32);
12121233

1234+
if let Some(opening_nls) =
1235+
visitor.advance_to_first_block_item(trait_items.first().map(|i| i.span().lo()))
1236+
{
1237+
result.push_str(&opening_nls);
1238+
}
1239+
12131240
for item in trait_items {
12141241
visitor.visit_trait_item(item);
12151242
}

src/formatting/visitor.rs

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -182,32 +182,68 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
182182
}
183183
}
184184

185-
/// Remove spaces between the opening brace and the first statement or the inner attribute
186-
/// of the block.
187-
fn trim_spaces_after_opening_brace(
185+
/// Advances the position of the visitor to the first statement or the inner attribute of a
186+
/// block.
187+
///
188+
/// If `preserve_block_start_blank_lines` is true, the return value contains the newlines that
189+
/// should be preserved.
190+
pub(crate) fn advance_to_first_block_item(
188191
&mut self,
189-
b: &ast::Block,
190-
inner_attrs: Option<&[ast::Attribute]>,
191-
) {
192-
if let Some(first_stmt) = b.stmts.first() {
193-
let hi = inner_attrs
194-
.and_then(|attrs| inner_attributes(attrs).first().map(|attr| attr.span.lo()))
195-
.unwrap_or_else(|| first_stmt.span().lo());
196-
let missing_span = self.next_span(hi);
197-
let snippet = self.snippet(missing_span);
198-
let len = CommentCodeSlices::new(snippet)
199-
.next()
200-
.and_then(|(kind, _, s)| {
201-
if kind == CodeCharKind::Normal {
202-
s.rfind('\n')
203-
} else {
204-
None
192+
first_item_pos: Option<BytePos>,
193+
) -> Option<String> {
194+
let missing_span = first_item_pos.map(|pos| self.next_span(pos))?;
195+
let snippet = self.snippet(missing_span);
196+
197+
let len = CommentCodeSlices::new(snippet)
198+
.next()
199+
.and_then(|(kind, _, s)| {
200+
if kind == CodeCharKind::Normal {
201+
s.rfind('\n')
202+
} else {
203+
None
204+
}
205+
});
206+
if let Some(len) = len {
207+
self.last_pos = self.last_pos + BytePos::from_usize(len);
208+
}
209+
210+
if self.config.preserve_block_start_blank_lines() {
211+
// First we need to find the span to look for blank lines in. This is either the
212+
// - span between the opening brace and first item, or
213+
// - span between the opening brace and a comment before the first item
214+
// We do this so that we get a span of contiguous whitespace, which makes processing the
215+
// blank lines easier.
216+
let blank_lines_snippet = if let Some(hi) = self
217+
.snippet_provider
218+
.span_to_snippet(missing_span)
219+
.and_then(|s| s.find('/'))
220+
{
221+
self.snippet_provider.span_to_snippet(mk_sp(
222+
missing_span.lo(),
223+
missing_span.lo() + BytePos::from_usize(hi),
224+
))
225+
} else {
226+
self.snippet_provider.span_to_snippet(missing_span)
227+
};
228+
229+
if let Some(snippet) = blank_lines_snippet {
230+
if snippet.find('\n') != snippet.rfind('\n') {
231+
let mut lines = snippet.lines().map(&str::trim);
232+
lines.next(); // Eat block-opening newline
233+
lines.next_back(); // Eat newline before the first item
234+
let mut result = String::new();
235+
while let Some("") = lines.next() {
236+
result.push('\n');
205237
}
206-
});
207-
if let Some(len) = len {
208-
self.last_pos = self.last_pos + BytePos::from_usize(len);
238+
if !result.is_empty() {
239+
return Some(result);
240+
}
241+
}
242+
} else {
243+
debug!("Failed to preserve blank lines for {:?}", missing_span);
209244
}
210245
}
246+
None
211247
}
212248

213249
pub(crate) fn visit_block(
@@ -227,7 +263,13 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
227263
self.last_pos = self.last_pos + brace_compensation;
228264
self.block_indent = self.block_indent.block_indent(self.config);
229265
self.push_str("{");
230-
self.trim_spaces_after_opening_brace(b, inner_attrs);
266+
267+
let first_non_ws = inner_attrs
268+
.and_then(|attrs| attrs.first().map(|attr| attr.span.lo()))
269+
.or_else(|| b.stmts.first().map(|s| s.span().lo()));
270+
if let Some(opening_nls) = self.advance_to_first_block_item(first_non_ws) {
271+
self.push_str(&opening_nls);
272+
}
231273

232274
// Format inner attributes if available.
233275
if let Some(attrs) = inner_attrs {
@@ -953,6 +995,18 @@ impl<'b, 'a: 'b> FmtVisitor<'a> {
953995
} else {
954996
self.last_pos = mod_lo;
955997
self.block_indent = self.block_indent.block_indent(self.config);
998+
999+
let first_non_ws = inner_attributes(attrs)
1000+
.first()
1001+
.map(|attr| attr.span.lo())
1002+
.or_else(|| m.items.first().map(|s| s.span().lo()));
1003+
if let Some(opening_nls) = self.advance_to_first_block_item(first_non_ws) {
1004+
self.push_str(&opening_nls);
1005+
if attrs.is_empty() {
1006+
self.push_str("\n");
1007+
}
1008+
}
1009+
9561010
self.visit_attrs(attrs, ast::AttrStyle::Inner);
9571011
self.walk_mod_items(m);
9581012
let missing_span = self.next_span(m.inner.hi() - BytePos(1));

0 commit comments

Comments
 (0)