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