1 //! This module handles auto-magic editing actions applied together with users
2 //! edits. For example, if the user typed
3 //!
4 //! ```text
5 //! foo
6 //! .bar()
7 //! .baz()
8 //! | // <- cursor is here
9 //! ```
10 //!
11 //! and types `.` next, we want to indent the dot.
12 //!
13 //! Language server executes such typing assists synchronously. That is, they
14 //! block user's typing and should be pretty fast for this reason!
15
16 mod on_enter;
17
18 use ide_db::{
19 base_db::{FilePosition, SourceDatabase},
20 RootDatabase,
21 };
22 use syntax::{
23 algo::find_node_at_offset,
24 ast::{self, edit::IndentLevel, AstToken},
25 AstNode, Parse, SourceFile,
26 SyntaxKind::{self, FIELD_EXPR, METHOD_CALL_EXPR},
27 TextRange, TextSize,
28 };
29
30 use text_edit::{Indel, TextEdit};
31
32 use crate::SourceChange;
33
34 pub(crate) use on_enter::on_enter;
35
36 // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
37 pub(crate) const TRIGGER_CHARS: &str = ".=>{";
38
39 // Feature: On Typing Assists
40 //
41 // Some features trigger on typing certain characters:
42 //
43 // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
44 // - typing `.` in a chain method call auto-indents
45 // - typing `{` in front of an expression inserts a closing `}` after the expression
46 //
47 // VS Code::
48 //
49 // Add the following to `settings.json`:
50 // [source,json]
51 // ----
52 // "editor.formatOnType": true,
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[]
on_char_typed( db: &RootDatabase, position: FilePosition, char_typed: char, ) -> Option<SourceChange>57 pub(crate) fn on_char_typed(
58 db: &RootDatabase,
59 position: FilePosition,
60 char_typed: char,
61 ) -> Option<SourceChange> {
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 }
69 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
70 Some(SourceChange::from_text_edit(position.file_id, edit))
71 }
72
on_char_typed_inner( file: &Parse<SourceFile>, offset: TextSize, char_typed: char, ) -> Option<TextEdit>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 }
81 match char_typed {
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),
86 _ => unreachable!(),
87 }
88 }
89
90 /// Inserts a closing `}` when the user types an opening `{`, wrapping an existing expression in a
91 /// block, or a part of a `use` item.
on_opening_brace_typed(file: &Parse<SourceFile>, offset: TextSize) -> Option<TextEdit>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 if brace_token.kind() != SyntaxKind::L_CURLY {
99 return None;
100 }
101
102 // Remove the `{` to get a better parse tree, and reparse.
103 let range = brace_token.text_range();
104 if !stdx::always!(range.len() == TextSize::of('{')) {
105 return None;
106 }
107 let file = file.reparse(&Indel::delete(range));
108
109 if let Some(edit) = brace_expr(&file.tree(), offset) {
110 return Some(edit);
111 }
112
113 if let Some(edit) = brace_use_path(&file.tree(), offset) {
114 return Some(edit);
115 }
116
117 return None;
118
119 fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
120 let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?;
121 if segment.syntax().text_range().start() != offset {
122 return None;
123 }
124
125 let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?;
126
127 Some(TextEdit::insert(
128 tree.syntax().text_range().end() + TextSize::of("{"),
129 "}".to_string(),
130 ))
131 }
132
133 fn brace_expr(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
134 let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?;
135 if expr.syntax().text_range().start() != offset {
136 return None;
137 }
138
139 // Enclose the outermost expression starting at `offset`
140 while let Some(parent) = expr.syntax().parent() {
141 if parent.text_range().start() != expr.syntax().text_range().start() {
142 break;
143 }
144
145 match ast::Expr::cast(parent) {
146 Some(parent) => expr = parent,
147 None => break,
148 }
149 }
150
151 // If it's a statement in a block, we don't know how many statements should be included
152 if ast::ExprStmt::can_cast(expr.syntax().parent()?.kind()) {
153 return None;
154 }
155
156 // Insert `}` right after the expression.
157 Some(TextEdit::insert(
158 expr.syntax().text_range().end() + TextSize::of("{"),
159 "}".to_string(),
160 ))
161 }
162 }
163
164 /// Returns an edit which should be applied after `=` was typed. Primarily,
165 /// this works when adding `let =`.
166 // FIXME: use a snippet completion instead of this hack here.
on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>167 fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
168 if !stdx::always!(file.syntax().text().char_at(offset) == Some('=')) {
169 return None;
170 }
171 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
172 if let_stmt.semicolon_token().is_some() {
173 return None;
174 }
175 if let Some(expr) = let_stmt.initializer() {
176 let expr_range = expr.syntax().text_range();
177 if expr_range.contains(offset) && offset != expr_range.start() {
178 return None;
179 }
180 if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
181 return None;
182 }
183 } else {
184 return None;
185 }
186 let offset = let_stmt.syntax().text_range().end();
187 Some(TextEdit::insert(offset, ";".to_string()))
188 }
189
190 /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>191 fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
192 if !stdx::always!(file.syntax().text().char_at(offset) == Some('.')) {
193 return None;
194 }
195 let whitespace =
196 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
197
198 let current_indent = {
199 let text = whitespace.text();
200 let newline = text.rfind('\n')?;
201 &text[newline + 1..]
202 };
203 let current_indent_len = TextSize::of(current_indent);
204
205 let parent = whitespace.syntax().parent()?;
206 // Make sure dot is a part of call chain
207 if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
208 return None;
209 }
210 let prev_indent = IndentLevel::from_node(&parent);
211 let target_indent = format!(" {}", prev_indent);
212 let target_indent_len = TextSize::of(&target_indent);
213 if current_indent_len == target_indent_len {
214 return None;
215 }
216
217 Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
218 }
219
220 /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit>221 fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
222 let file_text = file.syntax().text();
223 if !stdx::always!(file_text.char_at(offset) == Some('>')) {
224 return None;
225 }
226 let after_arrow = offset + TextSize::of('>');
227 if file_text.char_at(after_arrow) != Some('{') {
228 return None;
229 }
230 if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
231 return None;
232 }
233
234 Some(TextEdit::insert(after_arrow, " ".to_string()))
235 }
236
237 #[cfg(test)]
238 mod tests {
239 use test_utils::{assert_eq_text, extract_offset};
240
241 use super::*;
242
do_type_char(char_typed: char, before: &str) -> Option<String>243 fn do_type_char(char_typed: char, before: &str) -> Option<String> {
244 let (offset, mut before) = extract_offset(before);
245 let edit = TextEdit::insert(offset, char_typed.to_string());
246 edit.apply(&mut before);
247 let parse = SourceFile::parse(&before);
248 on_char_typed_inner(&parse, offset, char_typed).map(|it| {
249 it.apply(&mut before);
250 before.to_string()
251 })
252 }
253
type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str)254 fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
255 let actual = do_type_char(char_typed, ra_fixture_before)
256 .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
257
258 assert_eq_text!(ra_fixture_after, &actual);
259 }
260
type_char_noop(char_typed: char, ra_fixture_before: &str)261 fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
262 let file_change = do_type_char(char_typed, ra_fixture_before);
263 assert!(file_change.is_none())
264 }
265
266 #[test]
test_on_eq_typed()267 fn test_on_eq_typed() {
268 // do_check(r"
269 // fn foo() {
270 // let foo =$0
271 // }
272 // ", r"
273 // fn foo() {
274 // let foo =;
275 // }
276 // ");
277 type_char(
278 '=',
279 r#"
280 fn foo() {
281 let foo $0 1 + 1
282 }
283 "#,
284 r#"
285 fn foo() {
286 let foo = 1 + 1;
287 }
288 "#,
289 );
290 // do_check(r"
291 // fn foo() {
292 // let foo =$0
293 // let bar = 1;
294 // }
295 // ", r"
296 // fn foo() {
297 // let foo =;
298 // let bar = 1;
299 // }
300 // ");
301 }
302
303 #[test]
indents_new_chain_call()304 fn indents_new_chain_call() {
305 type_char(
306 '.',
307 r#"
308 fn main() {
309 xs.foo()
310 $0
311 }
312 "#,
313 r#"
314 fn main() {
315 xs.foo()
316 .
317 }
318 "#,
319 );
320 type_char_noop(
321 '.',
322 r#"
323 fn main() {
324 xs.foo()
325 $0
326 }
327 "#,
328 )
329 }
330
331 #[test]
indents_new_chain_call_with_semi()332 fn indents_new_chain_call_with_semi() {
333 type_char(
334 '.',
335 r"
336 fn main() {
337 xs.foo()
338 $0;
339 }
340 ",
341 r#"
342 fn main() {
343 xs.foo()
344 .;
345 }
346 "#,
347 );
348 type_char_noop(
349 '.',
350 r#"
351 fn main() {
352 xs.foo()
353 $0;
354 }
355 "#,
356 )
357 }
358
359 #[test]
indents_new_chain_call_with_let()360 fn indents_new_chain_call_with_let() {
361 type_char(
362 '.',
363 r#"
364 fn main() {
365 let _ = foo
366 $0
367 bar()
368 }
369 "#,
370 r#"
371 fn main() {
372 let _ = foo
373 .
374 bar()
375 }
376 "#,
377 );
378 }
379
380 #[test]
indents_continued_chain_call()381 fn indents_continued_chain_call() {
382 type_char(
383 '.',
384 r#"
385 fn main() {
386 xs.foo()
387 .first()
388 $0
389 }
390 "#,
391 r#"
392 fn main() {
393 xs.foo()
394 .first()
395 .
396 }
397 "#,
398 );
399 type_char_noop(
400 '.',
401 r#"
402 fn main() {
403 xs.foo()
404 .first()
405 $0
406 }
407 "#,
408 );
409 }
410
411 #[test]
indents_middle_of_chain_call()412 fn indents_middle_of_chain_call() {
413 type_char(
414 '.',
415 r#"
416 fn source_impl() {
417 let var = enum_defvariant_list().unwrap()
418 $0
419 .nth(92)
420 .unwrap();
421 }
422 "#,
423 r#"
424 fn source_impl() {
425 let var = enum_defvariant_list().unwrap()
426 .
427 .nth(92)
428 .unwrap();
429 }
430 "#,
431 );
432 type_char_noop(
433 '.',
434 r#"
435 fn source_impl() {
436 let var = enum_defvariant_list().unwrap()
437 $0
438 .nth(92)
439 .unwrap();
440 }
441 "#,
442 );
443 }
444
445 #[test]
dont_indent_freestanding_dot()446 fn dont_indent_freestanding_dot() {
447 type_char_noop(
448 '.',
449 r#"
450 fn main() {
451 $0
452 }
453 "#,
454 );
455 type_char_noop(
456 '.',
457 r#"
458 fn main() {
459 $0
460 }
461 "#,
462 );
463 }
464
465 #[test]
adds_space_after_return_type()466 fn adds_space_after_return_type() {
467 type_char(
468 '>',
469 r#"
470 fn foo() -$0{ 92 }
471 "#,
472 r#"
473 fn foo() -> { 92 }
474 "#,
475 );
476 }
477
478 #[test]
adds_closing_brace_for_expr()479 fn adds_closing_brace_for_expr() {
480 type_char(
481 '{',
482 r#"
483 fn f() { match () { _ => $0() } }
484 "#,
485 r#"
486 fn f() { match () { _ => {()} } }
487 "#,
488 );
489 type_char(
490 '{',
491 r#"
492 fn f() { $0() }
493 "#,
494 r#"
495 fn f() { {()} }
496 "#,
497 );
498 type_char(
499 '{',
500 r#"
501 fn f() { let x = $0(); }
502 "#,
503 r#"
504 fn f() { let x = {()}; }
505 "#,
506 );
507 type_char(
508 '{',
509 r#"
510 fn f() { let x = $0a.b(); }
511 "#,
512 r#"
513 fn f() { let x = {a.b()}; }
514 "#,
515 );
516 type_char(
517 '{',
518 r#"
519 const S: () = $0();
520 fn f() {}
521 "#,
522 r#"
523 const S: () = {()};
524 fn f() {}
525 "#,
526 );
527 type_char(
528 '{',
529 r#"
530 const S: () = $0a.b();
531 fn f() {}
532 "#,
533 r#"
534 const S: () = {a.b()};
535 fn f() {}
536 "#,
537 );
538 type_char(
539 '{',
540 r#"
541 fn f() {
542 match x {
543 0 => $0(),
544 1 => (),
545 }
546 }
547 "#,
548 r#"
549 fn f() {
550 match x {
551 0 => {()},
552 1 => (),
553 }
554 }
555 "#,
556 );
557 }
558
559 #[test]
noop_in_string_literal()560 fn noop_in_string_literal() {
561 // Regression test for #9351
562 type_char_noop(
563 '{',
564 r##"
565 fn check_with(ra_fixture: &str, expect: Expect) {
566 let base = r#"
567 enum E { T(), R$0, C }
568 use self::E::X;
569 const Z: E = E::C;
570 mod m {}
571 asdasdasdasdasdasda
572 sdasdasdasdasdasda
573 sdasdasdasdasd
574 "#;
575 let actual = completion_list(&format!("{}\n{}", base, ra_fixture));
576 expect.assert_eq(&actual)
577 }
578 "##,
579 );
580 }
581
582 #[test]
adds_closing_brace_for_use_tree()583 fn adds_closing_brace_for_use_tree() {
584 type_char(
585 '{',
586 r#"
587 use some::$0Path;
588 "#,
589 r#"
590 use some::{Path};
591 "#,
592 );
593 type_char(
594 '{',
595 r#"
596 use some::{Path, $0Other};
597 "#,
598 r#"
599 use some::{Path, {Other}};
600 "#,
601 );
602 type_char(
603 '{',
604 r#"
605 use some::{$0Path, Other};
606 "#,
607 r#"
608 use some::{{Path}, Other};
609 "#,
610 );
611 type_char(
612 '{',
613 r#"
614 use some::path::$0to::Item;
615 "#,
616 r#"
617 use some::path::{to::Item};
618 "#,
619 );
620 type_char(
621 '{',
622 r#"
623 use some::$0path::to::Item;
624 "#,
625 r#"
626 use some::{path::to::Item};
627 "#,
628 );
629 type_char(
630 '{',
631 r#"
632 use $0some::path::to::Item;
633 "#,
634 r#"
635 use {some::path::to::Item};
636 "#,
637 );
638 type_char(
639 '{',
640 r#"
641 use some::path::$0to::{Item};
642 "#,
643 r#"
644 use some::path::{to::{Item}};
645 "#,
646 );
647 type_char(
648 '{',
649 r#"
650 use $0Thing as _;
651 "#,
652 r#"
653 use {Thing as _};
654 "#,
655 );
656
657 type_char_noop(
658 '{',
659 r#"
660 use some::pa$0th::to::Item;
661 "#,
662 );
663 }
664 }
665