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.rs371
1 files changed, 260 insertions, 111 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index e10b7d98e..82c732390 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 })
@@ -162,8 +222,8 @@ mod tests {
162 assert_eq_text!(ra_fixture_after, &actual); 222 assert_eq_text!(ra_fixture_after, &actual);
163 } 223 }
164 224
165 fn type_char_noop(char_typed: char, before: &str) { 225 fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
166 let file_change = do_type_char(char_typed, before); 226 let file_change = do_type_char(char_typed, ra_fixture_before);
167 assert!(file_change.is_none()) 227 assert!(file_change.is_none())
168 } 228 }
169 229
@@ -180,16 +240,16 @@ mod tests {
180 // "); 240 // ");
181 type_char( 241 type_char(
182 '=', 242 '=',
183 r" 243 r#"
184fn foo() { 244fn foo() {
185 let foo $0 1 + 1 245 let foo $0 1 + 1
186} 246}
187", 247"#,
188 r" 248 r#"
189fn foo() { 249fn foo() {
190 let foo = 1 + 1; 250 let foo = 1 + 1;
191} 251}
192", 252"#,
193 ); 253 );
194 // do_check(r" 254 // do_check(r"
195 // fn foo() { 255 // fn foo() {
@@ -208,27 +268,27 @@ fn foo() {
208 fn indents_new_chain_call() { 268 fn indents_new_chain_call() {
209 type_char( 269 type_char(
210 '.', 270 '.',
211 r" 271 r#"
212 fn main() { 272fn main() {
213 xs.foo() 273 xs.foo()
214 $0 274 $0
215 } 275}
216 ", 276 "#,
217 r" 277 r#"
218 fn main() { 278fn main() {
219 xs.foo() 279 xs.foo()
220 . 280 .
221 } 281}
222 ", 282 "#,
223 ); 283 );
224 type_char_noop( 284 type_char_noop(
225 '.', 285 '.',
226 r" 286 r#"
227 fn main() { 287fn main() {
228 xs.foo() 288 xs.foo()
229 $0 289 $0
230 } 290}
231 ", 291 "#,
232 ) 292 )
233 } 293 }
234 294
@@ -237,26 +297,26 @@ fn foo() {
237 type_char( 297 type_char(
238 '.', 298 '.',
239 r" 299 r"
240 fn main() { 300fn main() {
241 xs.foo() 301 xs.foo()
242 $0; 302 $0;
243 } 303}
244 ",
245 r"
246 fn main() {
247 xs.foo()
248 .;
249 }
250 ", 304 ",
305 r#"
306fn main() {
307 xs.foo()
308 .;
309}
310 "#,
251 ); 311 );
252 type_char_noop( 312 type_char_noop(
253 '.', 313 '.',
254 r" 314 r#"
255 fn main() { 315fn main() {
256 xs.foo() 316 xs.foo()
257 $0; 317 $0;
258 } 318}
259 ", 319 "#,
260 ) 320 )
261 } 321 }
262 322
@@ -285,30 +345,30 @@ fn main() {
285 fn indents_continued_chain_call() { 345 fn indents_continued_chain_call() {
286 type_char( 346 type_char(
287 '.', 347 '.',
288 r" 348 r#"
289 fn main() { 349fn main() {
290 xs.foo() 350 xs.foo()
291 .first() 351 .first()
292 $0 352 $0
293 } 353}
294 ", 354 "#,
295 r" 355 r#"
296 fn main() { 356fn main() {
297 xs.foo() 357 xs.foo()
298 .first() 358 .first()
299 . 359 .
300 } 360}
301 ", 361 "#,
302 ); 362 );
303 type_char_noop( 363 type_char_noop(
304 '.', 364 '.',
305 r" 365 r#"
306 fn main() { 366fn main() {
307 xs.foo() 367 xs.foo()
308 .first() 368 .first()
309 $0 369 $0
310 } 370}
311 ", 371 "#,
312 ); 372 );
313 } 373 }
314 374
@@ -316,33 +376,33 @@ fn main() {
316 fn indents_middle_of_chain_call() { 376 fn indents_middle_of_chain_call() {
317 type_char( 377 type_char(
318 '.', 378 '.',
319 r" 379 r#"
320 fn source_impl() { 380fn source_impl() {
321 let var = enum_defvariant_list().unwrap() 381 let var = enum_defvariant_list().unwrap()
322 $0 382 $0
323 .nth(92) 383 .nth(92)
324 .unwrap(); 384 .unwrap();
325 } 385}
326 ", 386 "#,
327 r" 387 r#"
328 fn source_impl() { 388fn source_impl() {
329 let var = enum_defvariant_list().unwrap() 389 let var = enum_defvariant_list().unwrap()
330 . 390 .
331 .nth(92) 391 .nth(92)
332 .unwrap(); 392 .unwrap();
333 } 393}
334 ", 394 "#,
335 ); 395 );
336 type_char_noop( 396 type_char_noop(
337 '.', 397 '.',
338 r" 398 r#"
339 fn source_impl() { 399fn source_impl() {
340 let var = enum_defvariant_list().unwrap() 400 let var = enum_defvariant_list().unwrap()
341 $0 401 $0
342 .nth(92) 402 .nth(92)
343 .unwrap(); 403 .unwrap();
344 } 404}
345 ", 405 "#,
346 ); 406 );
347 } 407 }
348 408
@@ -350,24 +410,113 @@ fn main() {
350 fn dont_indent_freestanding_dot() { 410 fn dont_indent_freestanding_dot() {
351 type_char_noop( 411 type_char_noop(
352 '.', 412 '.',
353 r" 413 r#"
354 fn main() { 414fn main() {
355 $0 415 $0
356 } 416}
357 ", 417 "#,
358 ); 418 );
359 type_char_noop( 419 type_char_noop(
360 '.', 420 '.',
361 r" 421 r#"
362 fn main() { 422fn main() {
363 $0 423$0
364 } 424}
365 ", 425 "#,
366 ); 426 );
367 } 427 }
368 428
369 #[test] 429 #[test]
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(
432 '>',
433 r#"
434fn foo() -$0{ 92 }
435"#,
436 r#"
437fn foo() -> { 92 }
438"#,
439 );
440 }
441
442 #[test]
443 fn adds_closing_brace() {
444 type_char(
445 '{',
446 r#"
447fn f() { match () { _ => $0() } }
448 "#,
449 r#"
450fn f() { match () { _ => {()} } }
451 "#,
452 );
453 type_char(
454 '{',
455 r#"
456fn f() { $0() }
457 "#,
458 r#"
459fn f() { {()} }
460 "#,
461 );
462 type_char(
463 '{',
464 r#"
465fn f() { let x = $0(); }
466 "#,
467 r#"
468fn f() { let x = {()}; }
469 "#,
470 );
471 type_char(
472 '{',
473 r#"
474fn f() { let x = $0a.b(); }
475 "#,
476 r#"
477fn f() { let x = {a.b()}; }
478 "#,
479 );
480 type_char(
481 '{',
482 r#"
483const S: () = $0();
484fn f() {}
485 "#,
486 r#"
487const S: () = {()};
488fn f() {}
489 "#,
490 );
491 type_char(
492 '{',
493 r#"
494const S: () = $0a.b();
495fn f() {}
496 "#,
497 r#"
498const S: () = {a.b()};
499fn f() {}
500 "#,
501 );
502 type_char(
503 '{',
504 r#"
505fn f() {
506 match x {
507 0 => $0(),
508 1 => (),
509 }
510}
511 "#,
512 r#"
513fn f() {
514 match x {
515 0 => {()},
516 1 => (),
517 }
518}
519 "#,
520 );
372 } 521 }
373} 522}