aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/join_lines.rs
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-08-13 16:59:50 +0100
committerGitHub <[email protected]>2020-08-13 16:59:50 +0100
commit018a6cac072767dfd630c22e6d9ce134b7bb09af (patch)
tree4293492e643f9a604c5f30e051289bcea182694c /crates/ide/src/join_lines.rs
parent00fb411f3edea72a1a9739f7df6f21cca045730b (diff)
parent6bc2633c90cedad057c5201d1ab7f67b57247004 (diff)
Merge #5750
5750: Rename ra_ide -> ide r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'crates/ide/src/join_lines.rs')
-rw-r--r--crates/ide/src/join_lines.rs773
1 files changed, 773 insertions, 0 deletions
diff --git a/crates/ide/src/join_lines.rs b/crates/ide/src/join_lines.rs
new file mode 100644
index 000000000..e37702acd
--- /dev/null
+++ b/crates/ide/src/join_lines.rs
@@ -0,0 +1,773 @@
1use assists::utils::extract_trivial_expression;
2use itertools::Itertools;
3use 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};
10use 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// |===
21pub 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
57fn 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({<|>
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::{<|>
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
131fn 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
138fn 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
160fn 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
167fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
168 matches!((left, right), (T![,], T![')']) | (T![,], T![']']))
169}
170
171fn 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)]
195mod 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"
224fn foo() {
225 <|>foo(1,
226 )
227}
228",
229 r"
230fn foo() {
231 <|>foo(1)
232}
233",
234 );
235 }
236
237 #[test]
238 fn test_join_lines_lambda_block() {
239 check_join_lines(
240 r"
241pub fn reparse(&self, edit: &AtomTextEdit) -> File {
242 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
243 self.full_reparse(edit)
244 })
245}
246",
247 r"
248pub fn reparse(&self, edit: &AtomTextEdit) -> File {
249 <|>self.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"
259fn foo() {
260 foo(<|>{
261 92
262 })
263}",
264 r"
265fn foo() {
266 foo(<|>92)
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 => <|>{
278 continue;
279 }
280 }
281 }
282 }
283 ";
284 let after = r"
285 fn foo() {
286 loop {
287 match x {
288 92 => <|>continue,
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"
300fn foo(e: Result<U, V>) {
301 match e {
302 Ok(u) => <|>{
303 u.foo()
304 }
305 Err(v) => v,
306 }
307}",
308 r"
309fn foo(e: Result<U, V>) {
310 match e {
311 Ok(u) => <|>u.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"
322fn foo() {
323 match ty {
324 <|> Some(ty) => {
325 match ty {
326 _ => false,
327 }
328 }
329 _ => true,
330 }
331}
332",
333 r"
334fn foo() {
335 match ty {
336 <|> 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"
351fn foo(e: Result<U, V>) {
352 match e {
353 Ok(u) => <|>{
354 u.foo()
355 },
356 Err(v) => v,
357 }
358}",
359 r"
360fn foo(e: Result<U, V>) {
361 match e {
362 Ok(u) => <|>u.foo(),
363 Err(v) => v,
364 }
365}",
366 );
367
368 // comma with whitespace between brace and ,
369 check_join_lines(
370 r"
371fn foo(e: Result<U, V>) {
372 match e {
373 Ok(u) => <|>{
374 u.foo()
375 } ,
376 Err(v) => v,
377 }
378}",
379 r"
380fn foo(e: Result<U, V>) {
381 match e {
382 Ok(u) => <|>u.foo() ,
383 Err(v) => v,
384 }
385}",
386 );
387
388 // comma with newline between brace and ,
389 check_join_lines(
390 r"
391fn foo(e: Result<U, V>) {
392 match e {
393 Ok(u) => <|>{
394 u.foo()
395 }
396 ,
397 Err(v) => v,
398 }
399}",
400 r"
401fn foo(e: Result<U, V>) {
402 match e {
403 Ok(u) => <|>u.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"
416fn foo() {
417 let x = (<|>{
418 4
419 },);
420}",
421 r"
422fn foo() {
423 let x = (<|>4,);
424}",
425 );
426
427 // single arg tuple with whitespace between brace and comma
428 check_join_lines(
429 r"
430fn foo() {
431 let x = (<|>{
432 4
433 } ,);
434}",
435 r"
436fn foo() {
437 let x = (<|>4 ,);
438}",
439 );
440
441 // single arg tuple with newline between brace and comma
442 check_join_lines(
443 r"
444fn foo() {
445 let x = (<|>{
446 4
447 }
448 ,);
449}",
450 r"
451fn foo() {
452 let x = (<|>4
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<|>use syntax::{
464 TextSize, TextRange,
465};",
466 r"
467<|>use 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"
477use syntax::{
478<|> TextSize, TextRange
479};",
480 r"
481use syntax::{
482<|> 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"
491use syntax::{
492<|> TextSize, TextRange,
493};",
494 r"
495use syntax::{
496<|> TextSize, TextRange};",
497 );
498 }
499
500 #[test]
501 fn test_join_lines_use_tree() {
502 check_join_lines(
503 r"
504use syntax::{
505 algo::<|>{
506 find_token_at_offset,
507 },
508 ast,
509};",
510 r"
511use syntax::{
512 algo::<|>find_token_at_offset,
513 ast,
514};",
515 );
516 }
517
518 #[test]
519 fn test_join_lines_normal_comments() {
520 check_join_lines(
521 r"
522fn foo() {
523 // Hello<|>
524 // world!
525}
526",
527 r"
528fn foo() {
529 // Hello<|> world!
530}
531",
532 );
533 }
534
535 #[test]
536 fn test_join_lines_doc_comments() {
537 check_join_lines(
538 r"
539fn foo() {
540 /// Hello<|>
541 /// world!
542}
543",
544 r"
545fn foo() {
546 /// Hello<|> world!
547}
548",
549 );
550 }
551
552 #[test]
553 fn test_join_lines_mod_comments() {
554 check_join_lines(
555 r"
556fn foo() {
557 //! Hello<|>
558 //! world!
559}
560",
561 r"
562fn foo() {
563 //! Hello<|> world!
564}
565",
566 );
567 }
568
569 #[test]
570 fn test_join_lines_multiline_comments_1() {
571 check_join_lines(
572 r"
573fn foo() {
574 // Hello<|>
575 /* world! */
576}
577",
578 r"
579fn foo() {
580 // Hello<|> world! */
581}
582",
583 );
584 }
585
586 #[test]
587 fn test_join_lines_multiline_comments_2() {
588 check_join_lines(
589 r"
590fn foo() {
591 // The<|>
592 /* quick
593 brown
594 fox! */
595}
596",
597 r"
598fn foo() {
599 // The<|> 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"
623fn foo() {
624 <|>foo(1,
625 2,
626 3,
627 <|>)
628}
629 ",
630 r"
631fn 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"
642struct Foo <|>{
643 f: u32,
644}<|>
645 ",
646 r"
647struct Foo { f: u32 }
648 ",
649 );
650 }
651
652 #[test]
653 fn test_join_lines_selection_dot_chain() {
654 check_join_lines_sel(
655 r"
656fn foo() {
657 join(<|>type_params.type_params()
658 .filter_map(|it| it.name())
659 .map(|it| it.text())<|>)
660}",
661 r"
662fn 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"
672pub fn handle_find_matching_brace() {
673 params.offsets
674 .map(|offset| <|>{
675 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
676 }<|>)
677 .collect();
678}",
679 r"
680pub 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"
692fn main() {
693 let _ = {
694 // <|>foo
695 // bar
696 92
697 };
698}
699 ",
700 r"
701fn main() {
702 let _ = {
703 // <|>foo 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<|>fn foo() {
716 92
717}
718 ",
719 r"
720<|>fn foo() { 92
721}
722 ",
723 );
724
725 check_join_lines(
726 r"
727fn foo() {
728 <|>if true {
729 92
730 }
731}
732 ",
733 r"
734fn foo() {
735 <|>if true { 92
736 }
737}
738 ",
739 );
740
741 check_join_lines(
742 r"
743fn foo() {
744 <|>loop {
745 92
746 }
747}
748 ",
749 r"
750fn foo() {
751 <|>loop { 92
752 }
753}
754 ",
755 );
756
757 check_join_lines(
758 r"
759fn foo() {
760 <|>unsafe {
761 92
762 }
763}
764 ",
765 r"
766fn foo() {
767 <|>unsafe { 92
768 }
769}
770 ",
771 );
772 }
773}