aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-04-07 19:17:38 +0100
committerGitHub <[email protected]>2021-04-07 19:17:38 +0100
commit834fda0d285c323d0846bfd8d6d9739175cb35bc (patch)
treeb25dcb73f9a702f06dd2302a8837d04e610c2e52 /crates/ide
parent09b730d1e1b485ff582f3f237d39136ce50ea0cc (diff)
parentd789cf8f31b3f8281046a352a73e17e15ff3c224 (diff)
Merge #8388
8388: Autoclose blocks when typing `{` r=jonas-schievink a=jonas-schievink Co-authored-by: Jonas Schievink <[email protected]>
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/src/typing.rs168
1 files changed, 153 insertions, 15 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index 11408d445..1378048e5 100644
--- a/crates/ide/src/typing.rs
+++ b/crates/ide/src/typing.rs
@@ -22,18 +22,19 @@ use ide_db::{
22use syntax::{ 22use syntax::{
23 algo::find_node_at_offset, 23 algo::find_node_at_offset,
24 ast::{self, edit::IndentLevel, AstToken}, 24 ast::{self, edit::IndentLevel, AstToken},
25 AstNode, SourceFile, 25 AstNode, Parse, SourceFile,
26 SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR}, 26 SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
27 TextRange, TextSize, 27 TextRange, TextSize,
28}; 28};
29 29
30use text_edit::TextEdit; 30use text_edit::{Indel, TextEdit};
31 31
32use crate::SourceChange; 32use crate::SourceChange;
33 33
34pub(crate) use on_enter::on_enter; 34pub(crate) use on_enter::on_enter;
35 35
36pub(crate) const TRIGGER_CHARS: &str = ".=>"; 36// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
37pub(crate) const TRIGGER_CHARS: &str = ".=>{";
37 38
38// Feature: On Typing Assists 39// Feature: On Typing Assists
39// 40//
@@ -41,6 +42,7 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>";
41// 42//
42// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression 43// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
43// - typing `.` in a chain method call auto-indents 44// - typing `.` in a chain method call auto-indents
45// - typing `{` in front of an expression inserts a closing `}` after the expression
44// 46//
45// VS Code:: 47// VS Code::
46// 48//
@@ -57,28 +59,79 @@ pub(crate) fn on_char_typed(
57 position: FilePosition, 59 position: FilePosition,
58 char_typed: char, 60 char_typed: char,
59) -> Option<SourceChange> { 61) -> Option<SourceChange> {
60 assert!(TRIGGER_CHARS.contains(char_typed)); 62 if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
61 let file = &db.parse(position.file_id).tree(); 63 return None;
62 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); 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 }
63 let edit = on_char_typed_inner(file, position.offset, char_typed)?; 69 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
64 Some(SourceChange::from_text_edit(position.file_id, edit)) 70 Some(SourceChange::from_text_edit(position.file_id, edit))
65} 71}
66 72
67fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { 73fn on_char_typed_inner(
68 assert!(TRIGGER_CHARS.contains(char_typed)); 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 }
69 match char_typed { 81 match char_typed {
70 '.' => on_dot_typed(file, offset), 82 '.' => on_dot_typed(&file.tree(), offset),
71 '=' => on_eq_typed(file, offset), 83 '=' => on_eq_typed(&file.tree(), offset),
72 '>' => on_arrow_typed(file, offset), 84 '>' => on_arrow_typed(&file.tree(), offset),
85 '{' => on_opening_brace_typed(file, offset),
73 _ => unreachable!(), 86 _ => unreachable!(),
74 } 87 }
75} 88}
76 89
90/// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
91/// block.
92fn 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
77/// Returns an edit which should be applied after `=` was typed. Primarily, 128/// Returns an edit which should be applied after `=` was typed. Primarily,
78/// this works when adding `let =`. 129/// this works when adding `let =`.
79// FIXME: use a snippet completion instead of this hack here. 130// FIXME: use a snippet completion instead of this hack here.
80fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 131fn 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 }
82 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; 135 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
83 if let_stmt.semicolon_token().is_some() { 136 if let_stmt.semicolon_token().is_some() {
84 return None; 137 return None;
@@ -100,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100 153
101/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. 154/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
102fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 155fn 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 }
104 let whitespace = 159 let whitespace =
105 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; 160 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
106 161
@@ -129,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
129/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` 184/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
130fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 185fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
131 let file_text = file.syntax().text(); 186 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 }
133 let after_arrow = offset + TextSize::of('>'); 190 let after_arrow = offset + TextSize::of('>');
134 if file_text.char_at(after_arrow) != Some('{') { 191 if file_text.char_at(after_arrow) != Some('{') {
135 return None; 192 return None;
@@ -152,7 +209,7 @@ mod tests {
152 let edit = TextEdit::insert(offset, char_typed.to_string()); 209 let edit = TextEdit::insert(offset, char_typed.to_string());
153 edit.apply(&mut before); 210 edit.apply(&mut before);
154 let parse = SourceFile::parse(&before); 211 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| {
156 it.apply(&mut before); 213 it.apply(&mut before);
157 before.to_string() 214 before.to_string()
158 }) 215 })
@@ -373,4 +430,85 @@ fn main() {
373 fn adds_space_after_return_type() { 430 fn adds_space_after_return_type() {
374 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") 431 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
375 } 432 }
433
434 #[test]
435 fn adds_closing_brace() {
436 type_char(
437 '{',
438 r#"
439fn f() { match () { _ => $0() } }
440 "#,
441 r#"
442fn f() { match () { _ => {()} } }
443 "#,
444 );
445 type_char(
446 '{',
447 r#"
448fn f() { $0() }
449 "#,
450 r#"
451fn f() { {()} }
452 "#,
453 );
454 type_char(
455 '{',
456 r#"
457fn f() { let x = $0(); }
458 "#,
459 r#"
460fn f() { let x = {()}; }
461 "#,
462 );
463 type_char(
464 '{',
465 r#"
466fn f() { let x = $0a.b(); }
467 "#,
468 r#"
469fn f() { let x = {a.b()}; }
470 "#,
471 );
472 type_char(
473 '{',
474 r#"
475const S: () = $0();
476fn f() {}
477 "#,
478 r#"
479const S: () = {()};
480fn f() {}
481 "#,
482 );
483 type_char(
484 '{',
485 r#"
486const S: () = $0a.b();
487fn f() {}
488 "#,
489 r#"
490const S: () = {a.b()};
491fn f() {}
492 "#,
493 );
494 type_char(
495 '{',
496 r#"
497fn f() {
498 match x {
499 0 => $0(),
500 1 => (),
501 }
502}
503 "#,
504 r#"
505fn f() {
506 match x {
507 0 => {()},
508 1 => (),
509 }
510}
511 "#,
512 );
513 }
376} 514}