Skip to main content

ide/
join_lines.rs

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
20// Feature: Join Lines
21//
22// Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces.
23//
24// See [this gif](https://user-images.githubusercontent.com/1711539/124515923-4504e800-dde9-11eb-8d58-d97945a1a785.gif) for the cases handled specially by joined lines.
25//
26// | Editor  | Action Name |
27// |---------|-------------|
28// | VS Code | **rust-analyzer: Join lines** |
29//
30// ![Join Lines](https://user-images.githubusercontent.com/48062697/113020661-b6922200-917a-11eb-87c4-b75acc028f11.gif)
31pub(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    // The node is between two other nodes
119    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                // Removes: trailing comma, newline (incl. surrounding whitespace)
128                edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
129                return;
130            }
131            T!['}'] => {
132                // Removes: comma, newline (incl. surrounding whitespace)
133                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        // Special case that turns something like:
166        //
167        // ```
168        // my_function({$0
169        //    <some-expr>
170        // })
171        // ```
172        //
173        // into `my_function(<some-expr>)`
174        if join_single_expr_block(edit, token).is_some() {
175            return;
176        }
177        // ditto for
178        //
179        // ```
180        // use foo::{$0
181        //    bar
182        // };
183        // ```
184        if join_single_use_tree(edit, token).is_some() {
185            return;
186        }
187    }
188
189    // We can't use `prev` and `next`, since `DOC_COMMENT` has only one token, so `token` has no siblings.
190    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        // Removes: newline (incl. surrounding whitespace), start of the next comment
195        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    // Remove newline but add a computed amount of whitespace characters
203    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    // Match block needs to have a comma after the block
217    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        // We already have a comma
488        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        // comma with whitespace between brace and ,
508        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        // comma with newline between brace and ,
528        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        // A single arg tuple
553        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        // single arg tuple with whitespace between brace and comma
567        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        // single arg tuple with newline between brace and comma
581        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        // 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"
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        // No space after the '}'
628        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}