1use ide_db::base_db::RootQueryDb;
5use ide_db::{FilePosition, RootDatabase};
6use syntax::{
7 AstNode, SmolStr, SourceFile,
8 SyntaxKind::*,
9 SyntaxNode, SyntaxToken, TextRange, TextSize, TokenAtOffset,
10 algo::find_node_at_offset,
11 ast::{self, AstToken, edit::IndentLevel},
12};
13
14use ide_db::text_edit::TextEdit;
15
16pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
53 let editioned_file_id_wrapper =
54 ide_db::base_db::EditionedFileId::current_edition_guess_origin(db, position.file_id);
55 let parse = db.parse(editioned_file_id_wrapper);
56 let file = parse.tree();
57 let token = file.syntax().token_at_offset(position.offset).left_biased()?;
58
59 if let Some(comment) = ast::Comment::cast(token.clone()) {
60 return on_enter_in_comment(&comment, &file, position.offset);
61 }
62
63 if token.kind() == L_CURLY {
64 if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
66 .and_then(|block| on_enter_in_block(block, position))
67 {
68 cov_mark::hit!(indent_block_contents);
69 return Some(edit);
70 }
71
72 if let Some(edit) = find_node_at_offset(file.syntax(), position.offset - TextSize::of('{'))
74 .and_then(|list| on_enter_in_use_tree_list(list, position))
75 {
76 cov_mark::hit!(indent_block_contents);
77 return Some(edit);
78 }
79 }
80
81 None
82}
83
84fn on_enter_in_comment(
85 comment: &ast::Comment,
86 file: &ast::SourceFile,
87 offset: TextSize,
88) -> Option<TextEdit> {
89 if comment.kind().shape.is_block() {
90 return None;
91 }
92
93 let prefix = comment.prefix();
94 let comment_range = comment.syntax().text_range();
95 if offset < comment_range.start() + TextSize::of(prefix) {
96 return None;
97 }
98
99 let mut remove_trailing_whitespace = false;
100 if prefix == "//" && comment_range.end() == offset {
102 if comment.text().ends_with(' ') {
103 cov_mark::hit!(continues_end_of_line_comment_with_space);
104 remove_trailing_whitespace = true;
105 } else if !followed_by_comment(comment) {
106 return None;
107 }
108 }
109
110 let indent = node_indent(file, comment.syntax())?;
111 let inserted = format!("\n{indent}{prefix} $0");
112 let delete = if remove_trailing_whitespace {
113 let trimmed_len = comment.text().trim_end().len() as u32;
114 let trailing_whitespace_len = comment.text().len() as u32 - trimmed_len;
115 TextRange::new(offset - TextSize::from(trailing_whitespace_len), offset)
116 } else {
117 TextRange::empty(offset)
118 };
119 let edit = TextEdit::replace(delete, inserted);
120 Some(edit)
121}
122
123fn on_enter_in_block(block: ast::BlockExpr, position: FilePosition) -> Option<TextEdit> {
124 let contents = block_contents(&block)?;
125
126 if block.syntax().text().contains_char('\n') {
127 return None;
128 }
129
130 let indent = IndentLevel::from_node(block.syntax());
131 let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
132 edit.union(TextEdit::insert(contents.text_range().end(), format!("\n{indent}"))).ok()?;
133 Some(edit)
134}
135
136fn on_enter_in_use_tree_list(list: ast::UseTreeList, position: FilePosition) -> Option<TextEdit> {
137 if list.syntax().text().contains_char('\n') {
138 return None;
139 }
140
141 let indent = IndentLevel::from_node(list.syntax());
142 let mut edit = TextEdit::insert(position.offset, format!("\n{}$0", indent + 1));
143 edit.union(TextEdit::insert(list.r_curly_token()?.text_range().start(), format!("\n{indent}")))
144 .ok()?;
145 Some(edit)
146}
147
148fn block_contents(block: &ast::BlockExpr) -> Option<SyntaxNode> {
149 let mut node = block.tail_expr().map(|e| e.syntax().clone());
150
151 for stmt in block.statements() {
152 if node.is_some() {
153 return None;
155 }
156
157 node = Some(stmt.syntax().clone());
158 }
159
160 node
161}
162
163fn followed_by_comment(comment: &ast::Comment) -> bool {
164 let ws = match comment.syntax().next_token().and_then(ast::Whitespace::cast) {
165 Some(it) => it,
166 None => return false,
167 };
168 if ws.spans_multiple_lines() {
169 return false;
170 }
171 ws.syntax().next_token().and_then(ast::Comment::cast).is_some()
172}
173
174fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option<SmolStr> {
175 let ws = match file.syntax().token_at_offset(token.text_range().start()) {
176 TokenAtOffset::Between(l, r) => {
177 assert!(r == *token);
178 l
179 }
180 TokenAtOffset::Single(n) => {
181 assert!(n == *token);
182 return Some("".into());
183 }
184 TokenAtOffset::None => unreachable!(),
185 };
186 if ws.kind() != WHITESPACE {
187 return None;
188 }
189 let text = ws.text();
190 let pos = text.rfind('\n').map(|it| it + 1).unwrap_or(0);
191 Some(text[pos..].into())
192}
193
194#[cfg(test)]
195mod tests {
196 use stdx::trim_indent;
197 use test_utils::assert_eq_text;
198
199 use crate::fixture;
200
201 fn apply_on_enter(before: &str) -> Option<String> {
202 let (analysis, position) = fixture::position(before);
203 let result = analysis.on_enter(position).unwrap()?;
204
205 let mut actual = analysis.file_text(position.file_id).unwrap().to_string();
206 result.apply(&mut actual);
207 Some(actual)
208 }
209
210 fn do_check(
211 #[rust_analyzer::rust_fixture] ra_fixture_before: &str,
212 #[rust_analyzer::rust_fixture] ra_fixture_after: &str,
213 ) {
214 let ra_fixture_after = &trim_indent(ra_fixture_after);
215 let actual = apply_on_enter(ra_fixture_before).unwrap();
216 assert_eq_text!(ra_fixture_after, &actual);
217 }
218
219 fn do_check_noop(ra_fixture_text: &str) {
220 assert!(apply_on_enter(ra_fixture_text).is_none())
221 }
222
223 #[test]
224 fn continues_doc_comment() {
225 do_check(
226 r"
227/// Some docs$0
228fn foo() {
229}
230",
231 r"
232/// Some docs
233/// $0
234fn foo() {
235}
236",
237 );
238
239 do_check(
240 r"
241impl S {
242 /// Some$0 docs.
243 fn foo() {}
244}
245",
246 r"
247impl S {
248 /// Some
249 /// $0 docs.
250 fn foo() {}
251}
252",
253 );
254
255 do_check(
256 r"
257///$0 Some docs
258fn foo() {
259}
260",
261 r"
262///
263/// $0 Some docs
264fn foo() {
265}
266",
267 );
268 }
269
270 #[test]
271 fn does_not_continue_before_doc_comment() {
272 do_check_noop(r"$0//! docz");
273 }
274
275 #[test]
276 fn continues_another_doc_comment() {
277 do_check(
278 r#"
279fn main() {
280 //! Documentation for$0 on enter
281 let x = 1 + 1;
282}
283"#,
284 r#"
285fn main() {
286 //! Documentation for
287 //! $0 on enter
288 let x = 1 + 1;
289}
290"#,
291 );
292 }
293
294 #[test]
295 fn continues_code_comment_in_the_middle_of_line() {
296 do_check(
297 r"
298fn main() {
299 // Fix$0 me
300 let x = 1 + 1;
301}
302",
303 r"
304fn main() {
305 // Fix
306 // $0 me
307 let x = 1 + 1;
308}
309",
310 );
311 }
312
313 #[test]
314 fn continues_code_comment_in_the_middle_several_lines() {
315 do_check(
316 r"
317fn main() {
318 // Fix$0
319 // me
320 let x = 1 + 1;
321}
322",
323 r"
324fn main() {
325 // Fix
326 // $0
327 // me
328 let x = 1 + 1;
329}
330",
331 );
332 }
333
334 #[test]
335 fn does_not_continue_end_of_line_comment() {
336 do_check_noop(
337 r"
338fn main() {
339 // Fix me$0
340 let x = 1 + 1;
341}
342",
343 );
344 }
345
346 #[test]
347 fn continues_end_of_line_comment_with_space() {
348 cov_mark::check!(continues_end_of_line_comment_with_space);
349 do_check(
350 r#"
351fn main() {
352 // Fix me $0
353 let x = 1 + 1;
354}
355"#,
356 r#"
357fn main() {
358 // Fix me
359 // $0
360 let x = 1 + 1;
361}
362"#,
363 );
364 }
365
366 #[test]
367 fn trims_all_trailing_whitespace() {
368 do_check(
369 "
370fn main() {
371 // Fix me \t\t $0
372 let x = 1 + 1;
373}
374",
375 "
376fn main() {
377 // Fix me
378 // $0
379 let x = 1 + 1;
380}
381",
382 );
383 }
384
385 #[test]
386 fn indents_fn_body_block() {
387 cov_mark::check!(indent_block_contents);
388 do_check(
389 r#"
390fn f() {$0()}
391 "#,
392 r#"
393fn f() {
394 $0()
395}
396 "#,
397 );
398 }
399
400 #[test]
401 fn indents_block_expr() {
402 do_check(
403 r#"
404fn f() {
405 let x = {$0()};
406}
407 "#,
408 r#"
409fn f() {
410 let x = {
411 $0()
412 };
413}
414 "#,
415 );
416 }
417
418 #[test]
419 fn indents_match_arm() {
420 do_check(
421 r#"
422fn f() {
423 match 6 {
424 1 => {$0f()},
425 _ => (),
426 }
427}
428 "#,
429 r#"
430fn f() {
431 match 6 {
432 1 => {
433 $0f()
434 },
435 _ => (),
436 }
437}
438 "#,
439 );
440 }
441
442 #[test]
443 fn indents_block_with_statement() {
444 do_check(
445 r#"
446fn f() {$0a = b}
447 "#,
448 r#"
449fn f() {
450 $0a = b
451}
452 "#,
453 );
454 do_check(
455 r#"
456fn f() {$0fn f() {}}
457 "#,
458 r#"
459fn f() {
460 $0fn f() {}
461}
462 "#,
463 );
464 }
465
466 #[test]
467 fn indents_nested_blocks() {
468 do_check(
469 r#"
470fn f() {$0{}}
471 "#,
472 r#"
473fn f() {
474 $0{}
475}
476 "#,
477 );
478 }
479
480 #[test]
481 fn does_not_indent_empty_block() {
482 do_check_noop(
483 r#"
484fn f() {$0}
485 "#,
486 );
487 do_check_noop(
488 r#"
489fn f() {{$0}}
490 "#,
491 );
492 }
493
494 #[test]
495 fn does_not_indent_block_with_too_much_content() {
496 do_check_noop(
497 r#"
498fn f() {$0 a = b; ()}
499 "#,
500 );
501 do_check_noop(
502 r#"
503fn f() {$0 a = b; a = b; }
504 "#,
505 );
506 }
507
508 #[test]
509 fn does_not_indent_multiline_block() {
510 do_check_noop(
511 r#"
512fn f() {$0
513}
514 "#,
515 );
516 do_check_noop(
517 r#"
518fn f() {$0
519
520}
521 "#,
522 );
523 }
524
525 #[test]
526 fn indents_use_tree_list() {
527 do_check(
528 r#"
529use crate::{$0};
530 "#,
531 r#"
532use crate::{
533 $0
534};
535 "#,
536 );
537 do_check(
538 r#"
539use crate::{$0Object, path::to::OtherThing};
540 "#,
541 r#"
542use crate::{
543 $0Object, path::to::OtherThing
544};
545 "#,
546 );
547 do_check(
548 r#"
549use {crate::{$0Object, path::to::OtherThing}};
550 "#,
551 r#"
552use {crate::{
553 $0Object, path::to::OtherThing
554}};
555 "#,
556 );
557 do_check(
558 r#"
559use {
560 crate::{$0Object, path::to::OtherThing}
561};
562 "#,
563 r#"
564use {
565 crate::{
566 $0Object, path::to::OtherThing
567 }
568};
569 "#,
570 );
571 }
572
573 #[test]
574 fn does_not_indent_use_tree_list_when_not_at_curly_brace() {
575 do_check_noop(
576 r#"
577use path::{Thing$0};
578 "#,
579 );
580 }
581
582 #[test]
583 fn does_not_indent_use_tree_list_without_curly_braces() {
584 do_check_noop(
585 r#"
586use path::Thing$0;
587 "#,
588 );
589 do_check_noop(
590 r#"
591use path::$0Thing;
592 "#,
593 );
594 do_check_noop(
595 r#"
596use path::Thing$0};
597 "#,
598 );
599 do_check_noop(
600 r#"
601use path::{$0Thing;
602 "#,
603 );
604 }
605
606 #[test]
607 fn does_not_indent_multiline_use_tree_list() {
608 do_check_noop(
609 r#"
610use path::{$0
611 Thing
612};
613 "#,
614 );
615 }
616}