aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/typing.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/typing.rs')
-rw-r--r--crates/ide/src/typing.rs171
1 files changed, 156 insertions, 15 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index e10b7d98e..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//
@@ -49,33 +51,87 @@ pub(crate) const TRIGGER_CHARS: &str = ".=>";
49// ---- 51// ----
50// "editor.formatOnType": true, 52// "editor.formatOnType": true,
51// ---- 53// ----
54//
55// image::https://user-images.githubusercontent.com/48062697/113166163-69758500-923a-11eb-81ee-eb33ec380399.gif[]
56// image::https://user-images.githubusercontent.com/48062697/113171066-105c2000-923f-11eb-87ab-f4a263346567.gif[]
52pub(crate) fn on_char_typed( 57pub(crate) fn on_char_typed(
53 db: &RootDatabase, 58 db: &RootDatabase,
54 position: FilePosition, 59 position: FilePosition,
55 char_typed: char, 60 char_typed: char,
56) -> Option<SourceChange> { 61) -> Option<SourceChange> {
57 assert!(TRIGGER_CHARS.contains(char_typed)); 62 if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
58 let file = &db.parse(position.file_id).tree(); 63 return None;
59 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 }
60 let edit = on_char_typed_inner(file, position.offset, char_typed)?; 69 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
61 Some(SourceChange::from_text_edit(position.file_id, edit)) 70 Some(SourceChange::from_text_edit(position.file_id, edit))
62} 71}
63 72
64fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { 73fn on_char_typed_inner(
65 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 }
66 match char_typed { 81 match char_typed {
67 '.' => on_dot_typed(file, offset), 82 '.' => on_dot_typed(&file.tree(), offset),
68 '=' => on_eq_typed(file, offset), 83 '=' => on_eq_typed(&file.tree(), offset),
69 '>' => on_arrow_typed(file, offset), 84 '>' => on_arrow_typed(&file.tree(), offset),
85 '{' => on_opening_brace_typed(file, offset),
70 _ => unreachable!(), 86 _ => unreachable!(),
71 } 87 }
72} 88}
73 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
74/// Returns an edit which should be applied after `=` was typed. Primarily, 128/// Returns an edit which should be applied after `=` was typed. Primarily,
75/// this works when adding `let =`. 129/// this works when adding `let =`.
76// FIXME: use a snippet completion instead of this hack here. 130// FIXME: use a snippet completion instead of this hack here.
77fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 131fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
78 assert_eq!(file.syntax().text().char_at(offset), Some('=')); 132 if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
133 return None;
134 }
79 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)?;
80 if let_stmt.semicolon_token().is_some() { 136 if let_stmt.semicolon_token().is_some() {
81 return None; 137 return None;
@@ -97,7 +153,9 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
97 153
98/// 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.
99fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 155fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
100 assert_eq!(file.syntax().text().char_at(offset), Some('.')); 156 if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
157 return None;
158 }
101 let whitespace = 159 let whitespace =
102 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)?;
103 161
@@ -126,7 +184,9 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
126/// 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() -> { ... }`
127fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { 185fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
128 let file_text = file.syntax().text(); 186 let file_text = file.syntax().text();
129 assert_eq!(file_text.char_at(offset), Some('>')); 187 if !stdx::always!(file_text.char_at(offset) == Some('>')) {
188 return None;
189 }
130 let after_arrow = offset + TextSize::of('>'); 190 let after_arrow = offset + TextSize::of('>');
131 if file_text.char_at(after_arrow) != Some('{') { 191 if file_text.char_at(after_arrow) != Some('{') {
132 return None; 192 return None;
@@ -149,7 +209,7 @@ mod tests {
149 let edit = TextEdit::insert(offset, char_typed.to_string()); 209 let edit = TextEdit::insert(offset, char_typed.to_string());
150 edit.apply(&mut before); 210 edit.apply(&mut before);
151 let parse = SourceFile::parse(&before); 211 let parse = SourceFile::parse(&before);
152 on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { 212 on_char_typed_inner(&parse, offset, char_typed).map(|it| {
153 it.apply(&mut before); 213 it.apply(&mut before);
154 before.to_string() 214 before.to_string()
155 }) 215 })
@@ -370,4 +430,85 @@ fn main() {
370 fn adds_space_after_return_type() { 430 fn adds_space_after_return_type() {
371 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }") 431 type_char('>', "fn foo() -$0{ 92 }", "fn foo() -> { 92 }")
372 } 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 }
373} 514}