X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=crates%2Fide%2Fsrc%2Ftyping%2Fon_enter.rs;h=48c1713270b6c5c46c42deb7553ca98ac66384d3;hb=342c3c42bb282cae05e9d8740c2f724ea8014150;hp=f7d46146c50923b044e8f797afb00c3c9108c1d3;hpb=c26c911ec1e6c2ad1dcb7d155a6a1d528839ad1a;p=rust.git diff --git a/crates/ide/src/typing/on_enter.rs b/crates/ide/src/typing/on_enter.rs index f7d46146c50..48c1713270b 100644 --- a/crates/ide/src/typing/on_enter.rs +++ b/crates/ide/src/typing/on_enter.rs @@ -1,15 +1,16 @@ //! Handles the `Enter` key press. At the momently, this only continues //! comments, but should handle indent some time in the future as well. -use base_db::{FilePosition, SourceDatabase}; +use ide_db::base_db::{FilePosition, SourceDatabase}; use ide_db::RootDatabase; use syntax::{ - ast::{self, AstToken}, + algo::find_node_at_offset, + ast::{self, edit::IndentLevel, AstToken}, AstNode, SmolStr, SourceFile, SyntaxKind::*, - SyntaxToken, TextRange, TextSize, TokenAtOffset, + SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset, }; -use test_utils::mark; + use text_edit::TextEdit; // Feature: On Enter @@ -18,9 +19,15 @@ // // - kbd:[Enter] inside triple-slash comments automatically inserts `///` // - kbd:[Enter] in the middle or after a trailing space in `//` inserts `//` +// - kbd:[Enter] inside `//!` doc comments automatically inserts `//!` +// - kbd:[Enter] after `{` indents contents and closing `}` of single-line block // // This action needs to be assigned to shortcut explicitly. // +// Note that, depending on the other installed extensions, this feature can visibly slow down typing. +// Similarly, if rust-analyzer crashes or stops responding, `Enter` might not work. +// In that case, you can still press `Shift-Enter` to insert a newline. +// // VS Code:: // // Add the following to `keybindings.json`: @@ -32,47 +39,130 @@ // "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust" // } // ---- +// +// When using the Vim plugin: +// [source,json] +// ---- +// { +// "key": "Enter", +// "command": "rust-analyzer.onEnter", +// "when": "editorTextFocus && !suggestWidgetVisible && editorLangId == rust && vim.mode == 'Insert'" +// } +// ---- +// +// image::https://user-images.githubusercontent.com/48062697/113065578-04c21800-91b1-11eb-82b8-22b8c481e645.gif[] pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option { let parse = db.parse(position.file_id); let file = parse.tree(); - let comment = file - .syntax() - .token_at_offset(position.offset) - .left_biased() - .and_then(ast::Comment::cast)?; + let token = file.syntax().token_at_offset(position.offset).left_biased()?; + + if let Some(comment) = ast::Comment::cast(token.clone()) { + return on_enter_in_comment(&comment, &file, position.offset); + } + if token.kind() == L_CURLY { + // Typing enter after the `{` of a block expression, where the `}` is on the same line + if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) + .and_then(|block| on_enter_in_block(block, position)) + { + cov_mark::hit!(indent_block_contents); + return Some(edit); + } + + // Typing enter after the `{` of a use tree list. + if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{')) + .and_then(|list| on_enter_in_use_tree_list(list, position)) + { + cov_mark::hit!(indent_block_contents); + return Some(edit); + } + } + + None +} + +fn on_enter_in_comment( + comment: &ast::Comment, + file: &ast::SourceFile, + offset: TextSize, +) -> Option { if comment.kind().shape.is_block() { return None; } let prefix = comment.prefix(); let comment_range = comment.syntax().text_range(); - if position.offset < comment_range.start() + TextSize::of(prefix) { + if offset < comment_range.start() + TextSize::of(prefix) { return None; } - let mut remove_last_space = false; + let mut remove_trailing_whitespace = false; // Continuing single-line non-doc comments (like this one :) ) is annoying - if prefix == "//" && comment_range.end() == position.offset { + if prefix == "//" && comment_range.end() == offset { if comment.text().ends_with(' ') { - mark::hit!(continues_end_of_line_comment_with_space); - remove_last_space = true; - } else if !followed_by_comment(&comment) { + cov_mark::hit!(continues_end_of_line_comment_with_space); + remove_trailing_whitespace = true; + } else if !followed_by_comment(comment) { return None; } } - let indent = node_indent(&file, comment.syntax())?; + let indent = node_indent(file, comment.syntax())?; let inserted = format!("\n{}{} $0", indent, prefix); - let delete = if remove_last_space { - TextRange::new(position.offset - TextSize::of(' '), position.offset) + let delete = if remove_trailing_whitespace { + let trimmed_len = comment.text().trim_end().len() as u32; + let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len; + TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset) } else { - TextRange::empty(position.offset) + TextRange::empty(offset) }; let edit = TextEdit::replace(delete, inserted); Some(edit) } +fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option { + let contents = block_contents(&block)?; + + if block.syntax().text().contains_char('\n') { + return None; + } + + let indent = IndentLevel::from_node(block.syntax()); + let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); + edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{}", indent))).ok()?; + Some(edit) +} + +fn on_enter_in_use_tree_list(list: ast::UseTreeList, position: FilePosition) -> Option { + if list.syntax().text().contains_char('\n') { + return None; + } + + let indent = IndentLevel::from_node(list.syntax()); + let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1)); + edit.union(TextEdit::insert( + list.r_curly_token()?.text_range().start(), + format!("\n{}", indent), + )) + .ok()?; + Some(edit) +} + +fn block_contents(block: &ast::BlockExpr) -> Option { + let mut node = block.tail_expr().map(|e| e.syntax().clone()); + + for stmt in block.statements() { + if node.is_some() { + // More than 1 node in the block + return None; + } + + node = Some(stmt.syntax().clone()); + } + + node +} + fn followed_by_comment(comment: &ast::Comment) -> bool { let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) { Some(it) => it, @@ -107,12 +197,12 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option { #[cfg(test)] mod tests { use stdx::trim_indent; - use test_utils::{assert_eq_text, mark}; + use test_utils::assert_eq_text; - use crate::mock_analysis::analysis_and_position; + use crate::fixture; fn apply_on_enter(before: &str) -> Option { - let (analysis, position) = analysis_and_position(&before); + let (analysis, position) = fixture::position(before); let result = analysis.on_enter(position).unwrap()?; let mut actual = analysis.file_text(position.file_id).unwrap().to_string(); @@ -134,7 +224,7 @@ fn do_check_noop(ra_fixture_text: &str) { fn continues_doc_comment() { do_check( r" -/// Some docs<|> +/// Some docs$0 fn foo() { } ", @@ -149,7 +239,7 @@ fn foo() { do_check( r" impl S { - /// Some<|> docs. + /// Some$0 docs. fn foo() {} } ", @@ -164,7 +254,7 @@ fn foo() {} do_check( r" -///<|> Some docs +///$0 Some docs fn foo() { } ", @@ -179,7 +269,26 @@ fn foo() { #[test] fn does_not_continue_before_doc_comment() { - do_check_noop(r"<|>//! docz"); + do_check_noop(r"$0//! docz"); + } + + #[test] + fn continues_another_doc_comment() { + do_check( + r#" +fn main() { + //! Documentation for$0 on enter + let x = 1 + 1; +} +"#, + r#" +fn main() { + //! Documentation for + //! $0 on enter + let x = 1 + 1; +} +"#, + ); } #[test] @@ -187,7 +296,7 @@ fn continues_code_comment_in_the_middle_of_line() { do_check( r" fn main() { - // Fix<|> me + // Fix$0 me let x = 1 + 1; } ", @@ -206,7 +315,7 @@ fn continues_code_comment_in_the_middle_several_lines() { do_check( r" fn main() { - // Fix<|> + // Fix$0 // me let x = 1 + 1; } @@ -227,7 +336,7 @@ fn does_not_continue_end_of_line_comment() { do_check_noop( r" fn main() { - // Fix me<|> + // Fix me$0 let x = 1 + 1; } ", @@ -236,11 +345,11 @@ fn main() { #[test] fn continues_end_of_line_comment_with_space() { - mark::check!(continues_end_of_line_comment_with_space); + cov_mark::check!(continues_end_of_line_comment_with_space); do_check( r#" fn main() { - // Fix me <|> + // Fix me $0 let x = 1 + 1; } "#, @@ -253,4 +362,255 @@ fn main() { "#, ); } + + #[test] + fn trims_all_trailing_whitespace() { + do_check( + " +fn main() { + // Fix me \t\t $0 + let x = 1 + 1; +} +", + " +fn main() { + // Fix me + // $0 + let x = 1 + 1; +} +", + ); + } + + #[test] + fn indents_fn_body_block() { + cov_mark::check!(indent_block_contents); + do_check( + r#" +fn f() {$0()} + "#, + r#" +fn f() { + $0() +} + "#, + ); + } + + #[test] + fn indents_block_expr() { + do_check( + r#" +fn f() { + let x = {$0()}; +} + "#, + r#" +fn f() { + let x = { + $0() + }; +} + "#, + ); + } + + #[test] + fn indents_match_arm() { + do_check( + r#" +fn f() { + match 6 { + 1 => {$0f()}, + _ => (), + } +} + "#, + r#" +fn f() { + match 6 { + 1 => { + $0f() + }, + _ => (), + } +} + "#, + ); + } + + #[test] + fn indents_block_with_statement() { + do_check( + r#" +fn f() {$0a = b} + "#, + r#" +fn f() { + $0a = b +} + "#, + ); + do_check( + r#" +fn f() {$0fn f() {}} + "#, + r#" +fn f() { + $0fn f() {} +} + "#, + ); + } + + #[test] + fn indents_nested_blocks() { + do_check( + r#" +fn f() {$0{}} + "#, + r#" +fn f() { + $0{} +} + "#, + ); + } + + #[test] + fn does_not_indent_empty_block() { + do_check_noop( + r#" +fn f() {$0} + "#, + ); + do_check_noop( + r#" +fn f() {{$0}} + "#, + ); + } + + #[test] + fn does_not_indent_block_with_too_much_content() { + do_check_noop( + r#" +fn f() {$0 a = b; ()} + "#, + ); + do_check_noop( + r#" +fn f() {$0 a = b; a = b; } + "#, + ); + } + + #[test] + fn does_not_indent_multiline_block() { + do_check_noop( + r#" +fn f() {$0 +} + "#, + ); + do_check_noop( + r#" +fn f() {$0 + +} + "#, + ); + } + + #[test] + fn indents_use_tree_list() { + do_check( + r#" +use crate::{$0}; + "#, + r#" +use crate::{ + $0 +}; + "#, + ); + do_check( + r#" +use crate::{$0Object, path::to::OtherThing}; + "#, + r#" +use crate::{ + $0Object, path::to::OtherThing +}; + "#, + ); + do_check( + r#" +use {crate::{$0Object, path::to::OtherThing}}; + "#, + r#" +use {crate::{ + $0Object, path::to::OtherThing +}}; + "#, + ); + do_check( + r#" +use { + crate::{$0Object, path::to::OtherThing} +}; + "#, + r#" +use { + crate::{ + $0Object, path::to::OtherThing + } +}; + "#, + ); + } + + #[test] + fn does_not_indent_use_tree_list_when_not_at_curly_brace() { + do_check_noop( + r#" +use path::{Thing$0}; + "#, + ); + } + + #[test] + fn does_not_indent_use_tree_list_without_curly_braces() { + do_check_noop( + r#" +use path::Thing$0; + "#, + ); + do_check_noop( + r#" +use path::$0Thing; + "#, + ); + do_check_noop( + r#" +use path::Thing$0}; + "#, + ); + do_check_noop( + r#" +use path::{$0Thing; + "#, + ); + } + + #[test] + fn does_not_indent_multiline_use_tree_list() { + do_check_noop( + r#" +use path::{$0 + Thing +}; + "#, + ); + } }