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.rs364
1 files changed, 364 insertions, 0 deletions
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
new file mode 100644
index 000000000..899ce5f26
--- /dev/null
+++ b/crates/ide/src/typing.rs
@@ -0,0 +1,364 @@
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
16mod on_enter;
17
18use base_db::{FilePosition, SourceDatabase};
19use ide_db::{source_change::SourceFileEdit, RootDatabase};
20use syntax::{
21 algo::find_node_at_offset,
22 ast::{self, edit::IndentLevel, AstToken},
23 AstNode, SourceFile,
24 SyntaxKind::{FIELD_EXPR, METHOD_CALL_EXPR},
25 TextRange, TextSize,
26};
27
28use text_edit::TextEdit;
29
30use crate::SourceChange;
31
32pub(crate) use on_enter::on_enter;
33
34pub(crate) const TRIGGER_CHARS: &str = ".=>";
35
36// Feature: On Typing Assists
37//
38// Some features trigger on typing certain characters:
39//
40// - typing `let =` tries to smartly add `;` if `=` is followed by an existing expression
41// - typing `.` in a chain method call auto-indents
42pub(crate) fn on_char_typed(
43 db: &RootDatabase,
44 position: FilePosition,
45 char_typed: char,
46) -> Option<SourceChange> {
47 assert!(TRIGGER_CHARS.contains(char_typed));
48 let file = &db.parse(position.file_id).tree();
49 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
50 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
51 Some(SourceFileEdit { file_id: position.file_id, edit }.into())
52}
53
54fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
55 assert!(TRIGGER_CHARS.contains(char_typed));
56 match char_typed {
57 '.' => on_dot_typed(file, offset),
58 '=' => on_eq_typed(file, offset),
59 '>' => on_arrow_typed(file, offset),
60 _ => unreachable!(),
61 }
62}
63
64/// Returns an edit which should be applied after `=` was typed. Primarily,
65/// this works when adding `let =`.
66// FIXME: use a snippet completion instead of this hack here.
67fn on_eq_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
68 assert_eq!(file.syntax().text().char_at(offset), Some('='));
69 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
70 if let_stmt.semicolon_token().is_some() {
71 return None;
72 }
73 if let Some(expr) = let_stmt.initializer() {
74 let expr_range = expr.syntax().text_range();
75 if expr_range.contains(offset) && offset != expr_range.start() {
76 return None;
77 }
78 if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
79 return None;
80 }
81 } else {
82 return None;
83 }
84 let offset = let_stmt.syntax().text_range().end();
85 Some(TextEdit::insert(offset, ";".to_string()))
86}
87
88/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
89fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
90 assert_eq!(file.syntax().text().char_at(offset), Some('.'));
91 let whitespace =
92 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
93
94 let current_indent = {
95 let text = whitespace.text();
96 let newline = text.rfind('\n')?;
97 &text[newline + 1..]
98 };
99 let current_indent_len = TextSize::of(current_indent);
100
101 let parent = whitespace.syntax().parent();
102 // Make sure dot is a part of call chain
103 if !matches!(parent.kind(), FIELD_EXPR | METHOD_CALL_EXPR) {
104 return None;
105 }
106 let prev_indent = IndentLevel::from_node(&parent);
107 let target_indent = format!(" {}", prev_indent);
108 let target_indent_len = TextSize::of(&target_indent);
109 if current_indent_len == target_indent_len {
110 return None;
111 }
112
113 Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
114}
115
116/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
117fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
118 let file_text = file.syntax().text();
119 assert_eq!(file_text.char_at(offset), Some('>'));
120 let after_arrow = offset + TextSize::of('>');
121 if file_text.char_at(after_arrow) != Some('{') {
122 return None;
123 }
124 if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
125 return None;
126 }
127
128 Some(TextEdit::insert(after_arrow, " ".to_string()))
129}
130
131#[cfg(test)]
132mod tests {
133 use test_utils::{assert_eq_text, extract_offset};
134
135 use super::*;
136
137 fn do_type_char(char_typed: char, before: &str) -> Option<String> {
138 let (offset, before) = extract_offset(before);
139 let edit = TextEdit::insert(offset, char_typed.to_string());
140 let mut before = before.to_string();
141 edit.apply(&mut before);
142 let parse = SourceFile::parse(&before);
143 on_char_typed_inner(&parse.tree(), offset, char_typed).map(|it| {
144 it.apply(&mut before);
145 before.to_string()
146 })
147 }
148
149 fn type_char(char_typed: char, ra_fixture_before: &str, ra_fixture_after: &str) {
150 let actual = do_type_char(char_typed, ra_fixture_before)
151 .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
152
153 assert_eq_text!(ra_fixture_after, &actual);
154 }
155
156 fn type_char_noop(char_typed: char, before: &str) {
157 let file_change = do_type_char(char_typed, before);
158 assert!(file_change.is_none())
159 }
160
161 #[test]
162 fn test_on_eq_typed() {
163 // do_check(r"
164 // fn foo() {
165 // let foo =<|>
166 // }
167 // ", r"
168 // fn foo() {
169 // let foo =;
170 // }
171 // ");
172 type_char(
173 '=',
174 r"
175fn foo() {
176 let foo <|> 1 + 1
177}
178",
179 r"
180fn foo() {
181 let foo = 1 + 1;
182}
183",
184 );
185 // do_check(r"
186 // fn foo() {
187 // let foo =<|>
188 // let bar = 1;
189 // }
190 // ", r"
191 // fn foo() {
192 // let foo =;
193 // let bar = 1;
194 // }
195 // ");
196 }
197
198 #[test]
199 fn indents_new_chain_call() {
200 type_char(
201 '.',
202 r"
203 fn main() {
204 xs.foo()
205 <|>
206 }
207 ",
208 r"
209 fn main() {
210 xs.foo()
211 .
212 }
213 ",
214 );
215 type_char_noop(
216 '.',
217 r"
218 fn main() {
219 xs.foo()
220 <|>
221 }
222 ",
223 )
224 }
225
226 #[test]
227 fn indents_new_chain_call_with_semi() {
228 type_char(
229 '.',
230 r"
231 fn main() {
232 xs.foo()
233 <|>;
234 }
235 ",
236 r"
237 fn main() {
238 xs.foo()
239 .;
240 }
241 ",
242 );
243 type_char_noop(
244 '.',
245 r"
246 fn main() {
247 xs.foo()
248 <|>;
249 }
250 ",
251 )
252 }
253
254 #[test]
255 fn indents_new_chain_call_with_let() {
256 type_char(
257 '.',
258 r#"
259fn main() {
260 let _ = foo
261 <|>
262 bar()
263}
264"#,
265 r#"
266fn main() {
267 let _ = foo
268 .
269 bar()
270}
271"#,
272 );
273 }
274
275 #[test]
276 fn indents_continued_chain_call() {
277 type_char(
278 '.',
279 r"
280 fn main() {
281 xs.foo()
282 .first()
283 <|>
284 }
285 ",
286 r"
287 fn main() {
288 xs.foo()
289 .first()
290 .
291 }
292 ",
293 );
294 type_char_noop(
295 '.',
296 r"
297 fn main() {
298 xs.foo()
299 .first()
300 <|>
301 }
302 ",
303 );
304 }
305
306 #[test]
307 fn indents_middle_of_chain_call() {
308 type_char(
309 '.',
310 r"
311 fn source_impl() {
312 let var = enum_defvariant_list().unwrap()
313 <|>
314 .nth(92)
315 .unwrap();
316 }
317 ",
318 r"
319 fn source_impl() {
320 let var = enum_defvariant_list().unwrap()
321 .
322 .nth(92)
323 .unwrap();
324 }
325 ",
326 );
327 type_char_noop(
328 '.',
329 r"
330 fn source_impl() {
331 let var = enum_defvariant_list().unwrap()
332 <|>
333 .nth(92)
334 .unwrap();
335 }
336 ",
337 );
338 }
339
340 #[test]
341 fn dont_indent_freestanding_dot() {
342 type_char_noop(
343 '.',
344 r"
345 fn main() {
346 <|>
347 }
348 ",
349 );
350 type_char_noop(
351 '.',
352 r"
353 fn main() {
354 <|>
355 }
356 ",
357 );
358 }
359
360 #[test]
361 fn adds_space_after_return_type() {
362 type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -> { 92 }")
363 }
364}