aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/typing.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/typing.rs')
-rw-r--r--crates/ra_ide/src/typing.rs490
1 files changed, 490 insertions, 0 deletions
diff --git a/crates/ra_ide/src/typing.rs b/crates/ra_ide/src/typing.rs
new file mode 100644
index 000000000..21e5be9b3
--- /dev/null
+++ b/crates/ra_ide/src/typing.rs
@@ -0,0 +1,490 @@
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
16use ra_db::{FilePosition, SourceDatabase};
17use ra_fmt::leading_indent;
18use ra_syntax::{
19 algo::find_node_at_offset,
20 ast::{self, AstToken},
21 AstNode, SmolStr, SourceFile,
22 SyntaxKind::*,
23 SyntaxToken, TextRange, TextUnit, TokenAtOffset,
24};
25use ra_text_edit::TextEdit;
26
27use crate::{db::RootDatabase, source_change::SingleFileChange, SourceChange, SourceFileEdit};
28
29pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
30 let parse = db.parse(position.file_id);
31 let file = parse.tree();
32 let comment = file
33 .syntax()
34 .token_at_offset(position.offset)
35 .left_biased()
36 .and_then(ast::Comment::cast)?;
37
38 if comment.kind().shape.is_block() {
39 return None;
40 }
41
42 let prefix = comment.prefix();
43 let comment_range = comment.syntax().text_range();
44 if position.offset < comment_range.start() + TextUnit::of_str(prefix) + TextUnit::from(1) {
45 return None;
46 }
47
48 // Continuing non-doc line comments (like this one :) ) is annoying
49 if prefix == "//" && comment_range.end() == position.offset {
50 return None;
51 }
52
53 let indent = node_indent(&file, comment.syntax())?;
54 let inserted = format!("\n{}{} ", indent, prefix);
55 let cursor_position = position.offset + TextUnit::of_str(&inserted);
56 let edit = TextEdit::insert(position.offset, inserted);
57
58 Some(
59 SourceChange::source_file_edit(
60 "on enter",
61 SourceFileEdit { edit, file_id: position.file_id },
62 )
63 .with_cursor(FilePosition { offset: cursor_position, file_id: position.file_id }),
64 )
65}
66
67fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
68 let ws = match file.syntax().token_at_offset(token.text_range().start()) {
69 TokenAtOffset::Between(l, r) => {
70 assert!(r == *token);
71 l
72 }
73 TokenAtOffset::Single(n) => {
74 assert!(n == *token);
75 return Some("".into());
76 }
77 TokenAtOffset::None => unreachable!(),
78 };
79 if ws.kind() != WHITESPACE {
80 return None;
81 }
82 let text = ws.text();
83 let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
84 Some(text[pos..].into())
85}
86
87pub(crate) const TRIGGER_CHARS: &str = ".=>";
88
89pub(crate) fn on_char_typed(
90 db: &RootDatabase,
91 position: FilePosition,
92 char_typed: char,
93) -> Option<SourceChange> {
94 assert!(TRIGGER_CHARS.contains(char_typed));
95 let file = &db.parse(position.file_id).tree();
96 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
97 let single_file_change = on_char_typed_inner(file, position.offset, char_typed)?;
98 Some(single_file_change.into_source_change(position.file_id))
99}
100
101fn on_char_typed_inner(
102 file: &SourceFile,
103 offset: TextUnit,
104 char_typed: char,
105) -> Option<SingleFileChange> {
106 assert!(TRIGGER_CHARS.contains(char_typed));
107 match char_typed {
108 '.' => on_dot_typed(file, offset),
109 '=' => on_eq_typed(file, offset),
110 '>' => on_arrow_typed(file, offset),
111 _ => unreachable!(),
112 }
113}
114
115/// Returns an edit which should be applied after `=` was typed. Primarily,
116/// this works when adding `let =`.
117// FIXME: use a snippet completion instead of this hack here.
118fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> {
119 assert_eq!(file.syntax().text().char_at(offset), Some('='));
120 let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
121 if let_stmt.has_semi() {
122 return None;
123 }
124 if let Some(expr) = let_stmt.initializer() {
125 let expr_range = expr.syntax().text_range();
126 if expr_range.contains(offset) && offset != expr_range.start() {
127 return None;
128 }
129 if file.syntax().text().slice(offset..expr_range.start()).contains_char('\n') {
130 return None;
131 }
132 } else {
133 return None;
134 }
135 let offset = let_stmt.syntax().text_range().end();
136 Some(SingleFileChange {
137 label: "add semicolon".to_string(),
138 edit: TextEdit::insert(offset, ";".to_string()),
139 cursor_position: None,
140 })
141}
142
143/// Returns an edit which should be applied when a dot ('.') is typed on a blank line, indenting the line appropriately.
144fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> {
145 assert_eq!(file.syntax().text().char_at(offset), Some('.'));
146 let whitespace =
147 file.syntax().token_at_offset(offset).left_biased().and_then(ast::Whitespace::cast)?;
148
149 let current_indent = {
150 let text = whitespace.text();
151 let newline = text.rfind('\n')?;
152 &text[newline + 1..]
153 };
154 let current_indent_len = TextUnit::of_str(current_indent);
155
156 // Make sure dot is a part of call chain
157 let field_expr = ast::FieldExpr::cast(whitespace.syntax().parent())?;
158 let prev_indent = leading_indent(field_expr.syntax())?;
159 let target_indent = format!(" {}", prev_indent);
160 let target_indent_len = TextUnit::of_str(&target_indent);
161 if current_indent_len == target_indent_len {
162 return None;
163 }
164
165 Some(SingleFileChange {
166 label: "reindent dot".to_string(),
167 edit: TextEdit::replace(
168 TextRange::from_to(offset - current_indent_len, offset),
169 target_indent,
170 ),
171 cursor_position: Some(
172 offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
173 ),
174 })
175}
176
177/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
178fn on_arrow_typed(file: &SourceFile, offset: TextUnit) -> Option<SingleFileChange> {
179 let file_text = file.syntax().text();
180 assert_eq!(file_text.char_at(offset), Some('>'));
181 let after_arrow = offset + TextUnit::of_char('>');
182 if file_text.char_at(after_arrow) != Some('{') {
183 return None;
184 }
185 if find_node_at_offset::<ast::RetType>(file.syntax(), offset).is_none() {
186 return None;
187 }
188
189 Some(SingleFileChange {
190 label: "add space after return type".to_string(),
191 edit: TextEdit::insert(after_arrow, " ".to_string()),
192 cursor_position: Some(after_arrow),
193 })
194}
195
196#[cfg(test)]
197mod tests {
198 use test_utils::{add_cursor, assert_eq_text, extract_offset};
199
200 use crate::mock_analysis::single_file;
201
202 use super::*;
203
204 #[test]
205 fn test_on_enter() {
206 fn apply_on_enter(before: &str) -> Option<String> {
207 let (offset, before) = extract_offset(before);
208 let (analysis, file_id) = single_file(&before);
209 let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
210
211 assert_eq!(result.source_file_edits.len(), 1);
212 let actual = result.source_file_edits[0].edit.apply(&before);
213 let actual = add_cursor(&actual, result.cursor_position.unwrap().offset);
214 Some(actual)
215 }
216
217 fn do_check(before: &str, after: &str) {
218 let actual = apply_on_enter(before).unwrap();
219 assert_eq_text!(after, &actual);
220 }
221
222 fn do_check_noop(text: &str) {
223 assert!(apply_on_enter(text).is_none())
224 }
225
226 do_check(
227 r"
228/// Some docs<|>
229fn foo() {
230}
231",
232 r"
233/// Some docs
234/// <|>
235fn foo() {
236}
237",
238 );
239 do_check(
240 r"
241impl S {
242 /// Some<|> docs.
243 fn foo() {}
244}
245",
246 r"
247impl S {
248 /// Some
249 /// <|> docs.
250 fn foo() {}
251}
252",
253 );
254 do_check(
255 r"
256fn main() {
257 // Fix<|> me
258 let x = 1 + 1;
259}
260",
261 r"
262fn main() {
263 // Fix
264 // <|> me
265 let x = 1 + 1;
266}
267",
268 );
269 do_check_noop(
270 r"
271fn main() {
272 // Fix me<|>
273 let x = 1 + 1;
274}
275",
276 );
277
278 do_check_noop(r"<|>//! docz");
279 }
280
281 fn do_type_char(char_typed: char, before: &str) -> Option<(String, SingleFileChange)> {
282 let (offset, before) = extract_offset(before);
283 let edit = TextEdit::insert(offset, char_typed.to_string());
284 let before = edit.apply(&before);
285 let parse = SourceFile::parse(&before);
286 on_char_typed_inner(&parse.tree(), offset, char_typed)
287 .map(|it| (it.edit.apply(&before), it))
288 }
289
290 fn type_char(char_typed: char, before: &str, after: &str) {
291 let (actual, file_change) = do_type_char(char_typed, before)
292 .unwrap_or_else(|| panic!("typing `{}` did nothing", char_typed));
293
294 if after.contains("<|>") {
295 let (offset, after) = extract_offset(after);
296 assert_eq_text!(&after, &actual);
297 assert_eq!(file_change.cursor_position, Some(offset))
298 } else {
299 assert_eq_text!(after, &actual);
300 }
301 }
302
303 fn type_char_noop(char_typed: char, before: &str) {
304 let file_change = do_type_char(char_typed, before);
305 assert!(file_change.is_none())
306 }
307
308 #[test]
309 fn test_on_eq_typed() {
310 // do_check(r"
311 // fn foo() {
312 // let foo =<|>
313 // }
314 // ", r"
315 // fn foo() {
316 // let foo =;
317 // }
318 // ");
319 type_char(
320 '=',
321 r"
322fn foo() {
323 let foo <|> 1 + 1
324}
325",
326 r"
327fn foo() {
328 let foo = 1 + 1;
329}
330",
331 );
332 // do_check(r"
333 // fn foo() {
334 // let foo =<|>
335 // let bar = 1;
336 // }
337 // ", r"
338 // fn foo() {
339 // let foo =;
340 // let bar = 1;
341 // }
342 // ");
343 }
344
345 #[test]
346 fn indents_new_chain_call() {
347 type_char(
348 '.',
349 r"
350 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
351 self.child_impl(db, name)
352 <|>
353 }
354 ",
355 r"
356 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
357 self.child_impl(db, name)
358 .
359 }
360 ",
361 );
362 type_char_noop(
363 '.',
364 r"
365 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
366 self.child_impl(db, name)
367 <|>
368 }
369 ",
370 )
371 }
372
373 #[test]
374 fn indents_new_chain_call_with_semi() {
375 type_char(
376 '.',
377 r"
378 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
379 self.child_impl(db, name)
380 <|>;
381 }
382 ",
383 r"
384 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
385 self.child_impl(db, name)
386 .;
387 }
388 ",
389 );
390 type_char_noop(
391 '.',
392 r"
393 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
394 self.child_impl(db, name)
395 <|>;
396 }
397 ",
398 )
399 }
400
401 #[test]
402 fn indents_continued_chain_call() {
403 type_char(
404 '.',
405 r"
406 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
407 self.child_impl(db, name)
408 .first()
409 <|>
410 }
411 ",
412 r"
413 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
414 self.child_impl(db, name)
415 .first()
416 .
417 }
418 ",
419 );
420 type_char_noop(
421 '.',
422 r"
423 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
424 self.child_impl(db, name)
425 .first()
426 <|>
427 }
428 ",
429 );
430 }
431
432 #[test]
433 fn indents_middle_of_chain_call() {
434 type_char(
435 '.',
436 r"
437 fn source_impl() {
438 let var = enum_defvariant_list().unwrap()
439 <|>
440 .nth(92)
441 .unwrap();
442 }
443 ",
444 r"
445 fn source_impl() {
446 let var = enum_defvariant_list().unwrap()
447 .
448 .nth(92)
449 .unwrap();
450 }
451 ",
452 );
453 type_char_noop(
454 '.',
455 r"
456 fn source_impl() {
457 let var = enum_defvariant_list().unwrap()
458 <|>
459 .nth(92)
460 .unwrap();
461 }
462 ",
463 );
464 }
465
466 #[test]
467 fn dont_indent_freestanding_dot() {
468 type_char_noop(
469 '.',
470 r"
471 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
472 <|>
473 }
474 ",
475 );
476 type_char_noop(
477 '.',
478 r"
479 pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
480 <|>
481 }
482 ",
483 );
484 }
485
486 #[test]
487 fn adds_space_after_return_type() {
488 type_char('>', "fn foo() -<|>{ 92 }", "fn foo() -><|> { 92 }")
489 }
490}