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