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