]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/join_lines.rs
fix: move dir on rename mod
[rust.git] / crates / ide / src / join_lines.rs
1 use ide_assists::utils::extract_trivial_expression;
2 use ide_db::syntax_helpers::node_ext::expr_as_name_ref;
3 use itertools::Itertools;
4 use syntax::{
5     ast::{self, AstNode, AstToken, IsString},
6     NodeOrToken, SourceFile, SyntaxElement,
7     SyntaxKind::{self, USE_TREE, WHITESPACE},
8     SyntaxToken, TextRange, TextSize, T,
9 };
10
11 use text_edit::{TextEdit, TextEditBuilder};
12
13 pub 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
20 // Feature: Join Lines
21 //
22 // Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces.
23 //
24 // See
25 // https://user-images.githubusercontent.com/1711539/124515923-4504e800-dde9-11eb-8d58-d97945a1a785.gif[this gif]
26 // for the cases handled specially by joined lines.
27 //
28 // |===
29 // | Editor  | Action Name
30 //
31 // | VS Code | **Rust Analyzer: Join lines**
32 // |===
33 //
34 // image::https://user-images.githubusercontent.com/48062697/113020661-b6922200-917a-11eb-87c4-b75acc028f11.gif[]
35 pub(crate) fn join_lines(
36     config: &JoinLinesConfig,
37     file: &SourceFile,
38     range: TextRange,
39 ) -> TextEdit {
40     let range = if range.is_empty() {
41         let syntax = file.syntax();
42         let text = syntax.text().slice(range.start()..);
43         let pos = match text.find_char('\n') {
44             None => return TextEdit::builder().finish(),
45             Some(pos) => pos,
46         };
47         TextRange::at(range.start() + pos, TextSize::of('\n'))
48     } else {
49         range
50     };
51
52     let mut edit = TextEdit::builder();
53     match file.syntax().covering_element(range) {
54         NodeOrToken::Node(node) => {
55             for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
56                 remove_newlines(config, &mut edit, &token, range)
57             }
58         }
59         NodeOrToken::Token(token) => remove_newlines(config, &mut edit, &token, range),
60     };
61     edit.finish()
62 }
63
64 fn remove_newlines(
65     config: &JoinLinesConfig,
66     edit: &mut TextEditBuilder,
67     token: &SyntaxToken,
68     range: TextRange,
69 ) {
70     let intersection = match range.intersect(token.text_range()) {
71         Some(range) => range,
72         None => return,
73     };
74
75     let range = intersection - token.text_range().start();
76     let text = token.text();
77     for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
78         let pos: TextSize = (pos as u32).into();
79         let offset = token.text_range().start() + range.start() + pos;
80         if !edit.invalidates_offset(offset) {
81             remove_newline(config, edit, token, offset);
82         }
83     }
84 }
85
86 fn remove_newline(
87     config: &JoinLinesConfig,
88     edit: &mut TextEditBuilder,
89     token: &SyntaxToken,
90     offset: TextSize,
91 ) {
92     if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
93         let n_spaces_after_line_break = {
94             let suff = &token.text()[TextRange::new(
95                 offset - token.text_range().start() + TextSize::of('\n'),
96                 TextSize::of(token.text()),
97             )];
98             suff.bytes().take_while(|&b| b == b' ').count()
99         };
100
101         let mut no_space = false;
102         if let Some(string) = ast::String::cast(token.clone()) {
103             if let Some(range) = string.open_quote_text_range() {
104                 cov_mark::hit!(join_string_literal_open_quote);
105                 no_space |= range.end() == offset;
106             }
107             if let Some(range) = string.close_quote_text_range() {
108                 cov_mark::hit!(join_string_literal_close_quote);
109                 no_space |= range.start()
110                     == offset
111                         + TextSize::of('\n')
112                         + TextSize::try_from(n_spaces_after_line_break).unwrap();
113             }
114         }
115
116         let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into());
117         let replace_with = if no_space { "" } else { " " };
118         edit.replace(range, replace_with.to_string());
119         return;
120     }
121
122     // The node is between two other nodes
123     let (prev, next) = match (token.prev_sibling_or_token(), token.next_sibling_or_token()) {
124         (Some(prev), Some(next)) => (prev, next),
125         _ => return,
126     };
127
128     if config.remove_trailing_comma && prev.kind() == T![,] {
129         match next.kind() {
130             T![')'] | T![']'] => {
131                 // Removes: trailing comma, newline (incl. surrounding whitespace)
132                 edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
133                 return;
134             }
135             T!['}'] => {
136                 // Removes: comma, newline (incl. surrounding whitespace)
137                 let space = match prev.prev_sibling_or_token() {
138                     Some(left) => compute_ws(left.kind(), next.kind()),
139                     None => " ",
140                 };
141                 edit.replace(
142                     TextRange::new(prev.text_range().start(), token.text_range().end()),
143                     space.to_string(),
144                 );
145                 return;
146             }
147             _ => (),
148         }
149     }
150
151     if config.join_else_if {
152         if let (Some(prev), Some(_next)) = (as_if_expr(&prev), as_if_expr(&next)) {
153             match prev.else_token() {
154                 Some(_) => cov_mark::hit!(join_two_ifs_with_existing_else),
155                 None => {
156                     cov_mark::hit!(join_two_ifs);
157                     edit.replace(token.text_range(), " else ".to_string());
158                     return;
159                 }
160             }
161         }
162     }
163
164     if config.join_assignments {
165         if join_assignments(edit, &prev, &next).is_some() {
166             return;
167         }
168     }
169
170     if config.unwrap_trivial_blocks {
171         // Special case that turns something like:
172         //
173         // ```
174         // my_function({$0
175         //    <some-expr>
176         // })
177         // ```
178         //
179         // into `my_function(<some-expr>)`
180         if join_single_expr_block(edit, token).is_some() {
181             return;
182         }
183         // ditto for
184         //
185         // ```
186         // use foo::{$0
187         //    bar
188         // };
189         // ```
190         if join_single_use_tree(edit, token).is_some() {
191             return;
192         }
193     }
194
195     if let (Some(_), Some(next)) = (
196         prev.as_token().cloned().and_then(ast::Comment::cast),
197         next.as_token().cloned().and_then(ast::Comment::cast),
198     ) {
199         // Removes: newline (incl. surrounding whitespace), start of the next comment
200         edit.delete(TextRange::new(
201             token.text_range().start(),
202             next.syntax().text_range().start() + TextSize::of(next.prefix()),
203         ));
204         return;
205     }
206
207     // Remove newline but add a computed amount of whitespace characters
208     edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
209 }
210
211 fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
212     let block_expr = ast::BlockExpr::cast(token.ancestors().nth(1)?)?;
213     if !block_expr.is_standalone() {
214         return None;
215     }
216     let expr = extract_trivial_expression(&block_expr)?;
217
218     let block_range = block_expr.syntax().text_range();
219     let mut buf = expr.syntax().text().to_string();
220
221     // Match block needs to have a comma after the block
222     if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
223         if match_arm.comma_token().is_none() {
224             buf.push(',');
225         }
226     }
227
228     edit.replace(block_range, buf);
229
230     Some(())
231 }
232
233 fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
234     let use_tree_list = ast::UseTreeList::cast(token.parent()?)?;
235     let (tree,) = use_tree_list.use_trees().collect_tuple()?;
236     edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
237     Some(())
238 }
239
240 fn join_assignments(
241     edit: &mut TextEditBuilder,
242     prev: &SyntaxElement,
243     next: &SyntaxElement,
244 ) -> Option<()> {
245     let let_stmt = ast::LetStmt::cast(prev.as_node()?.clone())?;
246     if let_stmt.eq_token().is_some() {
247         cov_mark::hit!(join_assignments_already_initialized);
248         return None;
249     }
250     let let_ident_pat = match let_stmt.pat()? {
251         ast::Pat::IdentPat(it) => it,
252         _ => return None,
253     };
254
255     let expr_stmt = ast::ExprStmt::cast(next.as_node()?.clone())?;
256     let bin_expr = match expr_stmt.expr()? {
257         ast::Expr::BinExpr(it) => it,
258         _ => return None,
259     };
260     if !matches!(bin_expr.op_kind()?, ast::BinaryOp::Assignment { op: None }) {
261         return None;
262     }
263     let lhs = bin_expr.lhs()?;
264     let name_ref = expr_as_name_ref(&lhs)?;
265
266     if name_ref.to_string() != let_ident_pat.syntax().to_string() {
267         cov_mark::hit!(join_assignments_mismatch);
268         return None;
269     }
270
271     edit.delete(let_stmt.semicolon_token()?.text_range().cover(lhs.syntax().text_range()));
272     Some(())
273 }
274
275 fn as_if_expr(element: &SyntaxElement) -> Option<ast::IfExpr> {
276     let mut node = element.as_node()?.clone();
277     if let Some(stmt) = ast::ExprStmt::cast(node.clone()) {
278         node = stmt.expr()?.syntax().clone();
279     }
280     ast::IfExpr::cast(node)
281 }
282
283 fn compute_ws(left: SyntaxKind, right: SyntaxKind) -> &'static str {
284     match left {
285         T!['('] | T!['['] => return "",
286         T!['{'] => {
287             if let USE_TREE = right {
288                 return "";
289             }
290         }
291         _ => (),
292     }
293     match right {
294         T![')'] | T![']'] => return "",
295         T!['}'] => {
296             if let USE_TREE = left {
297                 return "";
298             }
299         }
300         T![.] => return "",
301         _ => (),
302     }
303     " "
304 }
305
306 #[cfg(test)]
307 mod tests {
308     use syntax::SourceFile;
309     use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range};
310
311     use super::*;
312
313     fn check_join_lines(ra_fixture_before: &str, ra_fixture_after: &str) {
314         let config = JoinLinesConfig {
315             join_else_if: true,
316             remove_trailing_comma: true,
317             unwrap_trivial_blocks: true,
318             join_assignments: true,
319         };
320
321         let (before_cursor_pos, before) = extract_offset(ra_fixture_before);
322         let file = SourceFile::parse(&before).ok().unwrap();
323
324         let range = TextRange::empty(before_cursor_pos);
325         let result = join_lines(&config, &file, range);
326
327         let actual = {
328             let mut actual = before;
329             result.apply(&mut actual);
330             actual
331         };
332         let actual_cursor_pos = result
333             .apply_to_offset(before_cursor_pos)
334             .expect("cursor position is affected by the edit");
335         let actual = add_cursor(&actual, actual_cursor_pos);
336         assert_eq_text!(ra_fixture_after, &actual);
337     }
338
339     fn check_join_lines_sel(ra_fixture_before: &str, ra_fixture_after: &str) {
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);
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"
362 fn foo() {
363     $0foo(1,
364     )
365 }
366 ",
367             r"
368 fn foo() {
369     $0foo(1)
370 }
371 ",
372         );
373     }
374
375     #[test]
376     fn test_join_lines_lambda_block() {
377         check_join_lines(
378             r"
379 pub fn reparse(&self, edit: &AtomTextEdit) -> File {
380     $0self.incremental_reparse(edit).unwrap_or_else(|| {
381         self.full_reparse(edit)
382     })
383 }
384 ",
385             r"
386 pub 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"
397 fn foo() {
398     foo($0{
399         92
400     })
401 }",
402             r"
403 fn foo() {
404     foo($092)
405 }",
406         );
407     }
408
409     #[test]
410     fn test_join_lines_diverging_block() {
411         check_join_lines(
412             r"
413 fn foo() {
414     loop {
415         match x {
416             92 => $0{
417                 continue;
418             }
419         }
420     }
421 }
422         ",
423             r"
424 fn 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"
439 fn foo(e: Result<U, V>) {
440     match e {
441         Ok(u) => $0{
442             u.foo()
443         }
444         Err(v) => v,
445     }
446 }",
447             r"
448 fn 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"
461 fn foo() {
462     match ty {
463         $0 Some(ty) => {
464             match ty {
465                 _ => false,
466             }
467         }
468         _ => true,
469     }
470 }
471 ",
472             r"
473 fn 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         // We already have a comma
488         check_join_lines(
489             r"
490 fn foo(e: Result<U, V>) {
491     match e {
492         Ok(u) => $0{
493             u.foo()
494         },
495         Err(v) => v,
496     }
497 }",
498             r"
499 fn foo(e: Result<U, V>) {
500     match e {
501         Ok(u) => $0u.foo(),
502         Err(v) => v,
503     }
504 }",
505         );
506
507         // comma with whitespace between brace and ,
508         check_join_lines(
509             r"
510 fn foo(e: Result<U, V>) {
511     match e {
512         Ok(u) => $0{
513             u.foo()
514         }    ,
515         Err(v) => v,
516     }
517 }",
518             r"
519 fn foo(e: Result<U, V>) {
520     match e {
521         Ok(u) => $0u.foo()    ,
522         Err(v) => v,
523     }
524 }",
525         );
526
527         // comma with newline between brace and ,
528         check_join_lines(
529             r"
530 fn 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"
540 fn 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         // A single arg tuple
553         check_join_lines(
554             r"
555 fn foo() {
556     let x = ($0{
557        4
558     },);
559 }",
560             r"
561 fn foo() {
562     let x = ($04,);
563 }",
564         );
565
566         // single arg tuple with whitespace between brace and comma
567         check_join_lines(
568             r"
569 fn foo() {
570     let x = ($0{
571        4
572     }   ,);
573 }",
574             r"
575 fn foo() {
576     let x = ($04   ,);
577 }",
578         );
579
580         // single arg tuple with newline between brace and comma
581         check_join_lines(
582             r"
583 fn foo() {
584     let x = ($0{
585        4
586     }
587     ,);
588 }",
589             r"
590 fn foo() {
591     let x = ($04
592     ,);
593 }",
594         );
595     }
596
597     #[test]
598     fn test_join_lines_use_items_left() {
599         // No space after the '{'
600         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         // No space after the '}'
614         check_join_lines(
615             r"
616 use syntax::{
617 $0    TextSize, TextRange
618 };",
619             r"
620 use syntax::{
621 $0    TextSize, TextRange};",
622         );
623     }
624
625     #[test]
626     fn test_join_lines_use_items_right_comma() {
627         // No space after the '}'
628         check_join_lines(
629             r"
630 use syntax::{
631 $0    TextSize, TextRange,
632 };",
633             r"
634 use syntax::{
635 $0    TextSize, TextRange};",
636         );
637     }
638
639     #[test]
640     fn test_join_lines_use_tree() {
641         check_join_lines(
642             r"
643 use syntax::{
644     algo::$0{
645         find_token_at_offset,
646     },
647     ast,
648 };",
649             r"
650 use 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"
661 fn foo() {
662     // Hello$0
663     // world!
664 }
665 ",
666             r"
667 fn 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 fn foo() {
679     /// Hello$0
680     /// world!
681 }
682 ",
683             r"
684 fn foo() {
685     /// Hello$0 world!
686 }
687 ",
688         );
689     }
690
691     #[test]
692     fn test_join_lines_mod_comments() {
693         check_join_lines(
694             r"
695 fn foo() {
696     //! Hello$0
697     //! world!
698 }
699 ",
700             r"
701 fn 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"
712 fn foo() {
713     // Hello$0
714     /* world! */
715 }
716 ",
717             r"
718 fn 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"
729 fn foo() {
730     // The$0
731     /* quick
732     brown
733     fox! */
734 }
735 ",
736             r"
737 fn 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"
750 fn foo() {
751     $0foo(1,
752         2,
753         3,
754     $0)
755 }
756     ",
757             r"
758 fn 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"
769 struct Foo $0{
770     f: u32,
771 }$0
772     ",
773             r"
774 struct Foo { f: u32 }
775     ",
776         );
777     }
778
779     #[test]
780     fn test_join_lines_selection_dot_chain() {
781         check_join_lines_sel(
782             r"
783 fn foo() {
784     join($0type_params.type_params()
785             .filter_map(|it| it.name())
786             .map(|it| it.text())$0)
787 }",
788             r"
789 fn 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"
799 pub 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"
807 pub 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"
819 fn main() {
820     let _ = {
821         // $0foo
822         // bar
823         92
824     };
825 }
826         ",
827             r"
828 fn 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"
854 fn foo() {
855     $0if true {
856         92
857     }
858 }
859         ",
860             r"
861 fn foo() {
862     $0if true { 92
863     }
864 }
865         ",
866         );
867
868         check_join_lines(
869             r"
870 fn foo() {
871     $0loop {
872         92
873     }
874 }
875         ",
876             r"
877 fn foo() {
878     $0loop { 92
879     }
880 }
881         ",
882         );
883
884         check_join_lines(
885             r"
886 fn foo() {
887     $0unsafe {
888         92
889     }
890 }
891         ",
892             r"
893 fn 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#"
907 fn main() {
908     $0"
909 hello
910 ";
911 }
912 "#,
913                 r#"
914 fn 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#"
926 fn main() {
927     $0"hello
928 ";
929 }
930 "#,
931                 r#"
932 fn main() {
933     $0"hello";
934 }
935 "#,
936             );
937             check_join_lines(
938                 r#"
939 fn main() {
940     $0r"hello
941     ";
942 }
943 "#,
944                 r#"
945 fn main() {
946     $0r"hello";
947 }
948 "#,
949             );
950         }
951
952         check_join_lines(
953             r#"
954 fn main() {
955     "
956 $0hello
957 world
958 ";
959 }
960 "#,
961             r#"
962 fn 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#"
975 fn main() {$0}
976 "#,
977             r#"
978 fn 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#"
988 fn main() {
989     if foo {
990
991     }$0
992     if bar {
993
994     }
995 }
996 "#,
997             r#"
998 fn 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#"
1014 fn main() {
1015     if foo {
1016
1017     } else {
1018
1019     }$0
1020     if bar {
1021
1022     }
1023 }
1024 "#,
1025             r#"
1026 fn 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#"
1043 fn foo() {
1044     $0let foo;
1045     foo = "bar";
1046 }
1047 "#,
1048             r#"
1049 fn foo() {
1050     $0let foo = "bar";
1051 }
1052 "#,
1053         );
1054
1055         cov_mark::check!(join_assignments_mismatch);
1056         check_join_lines(
1057             r#"
1058 fn foo() {
1059     let foo;
1060     let qux;$0
1061     foo = "bar";
1062 }
1063 "#,
1064             r#"
1065 fn 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#"
1075 fn foo() {
1076     let foo = "bar";$0
1077     foo = "bar";
1078 }
1079 "#,
1080             r#"
1081 fn foo() {
1082     let foo = "bar";$0 foo = "bar";
1083 }
1084 "#,
1085         );
1086     }
1087 }