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