1use ide_assists::utils::extract_trivial_expression;
2use ide_db::syntax_helpers::node_ext::expr_as_name_ref;
3use itertools::Itertools;
4use syntax::{
5 NodeOrToken, SourceFile, SyntaxElement,
6 SyntaxKind::{self, USE_TREE, WHITESPACE},
7 SyntaxToken, T, TextRange, TextSize,
8 ast::{self, AstNode, AstToken, IsString},
9};
10
11use ide_db::text_edit::{TextEdit, TextEditBuilder};
12
13pub struct JoinLinesConfig {
14 pub join_else_if: bool,
15 pub remove_trailing_comma: bool,
16 pub unwrap_trivial_blocks: bool,
17 pub join_assignments: bool,
18}
19
20pub(crate) fn join_lines(
32 config: &JoinLinesConfig,
33 file: &SourceFile,
34 range: TextRange,
35) -> TextEdit {
36 let range = if range.is_empty() {
37 let syntax = file.syntax();
38 let text = syntax.text().slice(range.start()..);
39 let pos = match text.find_char('\n') {
40 None => return TextEdit::builder().finish(),
41 Some(pos) => pos,
42 };
43 TextRange::at(range.start() + pos, TextSize::of('\n'))
44 } else {
45 range
46 };
47
48 let mut edit = TextEdit::builder();
49 match file.syntax().covering_element(range) {
50 NodeOrToken::Node(node) => {
51 for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
52 remove_newlines(config, &mut edit, &token, range)
53 }
54 }
55 NodeOrToken::Token(token) => remove_newlines(config, &mut edit, &token, range),
56 };
57 edit.finish()
58}
59
60fn remove_newlines(
61 config: &JoinLinesConfig,
62 edit: &mut TextEditBuilder,
63 token: &SyntaxToken,
64 range: TextRange,
65) {
66 let intersection = match range.intersect(token.text_range()) {
67 Some(range) => range,
68 None => return,
69 };
70
71 let range = intersection - token.text_range().start();
72 let text = token.text();
73 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
74 let pos: TextSize = (pos as u32).into();
75 let offset = token.text_range().start() + range.start() + pos;
76 if !edit.invalidates_offset(offset) {
77 remove_newline(config, edit, token, offset);
78 }
79 }
80}
81
82fn remove_newline(
83 config: &JoinLinesConfig,
84 edit: &mut TextEditBuilder,
85 token: &SyntaxToken,
86 offset: TextSize,
87) {
88 if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
89 let n_spaces_after_line_break = {
90 let suff = &token.text()[TextRange::new(
91 offset - token.text_range().start() + TextSize::of('\n'),
92 TextSize::of(token.text()),
93 )];
94 suff.bytes().take_while(|&b| b == b' ').count()
95 };
96
97 let mut no_space = false;
98 if let Some(string) = ast::String::cast(token.clone()) {
99 if let Some(range) = string.open_quote_text_range() {
100 cov_mark::hit!(join_string_literal_open_quote);
101 no_space |= range.end() == offset;
102 }
103 if let Some(range) = string.close_quote_text_range() {
104 cov_mark::hit!(join_string_literal_close_quote);
105 no_space |= range.start()
106 == offset
107 + TextSize::of('\n')
108 + TextSize::try_from(n_spaces_after_line_break).unwrap();
109 }
110 }
111
112 let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into());
113 let replace_with = if no_space { "" } else { " " };
114 edit.replace(range, replace_with.to_owned());
115 return;
116 }
117
118 let (prev, next) = match (token.prev_sibling_or_token(), token.next_sibling_or_token()) {
120 (Some(prev), Some(next)) => (prev, next),
121 _ => return,
122 };
123
124 if config.remove_trailing_comma && prev.kind() == T![,] {
125 match next.kind() {
126 T![')'] | T![']'] => {
127 edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
129 return;
130 }
131 T!['}'] => {
132 let space = match prev.prev_sibling_or_token() {
134 Some(left) => compute_ws(left.kind(), next.kind()),
135 None => " ",
136 };
137 edit.replace(
138 TextRange::new(prev.text_range().start(), token.text_range().end()),
139 space.to_owned(),
140 );
141 return;
142 }
143 _ => (),
144 }
145 }
146
147 if config.join_else_if
148 && let (Some(prev), Some(_next)) = (as_if_expr(&prev), as_if_expr(&next))
149 {
150 match prev.else_token() {
151 Some(_) => cov_mark::hit!(join_two_ifs_with_existing_else),
152 None => {
153 cov_mark::hit!(join_two_ifs);
154 edit.replace(token.text_range(), " else ".to_owned());
155 return;
156 }
157 }
158 }
159
160 if config.join_assignments && join_assignments(edit, &prev, &next).is_some() {
161 return;
162 }
163
164 if config.unwrap_trivial_blocks {
165 if join_single_expr_block(edit, token).is_some() {
175 return;
176 }
177 if join_single_use_tree(edit, token).is_some() {
185 return;
186 }
187 }
188
189 if let (Some(_), Some(next)) = (
190 prev.as_token().cloned().and_then(ast::Comment::cast),
191 next.as_token().cloned().and_then(ast::Comment::cast),
192 ) {
193 edit.delete(TextRange::new(
195 token.text_range().start(),
196 next.syntax().text_range().start() + TextSize::of(next.prefix()),
197 ));
198 return;
199 }
200
201 edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_owned());
203}
204
205fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
206 let block_expr = ast::BlockExpr::cast(token.parent_ancestors().nth(1)?)?;
207 if !block_expr.is_standalone() {
208 return None;
209 }
210 let expr = extract_trivial_expression(&block_expr)?;
211
212 let block_range = block_expr.syntax().text_range();
213 let mut buf = expr.syntax().text().to_string();
214
215 if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast)
217 && match_arm.comma_token().is_none()
218 {
219 buf.push(',');
220 }
221
222 edit.replace(block_range, buf);
223
224 Some(())
225}
226
227fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
228 let use_tree_list = ast::UseTreeList::cast(token.parent()?)?;
229 let (tree,) = use_tree_list.use_trees().collect_tuple()?;
230 edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
231 Some(())
232}
233
234fn join_assignments(
235 edit: &mut TextEditBuilder,
236 prev: &SyntaxElement,
237 next: &SyntaxElement,
238) -> Option<()> {
239 let let_stmt = ast::LetStmt::cast(prev.as_node()?.clone())?;
240 if let_stmt.eq_token().is_some() {
241 cov_mark::hit!(join_assignments_already_initialized);
242 return None;
243 }
244 let let_ident_pat = match let_stmt.pat()? {
245 ast::Pat::IdentPat(it) => it,
246 _ => return None,
247 };
248
249 let expr_stmt = ast::ExprStmt::cast(next.as_node()?.clone())?;
250 let bin_expr = match expr_stmt.expr()? {
251 ast::Expr::BinExpr(it) => it,
252 _ => return None,
253 };
254 if !matches!(bin_expr.op_kind()?, ast::BinaryOp::Assignment { op: None }) {
255 return None;
256 }
257 let lhs = bin_expr.lhs()?;
258 let name_ref = expr_as_name_ref(&lhs)?;
259
260 if name_ref.to_string() != let_ident_pat.syntax().to_string() {
261 cov_mark::hit!(join_assignments_mismatch);
262 return None;
263 }
264
265 edit.delete(let_stmt.semicolon_token()?.text_range().cover(lhs.syntax().text_range()));
266 Some(())
267}
268
269fn as_if_expr(element: &SyntaxElement) -> Option<ast::IfExpr> {
270 let mut node = element.as_node()?.clone();
271 if let Some(stmt) = ast::ExprStmt::cast(node.clone()) {
272 node = stmt.expr()?.syntax().clone();
273 }
274 ast::IfExpr::cast(node)
275}
276
277fn compute_ws(left: SyntaxKind, right: SyntaxKind) -> &'static str {
278 match left {
279 T!['('] | T!['['] => return "",
280 T!['{'] => {
281 if let USE_TREE = right {
282 return "";
283 }
284 }
285 _ => (),
286 }
287 match right {
288 T![')'] | T![']'] => return "",
289 T!['}'] => {
290 if let USE_TREE = left {
291 return "";
292 }
293 }
294 T![.] => return "",
295 _ => (),
296 }
297 " "
298}
299
300#[cfg(test)]
301mod tests {
302 use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range};
303
304 use super::*;
305
306 fn check_join_lines(
307 #[rust_analyzer::rust_fixture] ra_fixture_before: &str,
308 #[rust_analyzer::rust_fixture] ra_fixture_after: &str,
309 ) {
310 let config = JoinLinesConfig {
311 join_else_if: true,
312 remove_trailing_comma: true,
313 unwrap_trivial_blocks: true,
314 join_assignments: true,
315 };
316
317 let (before_cursor_pos, before) = extract_offset(ra_fixture_before);
318 let file = SourceFile::parse(&before, span::Edition::CURRENT).ok().unwrap();
319
320 let range = TextRange::empty(before_cursor_pos);
321 let result = join_lines(&config, &file, range);
322
323 let actual = {
324 let mut actual = before;
325 result.apply(&mut actual);
326 actual
327 };
328 let actual_cursor_pos = result
329 .apply_to_offset(before_cursor_pos)
330 .expect("cursor position is affected by the edit");
331 let actual = add_cursor(&actual, actual_cursor_pos);
332 assert_eq_text!(ra_fixture_after, &actual);
333 }
334
335 fn check_join_lines_sel(
336 #[rust_analyzer::rust_fixture] ra_fixture_before: &str,
337 #[rust_analyzer::rust_fixture] ra_fixture_after: &str,
338 ) {
339 let config = JoinLinesConfig {
340 join_else_if: true,
341 remove_trailing_comma: true,
342 unwrap_trivial_blocks: true,
343 join_assignments: true,
344 };
345
346 let (sel, before) = extract_range(ra_fixture_before);
347 let parse = SourceFile::parse(&before, span::Edition::CURRENT);
348 let result = join_lines(&config, &parse.tree(), sel);
349 let actual = {
350 let mut actual = before;
351 result.apply(&mut actual);
352 actual
353 };
354 assert_eq_text!(ra_fixture_after, &actual);
355 }
356
357 #[test]
358 fn test_join_lines_comma() {
359 check_join_lines(
360 r"
361fn foo() {
362 $0foo(1,
363 )
364}
365",
366 r"
367fn foo() {
368 $0foo(1)
369}
370",
371 );
372 }
373
374 #[test]
375 fn test_join_lines_lambda_block() {
376 check_join_lines(
377 r"
378pub fn reparse(&self, edit: &AtomTextEdit) -> File {
379 $0self.incremental_reparse(edit).unwrap_or_else(|| {
380 self.full_reparse(edit)
381 })
382}
383",
384 r"
385pub fn reparse(&self, edit: &AtomTextEdit) -> File {
386 $0self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
387}
388",
389 );
390 }
391
392 #[test]
393 fn test_join_lines_block() {
394 check_join_lines(
395 r"
396fn foo() {
397 foo($0{
398 92
399 })
400}",
401 r"
402fn foo() {
403 foo($092)
404}",
405 );
406 }
407
408 #[test]
409 fn test_join_lines_diverging_block() {
410 check_join_lines(
411 r"
412fn foo() {
413 loop {
414 match x {
415 92 => $0{
416 continue;
417 }
418 }
419 }
420}
421 ",
422 r"
423fn foo() {
424 loop {
425 match x {
426 92 => $0continue,
427 }
428 }
429}
430 ",
431 );
432 }
433
434 #[test]
435 fn join_lines_adds_comma_for_block_in_match_arm() {
436 check_join_lines(
437 r"
438fn foo(e: Result<U, V>) {
439 match e {
440 Ok(u) => $0{
441 u.foo()
442 }
443 Err(v) => v,
444 }
445}",
446 r"
447fn foo(e: Result<U, V>) {
448 match e {
449 Ok(u) => $0u.foo(),
450 Err(v) => v,
451 }
452}",
453 );
454 }
455
456 #[test]
457 fn join_lines_multiline_in_block() {
458 check_join_lines(
459 r"
460fn foo() {
461 match ty {
462 $0 Some(ty) => {
463 match ty {
464 _ => false,
465 }
466 }
467 _ => true,
468 }
469}
470",
471 r"
472fn foo() {
473 match ty {
474 $0 Some(ty) => match ty {
475 _ => false,
476 },
477 _ => true,
478 }
479}
480",
481 );
482 }
483
484 #[test]
485 fn join_lines_keeps_comma_for_block_in_match_arm() {
486 check_join_lines(
488 r"
489fn foo(e: Result<U, V>) {
490 match e {
491 Ok(u) => $0{
492 u.foo()
493 },
494 Err(v) => v,
495 }
496}",
497 r"
498fn foo(e: Result<U, V>) {
499 match e {
500 Ok(u) => $0u.foo(),
501 Err(v) => v,
502 }
503}",
504 );
505
506 check_join_lines(
508 r"
509fn foo(e: Result<U, V>) {
510 match e {
511 Ok(u) => $0{
512 u.foo()
513 } ,
514 Err(v) => v,
515 }
516}",
517 r"
518fn foo(e: Result<U, V>) {
519 match e {
520 Ok(u) => $0u.foo() ,
521 Err(v) => v,
522 }
523}",
524 );
525
526 check_join_lines(
528 r"
529fn foo(e: Result<U, V>) {
530 match e {
531 Ok(u) => $0{
532 u.foo()
533 }
534 ,
535 Err(v) => v,
536 }
537}",
538 r"
539fn foo(e: Result<U, V>) {
540 match e {
541 Ok(u) => $0u.foo()
542 ,
543 Err(v) => v,
544 }
545}",
546 );
547 }
548
549 #[test]
550 fn join_lines_keeps_comma_with_single_arg_tuple() {
551 check_join_lines(
553 r"
554fn foo() {
555 let x = ($0{
556 4
557 },);
558}",
559 r"
560fn foo() {
561 let x = ($04,);
562}",
563 );
564
565 check_join_lines(
567 r"
568fn foo() {
569 let x = ($0{
570 4
571 } ,);
572}",
573 r"
574fn foo() {
575 let x = ($04 ,);
576}",
577 );
578
579 check_join_lines(
581 r"
582fn foo() {
583 let x = ($0{
584 4
585 }
586 ,);
587}",
588 r"
589fn foo() {
590 let x = ($04
591 ,);
592}",
593 );
594 }
595
596 #[test]
597 fn test_join_lines_use_items_left() {
598 check_join_lines(
600 r"
601$0use syntax::{
602 TextSize, TextRange,
603};",
604 r"
605$0use syntax::{TextSize, TextRange,
606};",
607 );
608 }
609
610 #[test]
611 fn test_join_lines_use_items_right() {
612 check_join_lines(
614 r"
615use syntax::{
616$0 TextSize, TextRange
617};",
618 r"
619use syntax::{
620$0 TextSize, TextRange};",
621 );
622 }
623
624 #[test]
625 fn test_join_lines_use_items_right_comma() {
626 check_join_lines(
628 r"
629use syntax::{
630$0 TextSize, TextRange,
631};",
632 r"
633use syntax::{
634$0 TextSize, TextRange};",
635 );
636 }
637
638 #[test]
639 fn test_join_lines_use_tree() {
640 check_join_lines(
641 r"
642use syntax::{
643 algo::$0{
644 find_token_at_offset,
645 },
646 ast,
647};",
648 r"
649use syntax::{
650 algo::$0find_token_at_offset,
651 ast,
652};",
653 );
654 }
655
656 #[test]
657 fn test_join_lines_normal_comments() {
658 check_join_lines(
659 r"
660fn foo() {
661 // Hello$0
662 // world!
663}
664",
665 r"
666fn foo() {
667 // Hello$0 world!
668}
669",
670 );
671 }
672
673 #[test]
674 fn test_join_lines_doc_comments() {
675 check_join_lines(
676 r"
677fn foo() {
678 /// Hello$0
679 /// world!
680}
681",
682 r"
683fn foo() {
684 /// Hello$0 world!
685}
686",
687 );
688 }
689
690 #[test]
691 fn test_join_lines_mod_comments() {
692 check_join_lines(
693 r"
694fn foo() {
695 //! Hello$0
696 //! world!
697}
698",
699 r"
700fn foo() {
701 //! Hello$0 world!
702}
703",
704 );
705 }
706
707 #[test]
708 fn test_join_lines_multiline_comments_1() {
709 check_join_lines(
710 r"
711fn foo() {
712 // Hello$0
713 /* world! */
714}
715",
716 r"
717fn foo() {
718 // Hello$0 world! */
719}
720",
721 );
722 }
723
724 #[test]
725 fn test_join_lines_multiline_comments_2() {
726 check_join_lines(
727 r"
728fn foo() {
729 // The$0
730 /* quick
731 brown
732 fox! */
733}
734",
735 r"
736fn foo() {
737 // The$0 quick
738 brown
739 fox! */
740}
741",
742 );
743 }
744
745 #[test]
746 fn test_join_lines_selection_fn_args() {
747 check_join_lines_sel(
748 r"
749fn foo() {
750 $0foo(1,
751 2,
752 3,
753 $0)
754}
755 ",
756 r"
757fn foo() {
758 foo(1, 2, 3)
759}
760 ",
761 );
762 }
763
764 #[test]
765 fn test_join_lines_selection_struct() {
766 check_join_lines_sel(
767 r"
768struct Foo $0{
769 f: u32,
770}$0
771 ",
772 r"
773struct Foo { f: u32 }
774 ",
775 );
776 }
777
778 #[test]
779 fn test_join_lines_selection_dot_chain() {
780 check_join_lines_sel(
781 r"
782fn foo() {
783 join($0type_params.type_params()
784 .filter_map(|it| it.name())
785 .map(|it| it.text())$0)
786}",
787 r"
788fn foo() {
789 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
790}",
791 );
792 }
793
794 #[test]
795 fn test_join_lines_selection_lambda_block_body() {
796 check_join_lines_sel(
797 r"
798pub fn handle_find_matching_brace() {
799 params.offsets
800 .map(|offset| $0{
801 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
802 }$0)
803 .collect();
804}",
805 r"
806pub fn handle_find_matching_brace() {
807 params.offsets
808 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
809 .collect();
810}",
811 );
812 }
813
814 #[test]
815 fn test_join_lines_commented_block() {
816 check_join_lines(
817 r"
818fn main() {
819 let _ = {
820 // $0foo
821 // bar
822 92
823 };
824}
825 ",
826 r"
827fn main() {
828 let _ = {
829 // $0foo bar
830 92
831 };
832}
833 ",
834 )
835 }
836
837 #[test]
838 fn join_lines_mandatory_blocks_block() {
839 check_join_lines(
840 r"
841$0fn foo() {
842 92
843}
844 ",
845 r"
846$0fn foo() { 92
847}
848 ",
849 );
850
851 check_join_lines(
852 r"
853fn foo() {
854 $0if true {
855 92
856 }
857}
858 ",
859 r"
860fn foo() {
861 $0if true { 92
862 }
863}
864 ",
865 );
866
867 check_join_lines(
868 r"
869fn foo() {
870 $0loop {
871 92
872 }
873}
874 ",
875 r"
876fn foo() {
877 $0loop { 92
878 }
879}
880 ",
881 );
882
883 check_join_lines(
884 r"
885fn foo() {
886 $0unsafe {
887 92
888 }
889}
890 ",
891 r"
892fn foo() {
893 $0unsafe { 92
894 }
895}
896 ",
897 );
898 }
899
900 #[test]
901 fn join_string_literal() {
902 {
903 cov_mark::check!(join_string_literal_open_quote);
904 check_join_lines(
905 r#"
906fn main() {
907 $0"
908hello
909";
910}
911"#,
912 r#"
913fn main() {
914 $0"hello
915";
916}
917"#,
918 );
919 }
920
921 {
922 cov_mark::check!(join_string_literal_close_quote);
923 check_join_lines(
924 r#"
925fn main() {
926 $0"hello
927";
928}
929"#,
930 r#"
931fn main() {
932 $0"hello";
933}
934"#,
935 );
936 check_join_lines(
937 r#"
938fn main() {
939 $0r"hello
940 ";
941}
942"#,
943 r#"
944fn main() {
945 $0r"hello";
946}
947"#,
948 );
949 }
950
951 check_join_lines(
952 r#"
953fn main() {
954 "
955$0hello
956world
957";
958}
959"#,
960 r#"
961fn main() {
962 "
963$0hello world
964";
965}
966"#,
967 );
968 }
969
970 #[test]
971 fn join_last_line_empty() {
972 check_join_lines(
973 r#"
974fn main() {$0}
975"#,
976 r#"
977fn main() {$0}
978"#,
979 );
980 }
981
982 #[test]
983 fn join_two_ifs() {
984 cov_mark::check!(join_two_ifs);
985 check_join_lines(
986 r#"
987fn main() {
988 if foo {
989
990 }$0
991 if bar {
992
993 }
994}
995"#,
996 r#"
997fn main() {
998 if foo {
999
1000 }$0 else if bar {
1001
1002 }
1003}
1004"#,
1005 );
1006 }
1007
1008 #[test]
1009 fn join_two_ifs_with_existing_else() {
1010 cov_mark::check!(join_two_ifs_with_existing_else);
1011 check_join_lines(
1012 r#"
1013fn main() {
1014 if foo {
1015
1016 } else {
1017
1018 }$0
1019 if bar {
1020
1021 }
1022}
1023"#,
1024 r#"
1025fn main() {
1026 if foo {
1027
1028 } else {
1029
1030 }$0 if bar {
1031
1032 }
1033}
1034"#,
1035 );
1036 }
1037
1038 #[test]
1039 fn join_assignments() {
1040 check_join_lines(
1041 r#"
1042fn foo() {
1043 $0let foo;
1044 foo = "bar";
1045}
1046"#,
1047 r#"
1048fn foo() {
1049 $0let foo = "bar";
1050}
1051"#,
1052 );
1053
1054 cov_mark::check!(join_assignments_mismatch);
1055 check_join_lines(
1056 r#"
1057fn foo() {
1058 let foo;
1059 let qux;$0
1060 foo = "bar";
1061}
1062"#,
1063 r#"
1064fn foo() {
1065 let foo;
1066 let qux;$0 foo = "bar";
1067}
1068"#,
1069 );
1070
1071 cov_mark::check!(join_assignments_already_initialized);
1072 check_join_lines(
1073 r#"
1074fn foo() {
1075 let foo = "bar";$0
1076 foo = "bar";
1077}
1078"#,
1079 r#"
1080fn foo() {
1081 let foo = "bar";$0 foo = "bar";
1082}
1083"#,
1084 );
1085 }
1086}