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