Skip to content

Commit 834fda0

Browse files
Merge #8388
8388: Autoclose blocks when typing `{` r=jonas-schievink a=jonas-schievink Co-authored-by: Jonas Schievink <jonasschievink@gmail.com>
2 parents 09b730d + d789cf8 commit 834fda0

File tree

3 files changed

+154
-17
lines changed

3 files changed

+154
-17
lines changed

crates/ide/src/typing.rs

Lines changed: 153 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,27 @@ use ide_db::{
2222
use syntax::{
2323
algo::find_node_at_offset,
2424
ast::{self, edit::IndentLevel, AstToken},
25-
AstNode, SourceFile,
25+
AstNode, Parse, SourceFile,
2626
SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
2727
TextRange, TextSize,
2828
};
2929

30-
use text_edit::TextEdit;
30+
use text_edit::{Indel, TextEdit};
3131

3232
use crate::SourceChange;
3333

3434
pub(crate) use on_enter::on_enter;
3535

36-
pub(crate) const TRIGGER_CHARS: &str = ".=>";
36+
// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
37+
pub(crate) const TRIGGER_CHARS: &str = ".=>{";
3738

3839
// Feature: On Typing Assists
3940
//
4041
// Some features trigger on typing certain characters:
4142
//
4243
// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
4344
// - typing `.` in a chain method call auto-indents
45+
// - typing `{` in front of an expression inserts a closing `}` after the expression
4446
//
4547
// VS Code::
4648
//
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed(
5759
position: FilePosition,
5860
char_typed: char,
5961
) -> Option<SourceChange> {
60-
assert!(TRIGGER_CHARS.contains(char_typed));
61-
let file = &db.parse(position.file_id).tree();
62-
assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
62+
if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
63+
return None;
64+
}
65+
let file = &db.parse(position.file_id);
66+
if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) {
67+
return None;
68+
}
6369
let edit = on_char_typed_inner(file, position.offset, char_typed)?;
6470
Some(SourceChange::from_text_edit(position.file_id, edit))
6571
}
6672

67-
fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
68-
assert!(TRIGGER_CHARS.contains(char_typed));
73+
fn on_char_typed_inner(
74+
file: &Parse<SourceFile>,
75+
offset: TextSize,
76+
char_typed: char,
77+
) -> Option<TextEdit> {
78+
if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
79+
return None;
80+
}
6981
match char_typed {
70-
'.' => on_dot_typed(file, offset),
71-
'=' => on_eq_typed(file, offset),
72-
'>' => on_arrow_typed(file, offset),
82+
'.' => on_dot_typed(&file.tree(), offset),
83+
'=' => on_eq_typed(&file.tree(), offset),
84+
'>' => on_arrow_typed(&file.tree(), offset),
85+
'{' => on_opening_brace_typed(file, offset),
7386
_ => unreachable!(),
7487
}
7588
}
7689

90+
/// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
91+
/// block.
92+
fn on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit> {
93+
if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some('{')) {
94+
return None;
95+
}
96+
97+
let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?;
98+
99+
// Remove the `{` to get a better parse tree, and reparse
100+
let file = file.reparse(&Indel::delete(brace_token.text_range()));
101+
102+
let mut expr: ast::Expr = find_node_at_offset(file.tree().syntax(), offset)?;
103+
if expr.syntax().text_range().start() != offset {
104+
return None;
105+
}
106+
107+
// Enclose the outermost expression starting at `offset`
108+
while let Some(parent) = expr.syntax().parent() {
109+
if parent.text_range().start() != expr.syntax().text_range().start() {
110+
break;
111+
}
112+
113+
match ast::Expr::cast(parent) {
114+
Some(parent) => expr = parent,
115+
None => break,
116+
}
117+
}
118+
119+
// If it's a statement in a block, we don't know how many statements should be included
120+
if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
121+
return None;
122+
}
123+
124+
// Insert `}` right after the expression.
125+
Some(TextEdit::insert(expr.syntax().text_range().end() + TextSize::of("{"), "}".to_string()))
126+
}
127+
77128
/// Returns an edit which should be applied after `=` was typed. Primarily,
78129
/// this works when adding `let =`.
79130
// FIXME: use a snippet completion instead of this hack here.
80131
fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
81-
assert_eq!(file.syntax().text().char_at(offset), Some('='));
132+
if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
133+
return None;
134+
}
82135
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
83136
if let_stmt.semicolon_token().is_some() {
84137
return None;
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100153

101154
/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102155
fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
103-
assert_eq!(file.syntax().text().char_at(offset), Some('.'));
156+
if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
157+
return None;
158+
}
104159
let whitespace =
105160
file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
106161

@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
129184
/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130185
fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
131186
let file_text = file.syntax().text();
132-
assert_eq!(file_text.char_at(offset), Some('>'));
187+
if !stdx::always!(file_text.char_at(offset) == Some('>')) {
188+
return None;
189+
}
133190
let after_arrow = offset + TextSize::of('>');
134191
if file_text.char_at(after_arrow) != Some('{') {
135192
return None;
@@ -152,7 +209,7 @@ mod tests {
152209
let edit = TextEdit::insert(offset, char_typed.to_string());
153210
edit.apply(&mut before);
154211
let parse = SourceFile::parse(&before);
155-
on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
212+
on_char_typed_inner(&parse, offset, char_typed).map(|it| {
156213
it.apply(&mut before);
157214
before.to_string()
158215
})
@@ -373,4 +430,85 @@ fn main() {
373430
fn adds_space_after_return_type() {
374431
type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
375432
}
433+
434+
#[test]
435+
fn adds_closing_brace() {
436+
type_char(
437+
'{',
438+
r#"
439+
fn f() { match () { _ => $0() } }
440+
"#,
441+
r#"
442+
fn f() { match () { _ => {()} } }
443+
"#,
444+
);
445+
type_char(
446+
'{',
447+
r#"
448+
fn f() { $0() }
449+
"#,
450+
r#"
451+
fn f() { {()} }
452+
"#,
453+
);
454+
type_char(
455+
'{',
456+
r#"
457+
fn f() { let x = $0(); }
458+
"#,
459+
r#"
460+
fn f() { let x = {()}; }
461+
"#,
462+
);
463+
type_char(
464+
'{',
465+
r#"
466+
fn f() { let x = $0a.b(); }
467+
"#,
468+
r#"
469+
fn f() { let x = {a.b()}; }
470+
"#,
471+
);
472+
type_char(
473+
'{',
474+
r#"
475+
const S: () = $0();
476+
fn f() {}
477+
"#,
478+
r#"
479+
const S: () = {()};
480+
fn f() {}
481+
"#,
482+
);
483+
type_char(
484+
'{',
485+
r#"
486+
const S: () = $0a.b();
487+
fn f() {}
488+
"#,
489+
r#"
490+
const S: () = {a.b()};
491+
fn f() {}
492+
"#,
493+
);
494+
type_char(
495+
'{',
496+
r#"
497+
fn f() {
498+
match x {
499+
0 => $0(),
500+
1 => (),
501+
}
502+
}
503+
"#,
504+
r#"
505+
fn f() {
506+
match x {
507+
0 => {()},
508+
1 => (),
509+
}
510+
}
511+
"#,
512+
);
513+
}
376514
}

crates/rust-analyzer/src/caps.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
5757
document_range_formatting_provider: None,
5858
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
5959
first_trigger_character: "=".to_string(),
60-
more_trigger_character: Some(vec![".".to_string(), ">".to_string()]),
60+
more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]),
6161
}),
6262
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
6363
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),

crates/rust-analyzer/src/handlers.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,6 @@ pub(crate) fn handle_on_enter(
231231
Ok(Some(edit))
232232
}
233233

234-
// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
235234
pub(crate) fn handle_on_type_formatting(
236235
snap: GlobalStateSnapshot,
237236
params: lsp_types::DocumentOnTypeFormattingParams,

0 commit comments

Comments
 (0)