diff options
-rw-r--r-- | crates/ide/src/typing/on_enter.rs | 210 |
1 files changed, 199 insertions, 11 deletions
diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs index 6f1ce3689..7d2db201a 100644 --- a/crates/ide/src/typing/on_enter.rs +++ b/crates/ide/src/typing/on_enter.rs | |||
@@ -4,10 +4,11 @@ | |||
4 | use ide_db::base_db::{FilePosition, SourceDatabase}; | 4 | use ide_db::base_db::{FilePosition, SourceDatabase}; |
5 | use ide_db::RootDatabase; | 5 | use ide_db::RootDatabase; |
6 | use syntax::{ | 6 | use syntax::{ |
7 | ast::{self, AstToken}, | 7 | algo::find_node_at_offset, |
8 | ast::{self, edit::IndentLevel, AstToken}, | ||
8 | AstNode, SmolStr, SourceFile, | 9 | AstNode, SmolStr, SourceFile, |
9 | SyntaxKind::*, | 10 | SyntaxKind::*, |
10 | SyntaxToken, TextRange, TextSize, TokenAtOffset, | 11 | SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, |
11 | }; | 12 | }; |
12 | 13 | ||
13 | use text_edit::TextEdit; | 14 | use text_edit::TextEdit; |
@@ -19,6 +20,7 @@ use text_edit::TextEdit; | |||
19 | // - kbd:[Enter] inside triple-slash comments automatically inserts `///` | 20 | // - kbd:[Enter] inside triple-slash comments automatically inserts `///` |
20 | // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` | 21 | // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` |
21 | // - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` | 22 | // - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` |
23 | // - kbd:[Enter] after `{` indents contents and closing `}` of single-line block | ||
22 | // | 24 | // |
23 | // This action needs to be assigned to shortcut explicitly. | 25 | // This action needs to be assigned to shortcut explicitly. |
24 | // | 26 | // |
@@ -38,25 +40,43 @@ use text_edit::TextEdit; | |||
38 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { | 40 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { |
39 | let parse = db.parse(position.file_id); | 41 | let parse = db.parse(position.file_id); |
40 | let file = parse.tree(); | 42 | let file = parse.tree(); |
41 | let comment = file | 43 | let token = file.syntax().token_at_offset(position.offset).left_biased()?; |
42 | .syntax() | ||
43 | .token_at_offset(position.offset) | ||
44 | .left_biased() | ||
45 | .and_then(ast::Comment::cast)?; | ||
46 | 44 | ||
45 | if let Some(comment) = ast::Comment::cast(token.clone()) { | ||
46 | return on_enter_in_comment(&comment, &file, position.offset); | ||
47 | } | ||
48 | |||
49 | if token.kind() == L_CURLY { | ||
50 | // Typing enter after the `{` of a block expression, where the `}` is on the same line | ||
51 | if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) | ||
52 | .and_then(|block| on_enter_in_block(block, position)) | ||
53 | { | ||
54 | cov_mark::hit!(indent_block_contents); | ||
55 | return Some(edit); | ||
56 | } | ||
57 | } | ||
58 | |||
59 | None | ||
60 | } | ||
61 | |||
62 | fn on_enter_in_comment( | ||
63 | comment: &ast::Comment, | ||
64 | file: &ast::SourceFile, | ||
65 | offset: TextSize, | ||
66 | ) -> Option<TextEdit> { | ||
47 | if comment.kind().shape.is_block() { | 67 | if comment.kind().shape.is_block() { |
48 | return None; | 68 | return None; |
49 | } | 69 | } |
50 | 70 | ||
51 | let prefix = comment.prefix(); | 71 | let prefix = comment.prefix(); |
52 | let comment_range = comment.syntax().text_range(); | 72 | let comment_range = comment.syntax().text_range(); |
53 | if position.offset < comment_range.start() + TextSize::of(prefix) { | 73 | if offset < comment_range.start() + TextSize::of(prefix) { |
54 | return None; | 74 | return None; |
55 | } | 75 | } |
56 | 76 | ||
57 | let mut remove_trailing_whitespace = false; | 77 | let mut remove_trailing_whitespace = false; |
58 | // Continuing single-line non-doc comments (like this one :) ) is annoying | 78 | // Continuing single-line non-doc comments (like this one :) ) is annoying |
59 | if prefix == "//" && comment_range.end() == position.offset { | 79 | if prefix == "//" && comment_range.end() == offset { |
60 | if comment.text().ends_with(' ') { | 80 | if comment.text().ends_with(' ') { |
61 | cov_mark::hit!(continues_end_of_line_comment_with_space); | 81 | cov_mark::hit!(continues_end_of_line_comment_with_space); |
62 | remove_trailing_whitespace = true; | 82 | remove_trailing_whitespace = true; |
@@ -70,14 +90,42 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Text | |||
70 | let delete = if remove_trailing_whitespace { | 90 | let delete = if remove_trailing_whitespace { |
71 | let trimmed_len = comment.text().trim_end().len() as u32; | 91 | let trimmed_len = comment.text().trim_end().len() as u32; |
72 | let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; | 92 | let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; |
73 | TextRange::new(position.offset - TextSize::from(trailing_whitespace_len), position.offset) | 93 | TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset) |
74 | } else { | 94 | } else { |
75 | TextRange::empty(position.offset) | 95 | TextRange::empty(offset) |
76 | }; | 96 | }; |
77 | let edit = TextEdit::replace(delete, inserted); | 97 | let edit = TextEdit::replace(delete, inserted); |
78 | Some(edit) | 98 | Some(edit) |
79 | } | 99 | } |
80 | 100 | ||
101 | fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> { | ||
102 | let contents = block_contents(&block)?; | ||
103 | |||
104 | if block.syntax().text().contains_char('\n') { | ||
105 | return None; | ||
106 | } | ||
107 | |||
108 | let indent = IndentLevel::from_node(block.syntax()); | ||
109 | let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); | ||
110 | edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?; | ||
111 | Some(edit) | ||
112 | } | ||
113 | |||
114 | fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> { | ||
115 | let mut node = block.tail_expr().map(|e| e.syntax().clone()); | ||
116 | |||
117 | for stmt in block.statements() { | ||
118 | if node.is_some() { | ||
119 | // More than 1 node in the block | ||
120 | return None; | ||
121 | } | ||
122 | |||
123 | node = Some(stmt.syntax().clone()); | ||
124 | } | ||
125 | |||
126 | node | ||
127 | } | ||
128 | |||
81 | fn followed_by_comment(comment: &ast::Comment) -> bool { | 129 | fn followed_by_comment(comment: &ast::Comment) -> bool { |
82 | let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { | 130 | let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { |
83 | Some(it) => it, | 131 | Some(it) => it, |
@@ -296,4 +344,144 @@ fn main() { | |||
296 | ", | 344 | ", |
297 | ); | 345 | ); |
298 | } | 346 | } |
347 | |||
348 | #[test] | ||
349 | fn indents_fn_body_block() { | ||
350 | cov_mark::check!(indent_block_contents); | ||
351 | do_check( | ||
352 | r#" | ||
353 | fn f() {$0()} | ||
354 | "#, | ||
355 | r#" | ||
356 | fn f() { | ||
357 | $0() | ||
358 | } | ||
359 | "#, | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn indents_block_expr() { | ||
365 | do_check( | ||
366 | r#" | ||
367 | fn f() { | ||
368 | let x = {$0()}; | ||
369 | } | ||
370 | "#, | ||
371 | r#" | ||
372 | fn f() { | ||
373 | let x = { | ||
374 | $0() | ||
375 | }; | ||
376 | } | ||
377 | "#, | ||
378 | ); | ||
379 | } | ||
380 | |||
381 | #[test] | ||
382 | fn indents_match_arm() { | ||
383 | do_check( | ||
384 | r#" | ||
385 | fn f() { | ||
386 | match 6 { | ||
387 | 1 => {$0f()}, | ||
388 | _ => (), | ||
389 | } | ||
390 | } | ||
391 | "#, | ||
392 | r#" | ||
393 | fn f() { | ||
394 | match 6 { | ||
395 | 1 => { | ||
396 | $0f() | ||
397 | }, | ||
398 | _ => (), | ||
399 | } | ||
400 | } | ||
401 | "#, | ||
402 | ); | ||
403 | } | ||
404 | |||
405 | #[test] | ||
406 | fn indents_block_with_statement() { | ||
407 | do_check( | ||
408 | r#" | ||
409 | fn f() {$0a = b} | ||
410 | "#, | ||
411 | r#" | ||
412 | fn f() { | ||
413 | $0a = b | ||
414 | } | ||
415 | "#, | ||
416 | ); | ||
417 | do_check( | ||
418 | r#" | ||
419 | fn f() {$0fn f() {}} | ||
420 | "#, | ||
421 | r#" | ||
422 | fn f() { | ||
423 | $0fn f() {} | ||
424 | } | ||
425 | "#, | ||
426 | ); | ||
427 | } | ||
428 | |||
429 | #[test] | ||
430 | fn indents_nested_blocks() { | ||
431 | do_check( | ||
432 | r#" | ||
433 | fn f() {$0{}} | ||
434 | "#, | ||
435 | r#" | ||
436 | fn f() { | ||
437 | $0{} | ||
438 | } | ||
439 | "#, | ||
440 | ); | ||
441 | } | ||
442 | |||
443 | #[test] | ||
444 | fn does_not_indent_empty_block() { | ||
445 | do_check_noop( | ||
446 | r#" | ||
447 | fn f() {$0} | ||
448 | "#, | ||
449 | ); | ||
450 | do_check_noop( | ||
451 | r#" | ||
452 | fn f() {{$0}} | ||
453 | "#, | ||
454 | ); | ||
455 | } | ||
456 | |||
457 | #[test] | ||
458 | fn does_not_indent_block_with_too_much_content() { | ||
459 | do_check_noop( | ||
460 | r#" | ||
461 | fn f() {$0 a = b; ()} | ||
462 | "#, | ||
463 | ); | ||
464 | do_check_noop( | ||
465 | r#" | ||
466 | fn f() {$0 a = b; a = b; } | ||
467 | "#, | ||
468 | ); | ||
469 | } | ||
470 | |||
471 | #[test] | ||
472 | fn does_not_indent_multiline_block() { | ||
473 | do_check_noop( | ||
474 | r#" | ||
475 | fn f() {$0 | ||
476 | } | ||
477 | "#, | ||
478 | ); | ||
479 | do_check_noop( | ||
480 | r#" | ||
481 | fn f() {$0 | ||
482 | |||
483 | } | ||
484 | "#, | ||
485 | ); | ||
486 | } | ||
299 | } | 487 | } |