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