diff options
Diffstat (limited to 'crates/ra_ide/src/typing.rs')
-rw-r--r-- | crates/ra_ide/src/typing.rs | 67 |
1 files changed, 28 insertions, 39 deletions
diff --git a/crates/ra_ide/src/typing.rs b/crates/ra_ide/src/typing.rs index 2a8b4327f..67e2c33a0 100644 --- a/crates/ra_ide/src/typing.rs +++ b/crates/ra_ide/src/typing.rs | |||
@@ -23,14 +23,22 @@ use ra_syntax::{ | |||
23 | ast::{self, AstToken}, | 23 | ast::{self, AstToken}, |
24 | AstNode, SourceFile, TextRange, TextSize, | 24 | AstNode, SourceFile, TextRange, TextSize, |
25 | }; | 25 | }; |
26 | |||
26 | use ra_text_edit::TextEdit; | 27 | use ra_text_edit::TextEdit; |
27 | 28 | ||
28 | use crate::{source_change::SingleFileChange, SourceChange}; | 29 | use crate::SourceChange; |
29 | 30 | ||
30 | pub(crate) use on_enter::on_enter; | 31 | pub(crate) use on_enter::on_enter; |
31 | 32 | ||
32 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; | 33 | pub(crate) const TRIGGER_CHARS: &str = ".=>"; |
33 | 34 | ||
35 | // Feature: On Typing Assists | ||
36 | // | ||
37 | // Some features trigger on typing certain characters: | ||
38 | // | ||
39 | // - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression | ||
40 | // - Enter inside comments automatically inserts `///` | ||
41 | // - typing `.` in a chain method call auto-indents | ||
34 | pub(crate) fn on_char_typed( | 42 | pub(crate) fn on_char_typed( |
35 | db: &RootDatabase, | 43 | db: &RootDatabase, |
36 | position: FilePosition, | 44 | position: FilePosition, |
@@ -39,15 +47,11 @@ pub(crate) fn on_char_typed( | |||
39 | assert!(TRIGGER_CHARS.contains(char_typed)); | 47 | assert!(TRIGGER_CHARS.contains(char_typed)); |
40 | let file = &db.parse(position.file_id).tree(); | 48 | let file = &db.parse(position.file_id).tree(); |
41 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); | 49 | assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); |
42 | let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?; | 50 | let text_edit = on_char_typed_inner(file, position.offset, char_typed)?; |
43 | Some(single_file_change.into_source_change(position.file_id)) | 51 | Some(SourceChange::source_file_edit_from(position.file_id, text_edit)) |
44 | } | 52 | } |
45 | 53 | ||
46 | fn on_char_typed_inner( | 54 | fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { |
47 | file: &SourceFile, | ||
48 | offset: TextSize, | ||
49 | char_typed: char, | ||
50 | ) -> Option<SingleFileChange> { | ||
51 | assert!(TRIGGER_CHARS.contains(char_typed)); | 55 | assert!(TRIGGER_CHARS.contains(char_typed)); |
52 | match char_typed { | 56 | match char_typed { |
53 | '.' => on_dot_typed(file, offset), | 57 | '.' => on_dot_typed(file, offset), |
@@ -60,7 +64,7 @@ fn on_char_typed_inner( | |||
60 | /// Returns an edit which should be applied after `=` was typed. Primarily, | 64 | /// Returns an edit which should be applied after `=` was typed. Primarily, |
61 | /// this works when adding `let =`. | 65 | /// this works when adding `let =`. |
62 | // FIXME: use a snippet completion instead of this hack here. | 66 | // FIXME: use a snippet completion instead of this hack here. |
63 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChange> { | 67 | fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
64 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); | 68 | assert_eq!(file.syntax().text().char_at(offset), Some('=')); |
65 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; | 69 | let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; |
66 | if let_stmt.semicolon_token().is_some() { | 70 | if let_stmt.semicolon_token().is_some() { |
@@ -78,15 +82,11 @@ fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChange> | |||
78 | return None; | 82 | return None; |
79 | } | 83 | } |
80 | let offset = let_stmt.syntax().text_range().end(); | 84 | let offset = let_stmt.syntax().text_range().end(); |
81 | Some(SingleFileChange { | 85 | Some(TextEdit::insert(offset, ";".to_string())) |
82 | label: "add semicolon".to_string(), | ||
83 | edit: TextEdit::insert(offset, ";".to_string()), | ||
84 | cursor_position: None, | ||
85 | }) | ||
86 | } | 86 | } |
87 | 87 | ||
88 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. | 88 | /// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately. |
89 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChange> { | 89 | fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
90 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); | 90 | assert_eq!(file.syntax().text().char_at(offset), Some('.')); |
91 | let whitespace = | 91 | let whitespace = |
92 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; | 92 | file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?; |
@@ -107,15 +107,11 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChange> | |||
107 | return None; | 107 | return None; |
108 | } | 108 | } |
109 | 109 | ||
110 | Some(SingleFileChange { | 110 | Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent)) |
111 | label: "reindent dot".to_string(), | ||
112 | edit: TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent), | ||
113 | cursor_position: Some(offset + target_indent_len - current_indent_len + TextSize::of('.')), | ||
114 | }) | ||
115 | } | 111 | } |
116 | 112 | ||
117 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` | 113 | /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` |
118 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChange> { | 114 | fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> { |
119 | let file_text = file.syntax().text(); | 115 | let file_text = file.syntax().text(); |
120 | assert_eq!(file_text.char_at(offset), Some('>')); | 116 | assert_eq!(file_text.char_at(offset), Some('>')); |
121 | let after_arrow = offset + TextSize::of('>'); | 117 | let after_arrow = offset + TextSize::of('>'); |
@@ -126,11 +122,7 @@ fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<SingleFileChang | |||
126 | return None; | 122 | return None; |
127 | } | 123 | } |
128 | 124 | ||
129 | Some(SingleFileChange { | 125 | Some(TextEdit::insert(after_arrow, " ".to_string())) |
130 | label: "add space after return type".to_string(), | ||
131 | edit: TextEdit::insert(after_arrow, " ".to_string()), | ||
132 | cursor_position: Some(after_arrow), | ||
133 | }) | ||
134 | } | 126 | } |
135 | 127 | ||
136 | #[cfg(test)] | 128 | #[cfg(test)] |
@@ -139,26 +131,23 @@ mod tests { | |||
139 | 131 | ||
140 | use super::*; | 132 | use super::*; |
141 | 133 | ||
142 | fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> { | 134 | fn do_type_char(char_typed: char, before: &str) -> Option<String> { |
143 | let (offset, before) = extract_offset(before); | 135 | let (offset, before) = extract_offset(before); |
144 | let edit = TextEdit::insert(offset, char_typed.to_string()); | 136 | let edit = TextEdit::insert(offset, char_typed.to_string()); |
145 | let before = edit.apply(&before); | 137 | let mut before = before.to_string(); |
138 | edit.apply(&mut before); | ||
146 | let parse = SourceFile::parse(&before); | 139 | let parse = SourceFile::parse(&before); |
147 | on_char_typed_inner(&parse.tree(), offset, char_typed) | 140 | on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| { |
148 | .map(|it| (it.edit.apply(&before), it)) | 141 | it.apply(&mut before); |
142 | before.to_string() | ||
143 | }) | ||
149 | } | 144 | } |
150 | 145 | ||
151 | fn type_char(char_typed: char, before: &str, after: &str) { | 146 | fn type_char(char_typed: char, before: &str, after: &str) { |
152 | let (actual, file_change) = do_type_char(char_typed, before) | 147 | let actual = do_type_char(char_typed, before) |
153 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); | 148 | .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed)); |
154 | 149 | ||
155 | if after.contains("<|>") { | 150 | assert_eq_text!(after, &actual); |
156 | let (offset, after) = extract_offset(after); | ||
157 | assert_eq_text!(&after, &actual); | ||
158 | assert_eq!(file_change.cursor_position, Some(offset)) | ||
159 | } else { | ||
160 | assert_eq_text!(after, &actual); | ||
161 | } | ||
162 | } | 151 | } |
163 | 152 | ||
164 | fn type_char_noop(char_typed: char, before: &str) { | 153 | fn type_char_noop(char_typed: char, before: &str) { |
@@ -346,6 +335,6 @@ fn foo() { | |||
346 | 335 | ||
347 | #[test] | 336 | #[test] |
348 | fn adds_space_after_return_type() { | 337 | fn adds_space_after_return_type() { |
349 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }") | 338 | type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -> { 92 }") |
350 | } | 339 | } |
351 | } | 340 | } |