]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/join_lines.rs
c67ccd1a9f7e9731dae814878da28abae7799381
[rust.git] / crates / ide / src / join_lines.rs
1 use std::convert::TryFrom;
2
3 use ide_assists::utils::extract_trivial_expression;
4 use itertools::Itertools;
5 use syntax::{
6     algo::non_trivia_sibling,
7     ast::{self, AstNode, AstToken, IsString},
8     Direction, NodeOrToken, SourceFile,
9     SyntaxKind::{self, USE_TREE, WHITESPACE},
10     SyntaxNode, SyntaxToken, TextRange, TextSize, T,
11 };
12
13 use text_edit::{TextEdit, TextEditBuilder};
14
15 // Feature: Join Lines
16 //
17 // Join selected lines into one, smartly fixing up whitespace, trailing commas, and braces.
18 //
19 // |===
20 // | Editor  | Action Name
21 //
22 // | VS Code | **Rust Analyzer: Join lines**
23 // |===
24 //
25 // image::https://user-images.githubusercontent.com/48062697/113020661-b6922200-917a-11eb-87c4-b75acc028f11.gif[]
26 pub(crate) fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
27     let range = if range.is_empty() {
28         let syntax = file.syntax();
29         let text = syntax.text().slice(range.start()..);
30         let pos = match text.find_char('\n') {
31             None => return TextEdit::builder().finish(),
32             Some(pos) => pos,
33         };
34         TextRange::at(range.start() + pos, TextSize::of('\n'))
35     } else {
36         range
37     };
38
39     let mut edit = TextEdit::builder();
40     match file.syntax().covering_element(range) {
41         NodeOrToken::Node(node) => {
42             for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
43                 remove_newlines(&mut edit, &token, range)
44             }
45         }
46         NodeOrToken::Token(token) => remove_newlines(&mut edit, &token, range),
47     };
48     edit.finish()
49 }
50
51 fn remove_newlines(edit: &mut TextEditBuilder, token: &SyntaxToken, range: TextRange) {
52     let intersection = match range.intersect(token.text_range()) {
53         Some(range) => range,
54         None => return,
55     };
56
57     let range = intersection - token.text_range().start();
58     let text = token.text();
59     for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
60         let pos: TextSize = (pos as u32).into();
61         let offset = token.text_range().start() + range.start() + pos;
62         if !edit.invalidates_offset(offset) {
63             remove_newline(edit, &token, offset);
64         }
65     }
66 }
67
68 fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextSize) {
69     if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
70         let n_spaces_after_line_break = {
71             let suff = &token.text()[TextRange::new(
72                 offset - token.text_range().start() + TextSize::of('\n'),
73                 TextSize::of(token.text()),
74             )];
75             suff.bytes().take_while(|&b| b == b' ').count()
76         };
77
78         let mut no_space = false;
79         if let Some(string) = ast::String::cast(token.clone()) {
80             if let Some(range) = string.open_quote_text_range() {
81                 cov_mark::hit!(join_string_literal_open_quote);
82                 no_space |= range.end() == offset;
83             }
84             if let Some(range) = string.close_quote_text_range() {
85                 cov_mark::hit!(join_string_literal_close_quote);
86                 no_space |= range.start()
87                     == offset
88                         + TextSize::of('\n')
89                         + TextSize::try_from(n_spaces_after_line_break).unwrap();
90             }
91         }
92
93         let range = TextRange::at(offset, ((n_spaces_after_line_break + 1) as u32).into());
94         let replace_with = if no_space { "" } else { " " };
95         edit.replace(range, replace_with.to_string());
96         return;
97     }
98
99     // The node is between two other nodes
100     let (prev, next) = match (token.prev_sibling_or_token(), token.next_sibling_or_token()) {
101         (Some(prev), Some(next)) => (prev, next),
102         _ => return,
103     };
104
105     if is_trailing_comma(prev.kind(), next.kind()) {
106         // Removes: trailing comma, newline (incl. surrounding whitespace)
107         edit.delete(TextRange::new(prev.text_range().start(), token.text_range().end()));
108         return;
109     }
110     if prev.kind() == T![,] && next.kind() == T!['}'] {
111         // Removes: comma, newline (incl. surrounding whitespace)
112         let space = if let Some(left) = prev.prev_sibling_or_token() {
113             compute_ws(left.kind(), next.kind())
114         } else {
115             " "
116         };
117         edit.replace(
118             TextRange::new(prev.text_range().start(), token.text_range().end()),
119             space.to_string(),
120         );
121         return;
122     }
123
124     if let (Some(_), Some(next)) = (
125         prev.as_token().cloned().and_then(ast::Comment::cast),
126         next.as_token().cloned().and_then(ast::Comment::cast),
127     ) {
128         // Removes: newline (incl. surrounding whitespace), start of the next comment
129         edit.delete(TextRange::new(
130             token.text_range().start(),
131             next.syntax().text_range().start() + TextSize::of(next.prefix()),
132         ));
133         return;
134     }
135
136     // Special case that turns something like:
137     //
138     // ```
139     // my_function({$0
140     //    <some-expr>
141     // })
142     // ```
143     //
144     // into `my_function(<some-expr>)`
145     if join_single_expr_block(edit, token).is_some() {
146         return;
147     }
148     // ditto for
149     //
150     // ```
151     // use foo::{$0
152     //    bar
153     // };
154     // ```
155     if join_single_use_tree(edit, token).is_some() {
156         return;
157     }
158
159     // Remove newline but add a computed amount of whitespace characters
160     edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
161 }
162
163 fn has_comma_after(node: &SyntaxNode) -> bool {
164     match non_trivia_sibling(node.clone().into(), Direction::Next) {
165         Some(n) => n.kind() == T![,],
166         _ => false,
167     }
168 }
169
170 fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
171     let block_expr = ast::BlockExpr::cast(token.parent()?)?;
172     if !block_expr.is_standalone() {
173         return None;
174     }
175     let expr = extract_trivial_expression(&block_expr)?;
176
177     let block_range = block_expr.syntax().text_range();
178     let mut buf = expr.syntax().text().to_string();
179
180     // Match block needs to have a comma after the block
181     if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
182         if !has_comma_after(match_arm.syntax()) {
183             buf.push(',');
184         }
185     }
186
187     edit.replace(block_range, buf);
188
189     Some(())
190 }
191
192 fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
193     let use_tree_list = ast::UseTreeList::cast(token.parent()?)?;
194     let (tree,) = use_tree_list.use_trees().collect_tuple()?;
195     edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
196     Some(())
197 }
198
199 fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
200     matches!((left, right), (T![,], T![')']) | (T![,], T![']']))
201 }
202
203 fn compute_ws(left: SyntaxKind, right: SyntaxKind) -> &'static str {
204     match left {
205         T!['('] | T!['['] => return "",
206         T!['{'] => {
207             if let USE_TREE = right {
208                 return "";
209             }
210         }
211         _ => (),
212     }
213     match right {
214         T![')'] | T![']'] => return "",
215         T!['}'] => {
216             if let USE_TREE = left {
217                 return "";
218             }
219         }
220         T![.] => return "",
221         _ => (),
222     }
223     " "
224 }
225
226 #[cfg(test)]
227 mod tests {
228     use syntax::SourceFile;
229     use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range};
230
231     use super::*;
232
233     fn check_join_lines(ra_fixture_before: &str, ra_fixture_after: &str) {
234         let (before_cursor_pos, before) = extract_offset(ra_fixture_before);
235         let file = SourceFile::parse(&before).ok().unwrap();
236
237         let range = TextRange::empty(before_cursor_pos);
238         let result = join_lines(&file, range);
239
240         let actual = {
241             let mut actual = before;
242             result.apply(&mut actual);
243             actual
244         };
245         let actual_cursor_pos = result
246             .apply_to_offset(before_cursor_pos)
247             .expect("cursor position is affected by the edit");
248         let actual = add_cursor(&actual, actual_cursor_pos);
249         assert_eq_text!(ra_fixture_after, &actual);
250     }
251
252     #[test]
253     fn test_join_lines_comma() {
254         check_join_lines(
255             r"
256 fn foo() {
257     $0foo(1,
258     )
259 }
260 ",
261             r"
262 fn foo() {
263     $0foo(1)
264 }
265 ",
266         );
267     }
268
269     #[test]
270     fn test_join_lines_lambda_block() {
271         check_join_lines(
272             r"
273 pub fn reparse(&self, edit: &AtomTextEdit) -> File {
274     $0self.incremental_reparse(edit).unwrap_or_else(|| {
275         self.full_reparse(edit)
276     })
277 }
278 ",
279             r"
280 pub fn reparse(&self, edit: &AtomTextEdit) -> File {
281     $0self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
282 }
283 ",
284         );
285     }
286
287     #[test]
288     fn test_join_lines_block() {
289         check_join_lines(
290             r"
291 fn foo() {
292     foo($0{
293         92
294     })
295 }",
296             r"
297 fn foo() {
298     foo($092)
299 }",
300         );
301     }
302
303     #[test]
304     fn test_join_lines_diverging_block() {
305         check_join_lines(
306             r"
307 fn foo() {
308     loop {
309         match x {
310             92 => $0{
311                 continue;
312             }
313         }
314     }
315 }
316         ",
317             r"
318 fn foo() {
319     loop {
320         match x {
321             92 => $0continue,
322         }
323     }
324 }
325         ",
326         );
327     }
328
329     #[test]
330     fn join_lines_adds_comma_for_block_in_match_arm() {
331         check_join_lines(
332             r"
333 fn foo(e: Result<U, V>) {
334     match e {
335         Ok(u) => $0{
336             u.foo()
337         }
338         Err(v) => v,
339     }
340 }",
341             r"
342 fn foo(e: Result<U, V>) {
343     match e {
344         Ok(u) => $0u.foo(),
345         Err(v) => v,
346     }
347 }",
348         );
349     }
350
351     #[test]
352     fn join_lines_multiline_in_block() {
353         check_join_lines(
354             r"
355 fn foo() {
356     match ty {
357         $0 Some(ty) => {
358             match ty {
359                 _ => false,
360             }
361         }
362         _ => true,
363     }
364 }
365 ",
366             r"
367 fn foo() {
368     match ty {
369         $0 Some(ty) => match ty {
370                 _ => false,
371             },
372         _ => true,
373     }
374 }
375 ",
376         );
377     }
378
379     #[test]
380     fn join_lines_keeps_comma_for_block_in_match_arm() {
381         // We already have a comma
382         check_join_lines(
383             r"
384 fn foo(e: Result<U, V>) {
385     match e {
386         Ok(u) => $0{
387             u.foo()
388         },
389         Err(v) => v,
390     }
391 }",
392             r"
393 fn foo(e: Result<U, V>) {
394     match e {
395         Ok(u) => $0u.foo(),
396         Err(v) => v,
397     }
398 }",
399         );
400
401         // comma with whitespace between brace and ,
402         check_join_lines(
403             r"
404 fn foo(e: Result<U, V>) {
405     match e {
406         Ok(u) => $0{
407             u.foo()
408         }    ,
409         Err(v) => v,
410     }
411 }",
412             r"
413 fn foo(e: Result<U, V>) {
414     match e {
415         Ok(u) => $0u.foo()    ,
416         Err(v) => v,
417     }
418 }",
419         );
420
421         // comma with newline between brace and ,
422         check_join_lines(
423             r"
424 fn foo(e: Result<U, V>) {
425     match e {
426         Ok(u) => $0{
427             u.foo()
428         }
429         ,
430         Err(v) => v,
431     }
432 }",
433             r"
434 fn foo(e: Result<U, V>) {
435     match e {
436         Ok(u) => $0u.foo()
437         ,
438         Err(v) => v,
439     }
440 }",
441         );
442     }
443
444     #[test]
445     fn join_lines_keeps_comma_with_single_arg_tuple() {
446         // A single arg tuple
447         check_join_lines(
448             r"
449 fn foo() {
450     let x = ($0{
451        4
452     },);
453 }",
454             r"
455 fn foo() {
456     let x = ($04,);
457 }",
458         );
459
460         // single arg tuple with whitespace between brace and comma
461         check_join_lines(
462             r"
463 fn foo() {
464     let x = ($0{
465        4
466     }   ,);
467 }",
468             r"
469 fn foo() {
470     let x = ($04   ,);
471 }",
472         );
473
474         // single arg tuple with newline between brace and comma
475         check_join_lines(
476             r"
477 fn foo() {
478     let x = ($0{
479        4
480     }
481     ,);
482 }",
483             r"
484 fn foo() {
485     let x = ($04
486     ,);
487 }",
488         );
489     }
490
491     #[test]
492     fn test_join_lines_use_items_left() {
493         // No space after the '{'
494         check_join_lines(
495             r"
496 $0use syntax::{
497     TextSize, TextRange,
498 };",
499             r"
500 $0use syntax::{TextSize, TextRange,
501 };",
502         );
503     }
504
505     #[test]
506     fn test_join_lines_use_items_right() {
507         // No space after the '}'
508         check_join_lines(
509             r"
510 use syntax::{
511 $0    TextSize, TextRange
512 };",
513             r"
514 use syntax::{
515 $0    TextSize, TextRange};",
516         );
517     }
518
519     #[test]
520     fn test_join_lines_use_items_right_comma() {
521         // No space after the '}'
522         check_join_lines(
523             r"
524 use syntax::{
525 $0    TextSize, TextRange,
526 };",
527             r"
528 use syntax::{
529 $0    TextSize, TextRange};",
530         );
531     }
532
533     #[test]
534     fn test_join_lines_use_tree() {
535         check_join_lines(
536             r"
537 use syntax::{
538     algo::$0{
539         find_token_at_offset,
540     },
541     ast,
542 };",
543             r"
544 use syntax::{
545     algo::$0find_token_at_offset,
546     ast,
547 };",
548         );
549     }
550
551     #[test]
552     fn test_join_lines_normal_comments() {
553         check_join_lines(
554             r"
555 fn foo() {
556     // Hello$0
557     // world!
558 }
559 ",
560             r"
561 fn foo() {
562     // Hello$0 world!
563 }
564 ",
565         );
566     }
567
568     #[test]
569     fn test_join_lines_doc_comments() {
570         check_join_lines(
571             r"
572 fn foo() {
573     /// Hello$0
574     /// world!
575 }
576 ",
577             r"
578 fn foo() {
579     /// Hello$0 world!
580 }
581 ",
582         );
583     }
584
585     #[test]
586     fn test_join_lines_mod_comments() {
587         check_join_lines(
588             r"
589 fn foo() {
590     //! Hello$0
591     //! world!
592 }
593 ",
594             r"
595 fn foo() {
596     //! Hello$0 world!
597 }
598 ",
599         );
600     }
601
602     #[test]
603     fn test_join_lines_multiline_comments_1() {
604         check_join_lines(
605             r"
606 fn foo() {
607     // Hello$0
608     /* world! */
609 }
610 ",
611             r"
612 fn foo() {
613     // Hello$0 world! */
614 }
615 ",
616         );
617     }
618
619     #[test]
620     fn test_join_lines_multiline_comments_2() {
621         check_join_lines(
622             r"
623 fn foo() {
624     // The$0
625     /* quick
626     brown
627     fox! */
628 }
629 ",
630             r"
631 fn foo() {
632     // The$0 quick
633     brown
634     fox! */
635 }
636 ",
637         );
638     }
639
640     fn check_join_lines_sel(ra_fixture_before: &str, ra_fixture_after: &str) {
641         let (sel, before) = extract_range(ra_fixture_before);
642         let parse = SourceFile::parse(&before);
643         let result = join_lines(&parse.tree(), sel);
644         let actual = {
645             let mut actual = before;
646             result.apply(&mut actual);
647             actual
648         };
649         assert_eq_text!(ra_fixture_after, &actual);
650     }
651
652     #[test]
653     fn test_join_lines_selection_fn_args() {
654         check_join_lines_sel(
655             r"
656 fn foo() {
657     $0foo(1,
658         2,
659         3,
660     $0)
661 }
662     ",
663             r"
664 fn foo() {
665     foo(1, 2, 3)
666 }
667     ",
668         );
669     }
670
671     #[test]
672     fn test_join_lines_selection_struct() {
673         check_join_lines_sel(
674             r"
675 struct Foo $0{
676     f: u32,
677 }$0
678     ",
679             r"
680 struct Foo { f: u32 }
681     ",
682         );
683     }
684
685     #[test]
686     fn test_join_lines_selection_dot_chain() {
687         check_join_lines_sel(
688             r"
689 fn foo() {
690     join($0type_params.type_params()
691             .filter_map(|it| it.name())
692             .map(|it| it.text())$0)
693 }",
694             r"
695 fn foo() {
696     join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
697 }",
698         );
699     }
700
701     #[test]
702     fn test_join_lines_selection_lambda_block_body() {
703         check_join_lines_sel(
704             r"
705 pub fn handle_find_matching_brace() {
706     params.offsets
707         .map(|offset| $0{
708             world.analysis().matching_brace(&file, offset).unwrap_or(offset)
709         }$0)
710         .collect();
711 }",
712             r"
713 pub fn handle_find_matching_brace() {
714     params.offsets
715         .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
716         .collect();
717 }",
718         );
719     }
720
721     #[test]
722     fn test_join_lines_commented_block() {
723         check_join_lines(
724             r"
725 fn main() {
726     let _ = {
727         // $0foo
728         // bar
729         92
730     };
731 }
732         ",
733             r"
734 fn main() {
735     let _ = {
736         // $0foo bar
737         92
738     };
739 }
740         ",
741         )
742     }
743
744     #[test]
745     fn join_lines_mandatory_blocks_block() {
746         check_join_lines(
747             r"
748 $0fn foo() {
749     92
750 }
751         ",
752             r"
753 $0fn foo() { 92
754 }
755         ",
756         );
757
758         check_join_lines(
759             r"
760 fn foo() {
761     $0if true {
762         92
763     }
764 }
765         ",
766             r"
767 fn foo() {
768     $0if true { 92
769     }
770 }
771         ",
772         );
773
774         check_join_lines(
775             r"
776 fn foo() {
777     $0loop {
778         92
779     }
780 }
781         ",
782             r"
783 fn foo() {
784     $0loop { 92
785     }
786 }
787         ",
788         );
789
790         check_join_lines(
791             r"
792 fn foo() {
793     $0unsafe {
794         92
795     }
796 }
797         ",
798             r"
799 fn foo() {
800     $0unsafe { 92
801     }
802 }
803         ",
804         );
805     }
806
807     #[test]
808     fn join_string_literal() {
809         {
810             cov_mark::check!(join_string_literal_open_quote);
811             check_join_lines(
812                 r#"
813 fn main() {
814     $0"
815 hello
816 ";
817 }
818 "#,
819                 r#"
820 fn main() {
821     $0"hello
822 ";
823 }
824 "#,
825             );
826         }
827
828         {
829             cov_mark::check!(join_string_literal_close_quote);
830             check_join_lines(
831                 r#"
832 fn main() {
833     $0"hello
834 ";
835 }
836 "#,
837                 r#"
838 fn main() {
839     $0"hello";
840 }
841 "#,
842             );
843             check_join_lines(
844                 r#"
845 fn main() {
846     $0r"hello
847     ";
848 }
849 "#,
850                 r#"
851 fn main() {
852     $0r"hello";
853 }
854 "#,
855             );
856         }
857
858         check_join_lines(
859             r#"
860 fn main() {
861     "
862 $0hello
863 world
864 ";
865 }
866 "#,
867             r#"
868 fn main() {
869     "
870 $0hello world
871 ";
872 }
873 "#,
874         );
875     }
876     #[test]
877     fn join_last_line_empty() {
878         check_join_lines(
879             r#"
880 fn main() {$0}
881 "#,
882             r#"
883 fn main() {$0}
884 "#,
885         );
886     }
887 }