Skip to content

Commit b8a6ea5

Browse files
committed
feat: make join lines behavior configurable
closes #9492
1 parent 91bfa4b commit b8a6ea5

File tree

6 files changed

+171
-84
lines changed

6 files changed

+171
-84
lines changed

crates/ide/src/join_lines.rs

Lines changed: 113 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,32 @@ use syntax::{
1212

1313
use text_edit::{TextEdit, TextEditBuilder};
1414

15+
pub struct JoinLinesConfig {
16+
pub join_else_if: bool,
17+
pub remove_trailing_comma: bool,
18+
pub unwrap_trivial_blocks: bool,
19+
}
20+
1521
// Feature: Join Lines
1622
//
1723
// Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces.
1824
//
25+
// See
26+
// https://user-images.githubusercontent.com/1711539/124515923-4504e800-dde9-11eb-8d58-d97945a1a785.gif[this gif]
27+
// for the cases handled specially by joined lines.
28+
//
1929
// |===
2030
// | Editor | Action Name
2131
//
2232
// | VS Code | **Rust Analyzer: Join lines**
2333
// |===
2434
//
2535
// image::https://user-images.githubusercontent.com/48062697/113020661-b6922200-917a-11eb-87c4-b75acc028f11.gif[]
26-
pub(crate) fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
36+
pub(crate) fn join_lines(
37+
config: &JoinLinesConfig,
38+
file: &SourceFile,
39+
range: TextRange,
40+
) -> TextEdit {
2741
let range = if range.is_empty() {
2842
let syntax = file.syntax();
2943
let text = syntax.text().slice(range.start()..);
@@ -40,15 +54,20 @@ pub(crate) fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
4054
match file.syntax().covering_element(range) {
4155
NodeOrToken::Node(node) => {
4256
for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
43-
remove_newlines(&mut edit, &token, range)
57+
remove_newlines(config, &mut edit, &token, range)
4458
}
4559
}
46-
NodeOrToken::Token(token) => remove_newlines(&mut edit, &token, range),
60+
NodeOrToken::Token(token) => remove_newlines(config, &mut edit, &token, range),
4761
};
4862
edit.finish()
4963
}
5064

51-
fn remove_newlines(edit: &mut TextEditBuilder, token: &SyntaxToken, range: TextRange) {
65+
fn remove_newlines(
66+
config: &JoinLinesConfig,
67+
edit: &mut TextEditBuilder,
68+
token: &SyntaxToken,
69+
range: TextRange,
70+
) {
5271
let intersection = match range.intersect(token.text_range()) {
5372
Some(range) => range,
5473
None => return,
@@ -60,12 +79,17 @@ fn remove_newlines(edit: &mut TextEditBuilder, token: &SyntaxToken, range: TextR
6079
let pos: TextSize = (pos as u32).into();
6180
let offset = token.text_range().start() + range.start() + pos;
6281
if !edit.invalidates_offset(offset) {
63-
remove_newline(edit, token, offset);
82+
remove_newline(config, edit, token, offset);
6483
}
6584
}
6685
}
6786

68-
fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) {
87+
fn remove_newline(
88+
config: &JoinLinesConfig,
89+
edit: &mut TextEditBuilder,
90+
token: &SyntaxToken,
91+
offset: TextSize,
92+
) {
6993
if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
7094
let n_spaces_after_line_break = {
7195
let suff = &token.text()[TextRange::new(
@@ -102,24 +126,66 @@ fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextS
102126
_ => return,
103127
};
104128

105-
if is_trailing_comma(prev.kind(), next.kind()) {
106-
// Removes: trailing comma, newline (incl. surrounding whitespace)
107-
edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
108-
return;
129+
if config.remove_trailing_comma && prev.kind() == T![,] {
130+
match next.kind() {
131+
T![')'] | T![']'] => {
132+
// Removes: trailing comma, newline (incl. surrounding whitespace)
133+
edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
134+
return;
135+
}
136+
T!['}'] => {
137+
// Removes: comma, newline (incl. surrounding whitespace)
138+
let space = if let Some(left) = prev.prev_sibling_or_token() {
139+
compute_ws(left.kind(), next.kind())
140+
} else {
141+
" "
142+
};
143+
edit.replace(
144+
TextRange::new(prev.text_range().start(), token.text_range().end()),
145+
space.to_string(),
146+
);
147+
return;
148+
}
149+
_ => (),
150+
}
109151
}
110152

111-
if prev.kind() == T![,] && next.kind() == T!['}'] {
112-
// Removes: comma, newline (incl. surrounding whitespace)
113-
let space = if let Some(left) = prev.prev_sibling_or_token() {
114-
compute_ws(left.kind(), next.kind())
115-
} else {
116-
" "
117-
};
118-
edit.replace(
119-
TextRange::new(prev.text_range().start(), token.text_range().end()),
120-
space.to_string(),
121-
);
122-
return;
153+
if config.join_else_if {
154+
if let (Some(prev), Some(_next)) = (as_if_expr(&prev), as_if_expr(&next)) {
155+
match prev.else_token() {
156+
Some(_) => cov_mark::hit!(join_two_ifs_with_existing_else),
157+
None => {
158+
cov_mark::hit!(join_two_ifs);
159+
edit.replace(token.text_range(), " else ".to_string());
160+
return;
161+
}
162+
}
163+
}
164+
}
165+
166+
if config.unwrap_trivial_blocks {
167+
// Special case that turns something like:
168+
//
169+
// ```
170+
// my_function({$0
171+
// <some-expr>
172+
// })
173+
// ```
174+
//
175+
// into `my_function(<some-expr>)`
176+
if join_single_expr_block(edit, token).is_some() {
177+
return;
178+
}
179+
// ditto for
180+
//
181+
// ```
182+
// use foo::{$0
183+
// bar
184+
// };
185+
// ```
186+
if join_single_use_tree(edit, token).is_some() {
187+
return;
188+
}
123189
}
124190

125191
if let (Some(_), Some(next)) = (
@@ -134,40 +200,6 @@ fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextS
134200
return;
135201
}
136202

137-
if let (Some(prev), Some(_next)) = (as_if_expr(&prev), as_if_expr(&next)) {
138-
match prev.else_token() {
139-
Some(_) => cov_mark::hit!(join_two_ifs_with_existing_else),
140-
None => {
141-
cov_mark::hit!(join_two_ifs);
142-
edit.replace(token.text_range(), " else ".to_string());
143-
return;
144-
}
145-
}
146-
}
147-
148-
// Special case that turns something like:
149-
//
150-
// ```
151-
// my_function({$0
152-
// <some-expr>
153-
// })
154-
// ```
155-
//
156-
// into `my_function(<some-expr>)`
157-
if join_single_expr_block(edit, token).is_some() {
158-
return;
159-
}
160-
// ditto for
161-
//
162-
// ```
163-
// use foo::{$0
164-
// bar
165-
// };
166-
// ```
167-
if join_single_use_tree(edit, token).is_some() {
168-
return;
169-
}
170-
171203
// Remove newline but add a computed amount of whitespace characters
172204
edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
173205
}
@@ -208,10 +240,6 @@ fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Opti
208240
Some(())
209241
}
210242

211-
fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
212-
matches!((left, right), (T![,], T![')'] | T![']']))
213-
}
214-
215243
fn as_if_expr(element: &SyntaxElement) -> Option<ast::IfExpr> {
216244
let mut node = element.as_node()?.clone();
217245
if let Some(stmt) = ast::ExprStmt::cast(node.clone()) {
@@ -251,11 +279,17 @@ mod tests {
251279
use super::*;
252280

253281
fn check_join_lines(ra_fixture_before: &str, ra_fixture_after: &str) {
282+
let config = JoinLinesConfig {
283+
join_else_if: true,
284+
remove_trailing_comma: true,
285+
unwrap_trivial_blocks: true,
286+
};
287+
254288
let (before_cursor_pos, before) = extract_offset(ra_fixture_before);
255289
let file = SourceFile::parse(&before).ok().unwrap();
256290

257291
let range = TextRange::empty(before_cursor_pos);
258-
let result = join_lines(&file, range);
292+
let result = join_lines(&config, &file, range);
259293

260294
let actual = {
261295
let mut actual = before;
@@ -269,6 +303,24 @@ mod tests {
269303
assert_eq_text!(ra_fixture_after, &actual);
270304
}
271305

306+
fn check_join_lines_sel(ra_fixture_before: &str, ra_fixture_after: &str) {
307+
let config = JoinLinesConfig {
308+
join_else_if: true,
309+
remove_trailing_comma: true,
310+
unwrap_trivial_blocks: true,
311+
};
312+
313+
let (sel, before) = extract_range(ra_fixture_before);
314+
let parse = SourceFile::parse(&before);
315+
let result = join_lines(&config, &parse.tree(), sel);
316+
let actual = {
317+
let mut actual = before;
318+
result.apply(&mut actual);
319+
actual
320+
};
321+
assert_eq_text!(ra_fixture_after, &actual);
322+
}
323+
272324
#[test]
273325
fn test_join_lines_comma() {
274326
check_join_lines(
@@ -657,18 +709,6 @@ fn foo() {
657709
);
658710
}
659711

660-
fn check_join_lines_sel(ra_fixture_before: &str, ra_fixture_after: &str) {
661-
let (sel, before) = extract_range(ra_fixture_before);
662-
let parse = SourceFile::parse(&before);
663-
let result = join_lines(&parse.tree(), sel);
664-
let actual = {
665-
let mut actual = before;
666-
result.apply(&mut actual);
667-
actual
668-
};
669-
assert_eq_text!(ra_fixture_after, &actual);
670-
}
671-
672712
#[test]
673713
fn test_join_lines_selection_fn_args() {
674714
check_join_lines_sel(

crates/ide/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,11 @@ mod view_item_tree;
5757
use std::sync::Arc;
5858

5959
use cfg::CfgOptions;
60-
61-
use ide_db::base_db::{
62-
salsa::{self, ParallelDatabase},
63-
Env, FileLoader, FileSet, SourceDatabase, VfsPath,
64-
};
6560
use ide_db::{
61+
base_db::{
62+
salsa::{self, ParallelDatabase},
63+
Env, FileLoader, FileSet, SourceDatabase, VfsPath,
64+
},
6665
symbol_index::{self, FileSymbol},
6766
LineIndexDatabase,
6867
};
@@ -80,6 +79,7 @@ pub use crate::{
8079
highlight_related::HighlightedRange,
8180
hover::{HoverAction, HoverConfig, HoverDocFormat, HoverGotoTypeData, HoverResult},
8281
inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
82+
join_lines::JoinLinesConfig,
8383
markup::Markup,
8484
move_item::Direction,
8585
prime_caches::PrimeCachesProgress,
@@ -308,10 +308,10 @@ impl Analysis {
308308

309309
/// Returns an edit to remove all newlines in the range, cleaning up minor
310310
/// stuff like trailing commas.
311-
pub fn join_lines(&self, frange: FileRange) -> Cancellable<TextEdit> {
311+
pub fn join_lines(&self, config: &JoinLinesConfig, frange: FileRange) -> Cancellable<TextEdit> {
312312
self.with_db(|db| {
313313
let parse = db.parse(frange.file_id);
314-
join_lines::join_lines(&parse.tree(), frange.range)
314+
join_lines::join_lines(&config, &parse.tree(), frange.range)
315315
})
316316
}
317317

crates/rust-analyzer/src/config.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{ffi::OsString, iter, path::PathBuf};
1212
use flycheck::FlycheckConfig;
1313
use ide::{
1414
AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, HoverDocFormat,
15-
InlayHintsConfig,
15+
InlayHintsConfig, JoinLinesConfig,
1616
};
1717
use ide_db::helpers::{
1818
insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
@@ -186,6 +186,13 @@ config_data! {
186186
/// Whether to show inlay type hints for variables.
187187
inlayHints_typeHints: bool = "true",
188188

189+
/// Join lines inserts else between consecutive ifs.
190+
joinLines_joinElseIf: bool = "true",
191+
/// Join lines removes trailing commas.
192+
joinLines_removeTrailingComma: bool = "true",
193+
/// Join lines unwraps trivial blocks.
194+
joinLines_unwrapTrivialBlock: bool = "true",
195+
189196
/// Whether to show `Debug` lens. Only applies when
190197
/// `#rust-analyzer.lens.enable#` is set.
191198
lens_debug: bool = "true",
@@ -752,6 +759,13 @@ impl Config {
752759
insert_use: self.insert_use_config(),
753760
}
754761
}
762+
pub fn join_lines(&self) -> JoinLinesConfig {
763+
JoinLinesConfig {
764+
join_else_if: self.data.joinLines_joinElseIf,
765+
remove_trailing_comma: self.data.joinLines_removeTrailingComma,
766+
unwrap_trivial_blocks: self.data.joinLines_unwrapTrivialBlock,
767+
}
768+
}
755769
pub fn call_info_full(&self) -> bool {
756770
self.data.callInfo_full
757771
}

crates/rust-analyzer/src/handlers.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,21 +233,24 @@ pub(crate) fn handle_join_lines(
233233
params: lsp_ext::JoinLinesParams,
234234
) -> Result<Vec<lsp_types::TextEdit>> {
235235
let _p = profile::span("handle_join_lines");
236+
237+
let config = snap.config.join_lines();
236238
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
237239
let line_index = snap.file_line_index(file_id)?;
240+
238241
let mut res = TextEdit::default();
239242
for range in params.ranges {
240243
let range = from_proto::text_range(&line_index, range);
241-
let edit = snap.analysis.join_lines(FileRange { file_id, range })?;
244+
let edit = snap.analysis.join_lines(&config, FileRange { file_id, range })?;
242245
match res.union(edit) {
243246
Ok(()) => (),
244247
Err(_edit) => {
245248
// just ignore overlapping edits
246249
}
247250
}
248251
}
249-
let res = to_proto::text_edit_vec(&line_index, res);
250-
Ok(res)
252+
253+
Ok(to_proto::text_edit_vec(&line_index, res))
251254
}
252255

253256
pub(crate) fn handle_on_enter(

0 commit comments

Comments
 (0)