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.rs611
1 files changed, 611 insertions, 0 deletions
diff --git a/crates/ra_ide/src/join_lines.rs b/crates/ra_ide/src/join_lines.rs
new file mode 100644
index 000000000..7deeb3494
--- /dev/null
+++ b/crates/ra_ide/src/join_lines.rs
@@ -0,0 +1,611 @@
1//! FIXME: write short doc here
2
3use itertools::Itertools;
4use ra_fmt::{compute_ws, extract_trivial_expression};
5use ra_syntax::{
6 algo::{find_covering_element, non_trivia_sibling},
7 ast::{self, AstNode, AstToken},
8 Direction, NodeOrToken, SourceFile,
9 SyntaxKind::{self, WHITESPACE},
10 SyntaxNode, SyntaxToken, TextRange, TextUnit, T,
11};
12use ra_text_edit::{TextEdit, TextEditBuilder};
13
14pub fn join_lines(file: &SourceFile, range: TextRange) -> TextEdit {
15 let range = if range.is_empty() {
16 let syntax = file.syntax();
17 let text = syntax.text().slice(range.start()..);
18 let pos = match text.find_char('\n') {
19 None => return TextEditBuilder::default().finish(),
20 Some(pos) => pos,
21 };
22 TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n'))
23 } else {
24 range
25 };
26
27 let node = match find_covering_element(file.syntax(), range) {
28 NodeOrToken::Node(node) => node,
29 NodeOrToken::Token(token) => token.parent(),
30 };
31 let mut edit = TextEditBuilder::default();
32 for token in node.descendants_with_tokens().filter_map(|it| it.into_token()) {
33 let range = match range.intersection(&token.text_range()) {
34 Some(range) => range,
35 None => continue,
36 } - token.text_range().start();
37 let text = token.text();
38 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
39 let pos: TextUnit = (pos as u32).into();
40 let off = token.text_range().start() + range.start() + pos;
41 if !edit.invalidates_offset(off) {
42 remove_newline(&mut edit, &token, off);
43 }
44 }
45 }
46
47 edit.finish()
48}
49
50fn remove_newline(edit: &mut TextEditBuilder, token: &SyntaxToken, offset: TextUnit) {
51 if token.kind() != WHITESPACE || token.text().bytes().filter(|&b| b == b'\n').count() != 1 {
52 // The node is either the first or the last in the file
53 let suff = &token.text()[TextRange::from_to(
54 offset - token.text_range().start() + TextUnit::of_char('\n'),
55 TextUnit::of_str(token.text()),
56 )];
57 let spaces = suff.bytes().take_while(|&b| b == b' ').count();
58
59 edit.replace(TextRange::offset_len(offset, ((spaces + 1) as u32).into()), " ".to_string());
60 return;
61 }
62
63 // Special case that turns something like:
64 //
65 // ```
66 // my_function({<|>
67 // <some-expr>
68 // })
69 // ```
70 //
71 // into `my_function(<some-expr>)`
72 if join_single_expr_block(edit, token).is_some() {
73 return;
74 }
75 // ditto for
76 //
77 // ```
78 // use foo::{<|>
79 // bar
80 // };
81 // ```
82 if join_single_use_tree(edit, token).is_some() {
83 return;
84 }
85
86 // The node is between two other nodes
87 let prev = token.prev_sibling_or_token().unwrap();
88 let next = token.next_sibling_or_token().unwrap();
89 if is_trailing_comma(prev.kind(), next.kind()) {
90 // Removes: trailing comma, newline (incl. surrounding whitespace)
91 edit.delete(TextRange::from_to(prev.text_range().start(), token.text_range().end()));
92 } else if prev.kind() == T![,] && next.kind() == T!['}'] {
93 // Removes: comma, newline (incl. surrounding whitespace)
94 let space = if let Some(left) = prev.prev_sibling_or_token() {
95 compute_ws(left.kind(), next.kind())
96 } else {
97 " "
98 };
99 edit.replace(
100 TextRange::from_to(prev.text_range().start(), token.text_range().end()),
101 space.to_string(),
102 );
103 } else if let (Some(_), Some(next)) = (
104 prev.as_token().cloned().and_then(ast::Comment::cast),
105 next.as_token().cloned().and_then(ast::Comment::cast),
106 ) {
107 // Removes: newline (incl. surrounding whitespace), start of the next comment
108 edit.delete(TextRange::from_to(
109 token.text_range().start(),
110 next.syntax().text_range().start() + TextUnit::of_str(next.prefix()),
111 ));
112 } else {
113 // Remove newline but add a computed amount of whitespace characters
114 edit.replace(token.text_range(), compute_ws(prev.kind(), next.kind()).to_string());
115 }
116}
117
118fn has_comma_after(node: &SyntaxNode) -> bool {
119 match non_trivia_sibling(node.clone().into(), Direction::Next) {
120 Some(n) => n.kind() == T![,],
121 _ => false,
122 }
123}
124
125fn join_single_expr_block(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
126 let block = ast::Block::cast(token.parent())?;
127 let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
128 let expr = extract_trivial_expression(&block_expr)?;
129
130 let block_range = block_expr.syntax().text_range();
131 let mut buf = expr.syntax().text().to_string();
132
133 // Match block needs to have a comma after the block
134 if let Some(match_arm) = block_expr.syntax().parent().and_then(ast::MatchArm::cast) {
135 if !has_comma_after(match_arm.syntax()) {
136 buf.push(',');
137 }
138 }
139
140 edit.replace(block_range, buf);
141
142 Some(())
143}
144
145fn join_single_use_tree(edit: &mut TextEditBuilder, token: &SyntaxToken) -> Option<()> {
146 let use_tree_list = ast::UseTreeList::cast(token.parent())?;
147 let (tree,) = use_tree_list.use_trees().collect_tuple()?;
148 edit.replace(use_tree_list.syntax().text_range(), tree.syntax().text().to_string());
149 Some(())
150}
151
152fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
153 match (left, right) {
154 (T![,], T![')']) | (T![,], T![']']) => true,
155 _ => false,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use crate::test_utils::{assert_eq_text, check_action, extract_range};
162
163 use super::*;
164
165 fn check_join_lines(before: &str, after: &str) {
166 check_action(before, after, |file, offset| {
167 let range = TextRange::offset_len(offset, 0.into());
168 let res = join_lines(file, range);
169 Some(res)
170 })
171 }
172
173 #[test]
174 fn test_join_lines_comma() {
175 check_join_lines(
176 r"
177fn foo() {
178 <|>foo(1,
179 )
180}
181",
182 r"
183fn foo() {
184 <|>foo(1)
185}
186",
187 );
188 }
189
190 #[test]
191 fn test_join_lines_lambda_block() {
192 check_join_lines(
193 r"
194pub fn reparse(&self, edit: &AtomTextEdit) -> File {
195 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
196 self.full_reparse(edit)
197 })
198}
199",
200 r"
201pub fn reparse(&self, edit: &AtomTextEdit) -> File {
202 <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
203}
204",
205 );
206 }
207
208 #[test]
209 fn test_join_lines_block() {
210 check_join_lines(
211 r"
212fn foo() {
213 foo(<|>{
214 92
215 })
216}",
217 r"
218fn foo() {
219 foo(<|>92)
220}",
221 );
222 }
223
224 #[test]
225 fn join_lines_adds_comma_for_block_in_match_arm() {
226 check_join_lines(
227 r"
228fn foo(e: Result<U, V>) {
229 match e {
230 Ok(u) => <|>{
231 u.foo()
232 }
233 Err(v) => v,
234 }
235}",
236 r"
237fn foo(e: Result<U, V>) {
238 match e {
239 Ok(u) => <|>u.foo(),
240 Err(v) => v,
241 }
242}",
243 );
244 }
245
246 #[test]
247 fn join_lines_multiline_in_block() {
248 check_join_lines(
249 r"
250fn foo() {
251 match ty {
252 <|> Some(ty) => {
253 match ty {
254 _ => false,
255 }
256 }
257 _ => true,
258 }
259}
260",
261 r"
262fn foo() {
263 match ty {
264 <|> Some(ty) => match ty {
265 _ => false,
266 },
267 _ => true,
268 }
269}
270",
271 );
272 }
273
274 #[test]
275 fn join_lines_keeps_comma_for_block_in_match_arm() {
276 // We already have a comma
277 check_join_lines(
278 r"
279fn foo(e: Result<U, V>) {
280 match e {
281 Ok(u) => <|>{
282 u.foo()
283 },
284 Err(v) => v,
285 }
286}",
287 r"
288fn foo(e: Result<U, V>) {
289 match e {
290 Ok(u) => <|>u.foo(),
291 Err(v) => v,
292 }
293}",
294 );
295
296 // comma with whitespace between brace and ,
297 check_join_lines(
298 r"
299fn foo(e: Result<U, V>) {
300 match e {
301 Ok(u) => <|>{
302 u.foo()
303 } ,
304 Err(v) => v,
305 }
306}",
307 r"
308fn foo(e: Result<U, V>) {
309 match e {
310 Ok(u) => <|>u.foo() ,
311 Err(v) => v,
312 }
313}",
314 );
315
316 // comma with newline between brace and ,
317 check_join_lines(
318 r"
319fn foo(e: Result<U, V>) {
320 match e {
321 Ok(u) => <|>{
322 u.foo()
323 }
324 ,
325 Err(v) => v,
326 }
327}",
328 r"
329fn foo(e: Result<U, V>) {
330 match e {
331 Ok(u) => <|>u.foo()
332 ,
333 Err(v) => v,
334 }
335}",
336 );
337 }
338
339 #[test]
340 fn join_lines_keeps_comma_with_single_arg_tuple() {
341 // A single arg tuple
342 check_join_lines(
343 r"
344fn foo() {
345 let x = (<|>{
346 4
347 },);
348}",
349 r"
350fn foo() {
351 let x = (<|>4,);
352}",
353 );
354
355 // single arg tuple with whitespace between brace and comma
356 check_join_lines(
357 r"
358fn foo() {
359 let x = (<|>{
360 4
361 } ,);
362}",
363 r"
364fn foo() {
365 let x = (<|>4 ,);
366}",
367 );
368
369 // single arg tuple with newline between brace and comma
370 check_join_lines(
371 r"
372fn foo() {
373 let x = (<|>{
374 4
375 }
376 ,);
377}",
378 r"
379fn foo() {
380 let x = (<|>4
381 ,);
382}",
383 );
384 }
385
386 #[test]
387 fn test_join_lines_use_items_left() {
388 // No space after the '{'
389 check_join_lines(
390 r"
391<|>use ra_syntax::{
392 TextUnit, TextRange,
393};",
394 r"
395<|>use ra_syntax::{TextUnit, TextRange,
396};",
397 );
398 }
399
400 #[test]
401 fn test_join_lines_use_items_right() {
402 // No space after the '}'
403 check_join_lines(
404 r"
405use ra_syntax::{
406<|> TextUnit, TextRange
407};",
408 r"
409use ra_syntax::{
410<|> TextUnit, TextRange};",
411 );
412 }
413
414 #[test]
415 fn test_join_lines_use_items_right_comma() {
416 // No space after the '}'
417 check_join_lines(
418 r"
419use ra_syntax::{
420<|> TextUnit, TextRange,
421};",
422 r"
423use ra_syntax::{
424<|> TextUnit, TextRange};",
425 );
426 }
427
428 #[test]
429 fn test_join_lines_use_tree() {
430 check_join_lines(
431 r"
432use ra_syntax::{
433 algo::<|>{
434 find_token_at_offset,
435 },
436 ast,
437};",
438 r"
439use ra_syntax::{
440 algo::<|>find_token_at_offset,
441 ast,
442};",
443 );
444 }
445
446 #[test]
447 fn test_join_lines_normal_comments() {
448 check_join_lines(
449 r"
450fn foo() {
451 // Hello<|>
452 // world!
453}
454",
455 r"
456fn foo() {
457 // Hello<|> world!
458}
459",
460 );
461 }
462
463 #[test]
464 fn test_join_lines_doc_comments() {
465 check_join_lines(
466 r"
467fn foo() {
468 /// Hello<|>
469 /// world!
470}
471",
472 r"
473fn foo() {
474 /// Hello<|> world!
475}
476",
477 );
478 }
479
480 #[test]
481 fn test_join_lines_mod_comments() {
482 check_join_lines(
483 r"
484fn foo() {
485 //! Hello<|>
486 //! world!
487}
488",
489 r"
490fn foo() {
491 //! Hello<|> world!
492}
493",
494 );
495 }
496
497 #[test]
498 fn test_join_lines_multiline_comments_1() {
499 check_join_lines(
500 r"
501fn foo() {
502 // Hello<|>
503 /* world! */
504}
505",
506 r"
507fn foo() {
508 // Hello<|> world! */
509}
510",
511 );
512 }
513
514 #[test]
515 fn test_join_lines_multiline_comments_2() {
516 check_join_lines(
517 r"
518fn foo() {
519 // The<|>
520 /* quick
521 brown
522 fox! */
523}
524",
525 r"
526fn foo() {
527 // The<|> quick
528 brown
529 fox! */
530}
531",
532 );
533 }
534
535 fn check_join_lines_sel(before: &str, after: &str) {
536 let (sel, before) = extract_range(before);
537 let parse = SourceFile::parse(&before);
538 let result = join_lines(&parse.tree(), sel);
539 let actual = result.apply(&before);
540 assert_eq_text!(after, &actual);
541 }
542
543 #[test]
544 fn test_join_lines_selection_fn_args() {
545 check_join_lines_sel(
546 r"
547fn foo() {
548 <|>foo(1,
549 2,
550 3,
551 <|>)
552}
553 ",
554 r"
555fn foo() {
556 foo(1, 2, 3)
557}
558 ",
559 );
560 }
561
562 #[test]
563 fn test_join_lines_selection_struct() {
564 check_join_lines_sel(
565 r"
566struct Foo <|>{
567 f: u32,
568}<|>
569 ",
570 r"
571struct Foo { f: u32 }
572 ",
573 );
574 }
575
576 #[test]
577 fn test_join_lines_selection_dot_chain() {
578 check_join_lines_sel(
579 r"
580fn foo() {
581 join(<|>type_params.type_params()
582 .filter_map(|it| it.name())
583 .map(|it| it.text())<|>)
584}",
585 r"
586fn foo() {
587 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
588}",
589 );
590 }
591
592 #[test]
593 fn test_join_lines_selection_lambda_block_body() {
594 check_join_lines_sel(
595 r"
596pub fn handle_find_matching_brace() {
597 params.offsets
598 .map(|offset| <|>{
599 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
600 }<|>)
601 .collect();
602}",
603 r"
604pub fn handle_find_matching_brace() {
605 params.offsets
606 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
607 .collect();
608}",
609 );
610 }
611}