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