aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light/src/join_lines.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light/src/join_lines.rs')
-rw-r--r--crates/ra_ide_api_light/src/join_lines.rs451
1 files changed, 451 insertions, 0 deletions
diff --git a/crates/ra_ide_api_light/src/join_lines.rs b/crates/ra_ide_api_light/src/join_lines.rs
new file mode 100644
index 000000000..ab7c5b4b5
--- /dev/null
+++ b/crates/ra_ide_api_light/src/join_lines.rs
@@ -0,0 +1,451 @@
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,
6 ast,
7};
8
9use crate::{
10 LocalEdit, TextEditBuilder,
11 formatting::{compute_ws, extract_trivial_expression},
12};
13
14pub fn join_lines(file: &SourceFile, range: TextRange) -> LocalEdit {
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('\n') {
19 None => {
20 return LocalEdit {
21 label: "join lines".to_string(),
22 edit: TextEditBuilder::default().finish(),
23 cursor_position: None,
24 };
25 }
26 Some(pos) => pos,
27 };
28 TextRange::offset_len(range.start() + pos, TextUnit::of_char('\n'))
29 } else {
30 range
31 };
32
33 let node = find_covering_node(file.syntax(), range);
34 let mut edit = TextEditBuilder::default();
35 for node in node.descendants() {
36 let text = match node.leaf_text() {
37 Some(text) => text,
38 None => continue,
39 };
40 let range = match range.intersection(&node.range()) {
41 Some(range) => range,
42 None => continue,
43 } - node.range().start();
44 for (pos, _) in text[range].bytes().enumerate().filter(|&(_, b)| b == b'\n') {
45 let pos: TextUnit = (pos as u32).into();
46 let off = node.range().start() + range.start() + pos;
47 if !edit.invalidates_offset(off) {
48 remove_newline(&mut edit, node, text.as_str(), off);
49 }
50 }
51 }
52
53 LocalEdit {
54 label: "join lines".to_string(),
55 edit: edit.finish(),
56 cursor_position: None,
57 }
58}
59
60fn remove_newline(
61 edit: &mut TextEditBuilder,
62 node: &SyntaxNode,
63 node_text: &str,
64 offset: TextUnit,
65) {
66 if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 {
67 // The node is either the first or the last in the file
68 let suff = &node_text[TextRange::from_to(
69 offset - node.range().start() + TextUnit::of_char('\n'),
70 TextUnit::of_str(node_text),
71 )];
72 let spaces = suff.bytes().take_while(|&b| b == b' ').count();
73
74 edit.replace(
75 TextRange::offset_len(offset, ((spaces + 1) as u32).into()),
76 " ".to_string(),
77 );
78 return;
79 }
80
81 // Special case that turns something like:
82 //
83 // ```
84 // my_function({<|>
85 // <some-expr>
86 // })
87 // ```
88 //
89 // into `my_function(<some-expr>)`
90 if join_single_expr_block(edit, node).is_some() {
91 return;
92 }
93 // ditto for
94 //
95 // ```
96 // use foo::{<|>
97 // bar
98 // };
99 // ```
100 if join_single_use_tree(edit, node).is_some() {
101 return;
102 }
103
104 // The node is between two other nodes
105 let prev = node.prev_sibling().unwrap();
106 let next = node.next_sibling().unwrap();
107 if is_trailing_comma(prev.kind(), next.kind()) {
108 // Removes: trailing comma, newline (incl. surrounding whitespace)
109 edit.delete(TextRange::from_to(prev.range().start(), node.range().end()));
110 } else if prev.kind() == COMMA && next.kind() == R_CURLY {
111 // Removes: comma, newline (incl. surrounding whitespace)
112 let space = if let Some(left) = prev.prev_sibling() {
113 compute_ws(left, next)
114 } else {
115 " "
116 };
117 edit.replace(
118 TextRange::from_to(prev.range().start(), node.range().end()),
119 space.to_string(),
120 );
121 } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) {
122 // Removes: newline (incl. surrounding whitespace), start of the next comment
123 edit.delete(TextRange::from_to(
124 node.range().start(),
125 next.syntax().range().start() + TextUnit::of_str(next.prefix()),
126 ));
127 } else {
128 // Remove newline but add a computed amount of whitespace characters
129 edit.replace(node.range(), compute_ws(prev, next).to_string());
130 }
131}
132
133fn join_single_expr_block(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
134 let block = ast::Block::cast(node.parent()?)?;
135 let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
136 let expr = extract_trivial_expression(block)?;
137 edit.replace(
138 block_expr.syntax().range(),
139 expr.syntax().text().to_string(),
140 );
141 Some(())
142}
143
144fn join_single_use_tree(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
145 let use_tree_list = ast::UseTreeList::cast(node.parent()?)?;
146 let (tree,) = use_tree_list.use_trees().collect_tuple()?;
147 edit.replace(
148 use_tree_list.syntax().range(),
149 tree.syntax().text().to_string(),
150 );
151 Some(())
152}
153
154fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
155 match (left, right) {
156 (COMMA, R_PAREN) | (COMMA, R_BRACK) => true,
157 _ => false,
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use crate::test_utils::{assert_eq_text, check_action, extract_range};
164
165 use super::*;
166
167 fn check_join_lines(before: &str, after: &str) {
168 check_action(before, after, |file, offset| {
169 let range = TextRange::offset_len(offset, 0.into());
170 let res = join_lines(file, range);
171 Some(res)
172 })
173 }
174
175 #[test]
176 fn test_join_lines_comma() {
177 check_join_lines(
178 r"
179fn foo() {
180 <|>foo(1,
181 )
182}
183",
184 r"
185fn foo() {
186 <|>foo(1)
187}
188",
189 );
190 }
191
192 #[test]
193 fn test_join_lines_lambda_block() {
194 check_join_lines(
195 r"
196pub fn reparse(&self, edit: &AtomTextEdit) -> File {
197 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
198 self.full_reparse(edit)
199 })
200}
201",
202 r"
203pub fn reparse(&self, edit: &AtomTextEdit) -> File {
204 <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
205}
206",
207 );
208 }
209
210 #[test]
211 fn test_join_lines_block() {
212 check_join_lines(
213 r"
214fn foo() {
215 foo(<|>{
216 92
217 })
218}",
219 r"
220fn foo() {
221 foo(<|>92)
222}",
223 );
224 }
225
226 #[test]
227 fn test_join_lines_use_items_left() {
228 // No space after the '{'
229 check_join_lines(
230 r"
231<|>use ra_syntax::{
232 TextUnit, TextRange,
233};",
234 r"
235<|>use ra_syntax::{TextUnit, TextRange,
236};",
237 );
238 }
239
240 #[test]
241 fn test_join_lines_use_items_right() {
242 // No space after the '}'
243 check_join_lines(
244 r"
245use ra_syntax::{
246<|> TextUnit, TextRange
247};",
248 r"
249use ra_syntax::{
250<|> TextUnit, TextRange};",
251 );
252 }
253
254 #[test]
255 fn test_join_lines_use_items_right_comma() {
256 // No space after the '}'
257 check_join_lines(
258 r"
259use ra_syntax::{
260<|> TextUnit, TextRange,
261};",
262 r"
263use ra_syntax::{
264<|> TextUnit, TextRange};",
265 );
266 }
267
268 #[test]
269 fn test_join_lines_use_tree() {
270 check_join_lines(
271 r"
272use ra_syntax::{
273 algo::<|>{
274 find_leaf_at_offset,
275 },
276 ast,
277};",
278 r"
279use ra_syntax::{
280 algo::<|>find_leaf_at_offset,
281 ast,
282};",
283 );
284 }
285
286 #[test]
287 fn test_join_lines_normal_comments() {
288 check_join_lines(
289 r"
290fn foo() {
291 // Hello<|>
292 // world!
293}
294",
295 r"
296fn foo() {
297 // Hello<|> world!
298}
299",
300 );
301 }
302
303 #[test]
304 fn test_join_lines_doc_comments() {
305 check_join_lines(
306 r"
307fn foo() {
308 /// Hello<|>
309 /// world!
310}
311",
312 r"
313fn foo() {
314 /// Hello<|> world!
315}
316",
317 );
318 }
319
320 #[test]
321 fn test_join_lines_mod_comments() {
322 check_join_lines(
323 r"
324fn foo() {
325 //! Hello<|>
326 //! world!
327}
328",
329 r"
330fn foo() {
331 //! Hello<|> world!
332}
333",
334 );
335 }
336
337 #[test]
338 fn test_join_lines_multiline_comments_1() {
339 check_join_lines(
340 r"
341fn foo() {
342 // Hello<|>
343 /* world! */
344}
345",
346 r"
347fn foo() {
348 // Hello<|> world! */
349}
350",
351 );
352 }
353
354 #[test]
355 fn test_join_lines_multiline_comments_2() {
356 check_join_lines(
357 r"
358fn foo() {
359 // The<|>
360 /* quick
361 brown
362 fox! */
363}
364",
365 r"
366fn foo() {
367 // The<|> quick
368 brown
369 fox! */
370}
371",
372 );
373 }
374
375 fn check_join_lines_sel(before: &str, after: &str) {
376 let (sel, before) = extract_range(before);
377 let file = SourceFile::parse(&before);
378 let result = join_lines(&file, sel);
379 let actual = result.edit.apply(&before);
380 assert_eq_text!(after, &actual);
381 }
382
383 #[test]
384 fn test_join_lines_selection_fn_args() {
385 check_join_lines_sel(
386 r"
387fn foo() {
388 <|>foo(1,
389 2,
390 3,
391 <|>)
392}
393 ",
394 r"
395fn foo() {
396 foo(1, 2, 3)
397}
398 ",
399 );
400 }
401
402 #[test]
403 fn test_join_lines_selection_struct() {
404 check_join_lines_sel(
405 r"
406struct Foo <|>{
407 f: u32,
408}<|>
409 ",
410 r"
411struct Foo { f: u32 }
412 ",
413 );
414 }
415
416 #[test]
417 fn test_join_lines_selection_dot_chain() {
418 check_join_lines_sel(
419 r"
420fn foo() {
421 join(<|>type_params.type_params()
422 .filter_map(|it| it.name())
423 .map(|it| it.text())<|>)
424}",
425 r"
426fn foo() {
427 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
428}",
429 );
430 }
431
432 #[test]
433 fn test_join_lines_selection_lambda_block_body() {
434 check_join_lines_sel(
435 r"
436pub fn handle_find_matching_brace() {
437 params.offsets
438 .map(|offset| <|>{
439 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
440 }<|>)
441 .collect();
442}",
443 r"
444pub fn handle_find_matching_brace() {
445 params.offsets
446 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
447 .collect();
448}",
449 );
450 }
451}