aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/join_lines.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/join_lines.rs')
-rw-r--r--crates/ra_ide/src/join_lines.rs750
1 files changed, 0 insertions, 750 deletions
diff --git a/crates/ra_ide/src/join_lines.rs b/crates/ra_ide/src/join_lines.rs
deleted file mode 100644
index 6907c09e8..000000000
--- a/crates/ra_ide/src/join_lines.rs
+++ /dev/null
@@ -1,750 +0,0 @@
1use itertools::Itertools;
2use ra_fmt::{compute_ws, extract_trivial_expression};
3use ra_syntax::{
4 algo::{find_covering_element, non_trivia_sibling},
5 ast::{self, AstNode, AstToken},
6 Direction, NodeOrToken, SourceFile,
7 SyntaxKind::{self, WHITESPACE},
8 SyntaxNode, SyntaxToken, TextRange, TextSize, T,
9};
10use ra_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 TextEditBuilder::default().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 = TextEditBuilder::default();
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
171#[cfg(test)]
172mod tests {
173 use ra_syntax::SourceFile;
174 use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range};
175
176 use super::*;
177
178 fn check_join_lines(before: &str, after: &str) {
179 let (before_cursor_pos, before) = extract_offset(before);
180 let file = SourceFile::parse(&before).ok().unwrap();
181
182 let range = TextRange::empty(before_cursor_pos);
183 let result = join_lines(&file, range);
184
185 let actual = {
186 let mut actual = before.to_string();
187 result.apply(&mut actual);
188 actual
189 };
190 let actual_cursor_pos = result
191 .apply_to_offset(before_cursor_pos)
192 .expect("cursor position is affected by the edit");
193 let actual = add_cursor(&actual, actual_cursor_pos);
194 assert_eq_text!(after, &actual);
195 }
196
197 #[test]
198 fn test_join_lines_comma() {
199 check_join_lines(
200 r"
201fn foo() {
202 <|>foo(1,
203 )
204}
205",
206 r"
207fn foo() {
208 <|>foo(1)
209}
210",
211 );
212 }
213
214 #[test]
215 fn test_join_lines_lambda_block() {
216 check_join_lines(
217 r"
218pub fn reparse(&self, edit: &AtomTextEdit) -> File {
219 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
220 self.full_reparse(edit)
221 })
222}
223",
224 r"
225pub fn reparse(&self, edit: &AtomTextEdit) -> File {
226 <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
227}
228",
229 );
230 }
231
232 #[test]
233 fn test_join_lines_block() {
234 check_join_lines(
235 r"
236fn foo() {
237 foo(<|>{
238 92
239 })
240}",
241 r"
242fn foo() {
243 foo(<|>92)
244}",
245 );
246 }
247
248 #[test]
249 fn test_join_lines_diverging_block() {
250 let before = r"
251 fn foo() {
252 loop {
253 match x {
254 92 => <|>{
255 continue;
256 }
257 }
258 }
259 }
260 ";
261 let after = r"
262 fn foo() {
263 loop {
264 match x {
265 92 => <|>continue,
266 }
267 }
268 }
269 ";
270 check_join_lines(before, after);
271 }
272
273 #[test]
274 fn join_lines_adds_comma_for_block_in_match_arm() {
275 check_join_lines(
276 r"
277fn foo(e: Result<U, V>) {
278 match e {
279 Ok(u) => <|>{
280 u.foo()
281 }
282 Err(v) => v,
283 }
284}",
285 r"
286fn foo(e: Result<U, V>) {
287 match e {
288 Ok(u) => <|>u.foo(),
289 Err(v) => v,
290 }
291}",
292 );
293 }
294
295 #[test]
296 fn join_lines_multiline_in_block() {
297 check_join_lines(
298 r"
299fn foo() {
300 match ty {
301 <|> Some(ty) => {
302 match ty {
303 _ => false,
304 }
305 }
306 _ => true,
307 }
308}
309",
310 r"
311fn foo() {
312 match ty {
313 <|> Some(ty) => match ty {
314 _ => false,
315 },
316 _ => true,
317 }
318}
319",
320 );
321 }
322
323 #[test]
324 fn join_lines_keeps_comma_for_block_in_match_arm() {
325 // We already have a comma
326 check_join_lines(
327 r"
328fn foo(e: Result<U, V>) {
329 match e {
330 Ok(u) => <|>{
331 u.foo()
332 },
333 Err(v) => v,
334 }
335}",
336 r"
337fn foo(e: Result<U, V>) {
338 match e {
339 Ok(u) => <|>u.foo(),
340 Err(v) => v,
341 }
342}",
343 );
344
345 // comma with whitespace between brace and ,
346 check_join_lines(
347 r"
348fn foo(e: Result<U, V>) {
349 match e {
350 Ok(u) => <|>{
351 u.foo()
352 } ,
353 Err(v) => v,
354 }
355}",
356 r"
357fn foo(e: Result<U, V>) {
358 match e {
359 Ok(u) => <|>u.foo() ,
360 Err(v) => v,
361 }
362}",
363 );
364
365 // comma with newline between brace and ,
366 check_join_lines(
367 r"
368fn foo(e: Result<U, V>) {
369 match e {
370 Ok(u) => <|>{
371 u.foo()
372 }
373 ,
374 Err(v) => v,
375 }
376}",
377 r"
378fn foo(e: Result<U, V>) {
379 match e {
380 Ok(u) => <|>u.foo()
381 ,
382 Err(v) => v,
383 }
384}",
385 );
386 }
387
388 #[test]
389 fn join_lines_keeps_comma_with_single_arg_tuple() {
390 // A single arg tuple
391 check_join_lines(
392 r"
393fn foo() {
394 let x = (<|>{
395 4
396 },);
397}",
398 r"
399fn foo() {
400 let x = (<|>4,);
401}",
402 );
403
404 // single arg tuple with whitespace between brace and comma
405 check_join_lines(
406 r"
407fn foo() {
408 let x = (<|>{
409 4
410 } ,);
411}",
412 r"
413fn foo() {
414 let x = (<|>4 ,);
415}",
416 );
417
418 // single arg tuple with newline between brace and comma
419 check_join_lines(
420 r"
421fn foo() {
422 let x = (<|>{
423 4
424 }
425 ,);
426}",
427 r"
428fn foo() {
429 let x = (<|>4
430 ,);
431}",
432 );
433 }
434
435 #[test]
436 fn test_join_lines_use_items_left() {
437 // No space after the '{'
438 check_join_lines(
439 r"
440<|>use ra_syntax::{
441 TextSize, TextRange,
442};",
443 r"
444<|>use ra_syntax::{TextSize, TextRange,
445};",
446 );
447 }
448
449 #[test]
450 fn test_join_lines_use_items_right() {
451 // No space after the '}'
452 check_join_lines(
453 r"
454use ra_syntax::{
455<|> TextSize, TextRange
456};",
457 r"
458use ra_syntax::{
459<|> TextSize, TextRange};",
460 );
461 }
462
463 #[test]
464 fn test_join_lines_use_items_right_comma() {
465 // No space after the '}'
466 check_join_lines(
467 r"
468use ra_syntax::{
469<|> TextSize, TextRange,
470};",
471 r"
472use ra_syntax::{
473<|> TextSize, TextRange};",
474 );
475 }
476
477 #[test]
478 fn test_join_lines_use_tree() {
479 check_join_lines(
480 r"
481use ra_syntax::{
482 algo::<|>{
483 find_token_at_offset,
484 },
485 ast,
486};",
487 r"
488use ra_syntax::{
489 algo::<|>find_token_at_offset,
490 ast,
491};",
492 );
493 }
494
495 #[test]
496 fn test_join_lines_normal_comments() {
497 check_join_lines(
498 r"
499fn foo() {
500 // Hello<|>
501 // world!
502}
503",
504 r"
505fn foo() {
506 // Hello<|> world!
507}
508",
509 );
510 }
511
512 #[test]
513 fn test_join_lines_doc_comments() {
514 check_join_lines(
515 r"
516fn foo() {
517 /// Hello<|>
518 /// world!
519}
520",
521 r"
522fn foo() {
523 /// Hello<|> world!
524}
525",
526 );
527 }
528
529 #[test]
530 fn test_join_lines_mod_comments() {
531 check_join_lines(
532 r"
533fn foo() {
534 //! Hello<|>
535 //! world!
536}
537",
538 r"
539fn foo() {
540 //! Hello<|> world!
541}
542",
543 );
544 }
545
546 #[test]
547 fn test_join_lines_multiline_comments_1() {
548 check_join_lines(
549 r"
550fn foo() {
551 // Hello<|>
552 /* world! */
553}
554",
555 r"
556fn foo() {
557 // Hello<|> world! */
558}
559",
560 );
561 }
562
563 #[test]
564 fn test_join_lines_multiline_comments_2() {
565 check_join_lines(
566 r"
567fn foo() {
568 // The<|>
569 /* quick
570 brown
571 fox! */
572}
573",
574 r"
575fn foo() {
576 // The<|> quick
577 brown
578 fox! */
579}
580",
581 );
582 }
583
584 fn check_join_lines_sel(before: &str, after: &str) {
585 let (sel, before) = extract_range(before);
586 let parse = SourceFile::parse(&before);
587 let result = join_lines(&parse.tree(), sel);
588 let actual = {
589 let mut actual = before.to_string();
590 result.apply(&mut actual);
591 actual
592 };
593 assert_eq_text!(after, &actual);
594 }
595
596 #[test]
597 fn test_join_lines_selection_fn_args() {
598 check_join_lines_sel(
599 r"
600fn foo() {
601 <|>foo(1,
602 2,
603 3,
604 <|>)
605}
606 ",
607 r"
608fn foo() {
609 foo(1, 2, 3)
610}
611 ",
612 );
613 }
614
615 #[test]
616 fn test_join_lines_selection_struct() {
617 check_join_lines_sel(
618 r"
619struct Foo <|>{
620 f: u32,
621}<|>
622 ",
623 r"
624struct Foo { f: u32 }
625 ",
626 );
627 }
628
629 #[test]
630 fn test_join_lines_selection_dot_chain() {
631 check_join_lines_sel(
632 r"
633fn foo() {
634 join(<|>type_params.type_params()
635 .filter_map(|it| it.name())
636 .map(|it| it.text())<|>)
637}",
638 r"
639fn foo() {
640 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
641}",
642 );
643 }
644
645 #[test]
646 fn test_join_lines_selection_lambda_block_body() {
647 check_join_lines_sel(
648 r"
649pub fn handle_find_matching_brace() {
650 params.offsets
651 .map(|offset| <|>{
652 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
653 }<|>)
654 .collect();
655}",
656 r"
657pub fn handle_find_matching_brace() {
658 params.offsets
659 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
660 .collect();
661}",
662 );
663 }
664
665 #[test]
666 fn test_join_lines_commented_block() {
667 check_join_lines(
668 r"
669fn main() {
670 let _ = {
671 // <|>foo
672 // bar
673 92
674 };
675}
676 ",
677 r"
678fn main() {
679 let _ = {
680 // <|>foo bar
681 92
682 };
683}
684 ",
685 )
686 }
687
688 #[test]
689 fn join_lines_mandatory_blocks_block() {
690 check_join_lines(
691 r"
692<|>fn foo() {
693 92
694}
695 ",
696 r"
697<|>fn foo() { 92
698}
699 ",
700 );
701
702 check_join_lines(
703 r"
704fn foo() {
705 <|>if true {
706 92
707 }
708}
709 ",
710 r"
711fn foo() {
712 <|>if true { 92
713 }
714}
715 ",
716 );
717
718 check_join_lines(
719 r"
720fn foo() {
721 <|>loop {
722 92
723 }
724}
725 ",
726 r"
727fn foo() {
728 <|>loop { 92
729 }
730}
731 ",
732 );
733
734 check_join_lines(
735 r"
736fn foo() {
737 <|>unsafe {
738 92
739 }
740}
741 ",
742 r"
743fn foo() {
744 <|>unsafe { 92
745 }
746}
747 ",
748 );
749 }
750}