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