aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide_api_light
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide_api_light')
-rw-r--r--crates/ra_ide_api_light/src/assists/change_visibility.rs69
-rw-r--r--crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs26
-rw-r--r--crates/ra_ide_api_light/src/formatting.rs42
-rw-r--r--crates/ra_ide_api_light/src/join_lines.rs451
-rw-r--r--crates/ra_ide_api_light/src/lib.rs8
-rw-r--r--crates/ra_ide_api_light/src/typing.rs490
6 files changed, 569 insertions, 517 deletions
diff --git a/crates/ra_ide_api_light/src/assists/change_visibility.rs b/crates/ra_ide_api_light/src/assists/change_visibility.rs
index 89729e2c2..6e8bc2632 100644
--- a/crates/ra_ide_api_light/src/assists/change_visibility.rs
+++ b/crates/ra_ide_api_light/src/assists/change_visibility.rs
@@ -1,7 +1,7 @@
1use ra_syntax::{ 1use ra_syntax::{
2 AstNode, 2 AstNode, SyntaxNode, TextUnit,
3 ast::{self, VisibilityOwner, NameOwner}, 3 ast::{self, VisibilityOwner, NameOwner},
4 SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT}, 4 SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT, WHITESPACE, COMMENT, ATTR},
5}; 5};
6 6
7use crate::assists::{AssistCtx, Assist}; 7use crate::assists::{AssistCtx, Assist};
@@ -30,14 +30,14 @@ fn add_vis(ctx: AssistCtx) -> Option<Assist> {
30 if parent.children().any(|child| child.kind() == VISIBILITY) { 30 if parent.children().any(|child| child.kind() == VISIBILITY) {
31 return None; 31 return None;
32 } 32 }
33 parent.range().start() 33 vis_offset(parent)
34 } else { 34 } else {
35 let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?; 35 let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?;
36 let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?; 36 let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?;
37 if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() { 37 if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() {
38 return None; 38 return None;
39 } 39 }
40 field.syntax().range().start() 40 vis_offset(field.syntax())
41 }; 41 };
42 42
43 ctx.build("make pub(crate)", |edit| { 43 ctx.build("make pub(crate)", |edit| {
@@ -46,14 +46,31 @@ fn add_vis(ctx: AssistCtx) -> Option<Assist> {
46 }) 46 })
47} 47}
48 48
49fn vis_offset(node: &SyntaxNode) -> TextUnit {
50 node.children()
51 .skip_while(|it| match it.kind() {
52 WHITESPACE | COMMENT | ATTR => true,
53 _ => false,
54 })
55 .next()
56 .map(|it| it.range().start())
57 .unwrap_or(node.range().start())
58}
59
49fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> { 60fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> {
50 if vis.syntax().text() != "pub" { 61 if vis.syntax().text() == "pub" {
51 return None; 62 return ctx.build("chage to pub(crate)", |edit| {
63 edit.replace(vis.syntax().range(), "pub(crate)");
64 edit.set_cursor(vis.syntax().range().start());
65 });
52 } 66 }
53 ctx.build("chage to pub(crate)", |edit| { 67 if vis.syntax().text() == "pub(crate)" {
54 edit.replace(vis.syntax().range(), "pub(crate)"); 68 return ctx.build("chage to pub", |edit| {
55 edit.set_cursor(vis.syntax().range().start()); 69 edit.replace(vis.syntax().range(), "pub");
56 }) 70 edit.set_cursor(vis.syntax().range().start());
71 });
72 }
73 None
57} 74}
58 75
59#[cfg(test)] 76#[cfg(test)]
@@ -113,4 +130,36 @@ mod tests {
113 "<|>pub(crate) fn foo() {}", 130 "<|>pub(crate) fn foo() {}",
114 ) 131 )
115 } 132 }
133
134 #[test]
135 fn change_visibility_pub_crate_to_pub() {
136 check_assist(
137 change_visibility,
138 "<|>pub(crate) fn foo() {}",
139 "<|>pub fn foo() {}",
140 )
141 }
142
143 #[test]
144 fn change_visibility_handles_comment_attrs() {
145 check_assist(
146 change_visibility,
147 "
148 /// docs
149
150 // comments
151
152 #[derive(Debug)]
153 <|>struct Foo;
154 ",
155 "
156 /// docs
157
158 // comments
159
160 #[derive(Debug)]
161 <|>pub(crate) struct Foo;
162 ",
163 )
164 }
116} 165}
diff --git a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs
index 30c371480..d64c34d54 100644
--- a/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs
+++ b/crates/ra_ide_api_light/src/assists/replace_if_let_with_match.rs
@@ -1,9 +1,9 @@
1use ra_syntax::{ 1use ra_syntax::{AstNode, ast};
2 AstNode, SyntaxKind::{L_CURLY, R_CURLY, WHITESPACE},
3 ast,
4};
5 2
6use crate::assists::{AssistCtx, Assist}; 3use crate::{
4 assists::{AssistCtx, Assist},
5 formatting::extract_trivial_expression,
6};
7 7
8pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> { 8pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> {
9 let if_expr: &ast::IfExpr = ctx.node_at_offset()?; 9 let if_expr: &ast::IfExpr = ctx.node_at_offset()?;
@@ -39,26 +39,12 @@ fn build_match_expr(
39} 39}
40 40
41fn format_arm(block: &ast::Block) -> String { 41fn format_arm(block: &ast::Block) -> String {
42 match extract_expression(block) { 42 match extract_trivial_expression(block) {
43 None => block.syntax().text().to_string(), 43 None => block.syntax().text().to_string(),
44 Some(e) => format!("{},", e.syntax().text()), 44 Some(e) => format!("{},", e.syntax().text()),
45 } 45 }
46} 46}
47 47
48fn extract_expression(block: &ast::Block) -> Option<&ast::Expr> {
49 let expr = block.expr()?;
50 let non_trivial_children = block.syntax().children().filter(|it| {
51 !(it == &expr.syntax()
52 || it.kind() == L_CURLY
53 || it.kind() == R_CURLY
54 || it.kind() == WHITESPACE)
55 });
56 if non_trivial_children.count() > 0 {
57 return None;
58 }
59 Some(expr)
60}
61
62#[cfg(test)] 48#[cfg(test)]
63mod tests { 49mod tests {
64 use super::*; 50 use super::*;
diff --git a/crates/ra_ide_api_light/src/formatting.rs b/crates/ra_ide_api_light/src/formatting.rs
new file mode 100644
index 000000000..1f3769209
--- /dev/null
+++ b/crates/ra_ide_api_light/src/formatting.rs
@@ -0,0 +1,42 @@
1use ra_syntax::{
2 ast, AstNode,
3 SyntaxNode, SyntaxKind::*,
4};
5
6pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> {
7 let expr = block.expr()?;
8 if expr.syntax().text().contains('\n') {
9 return None;
10 }
11 let non_trivial_children = block.syntax().children().filter(|it| match it.kind() {
12 WHITESPACE | L_CURLY | R_CURLY => false,
13 _ => it != &expr.syntax(),
14 });
15 if non_trivial_children.count() > 0 {
16 return None;
17 }
18 Some(expr)
19}
20
21pub(crate) fn compute_ws(left: &SyntaxNode, right: &SyntaxNode) -> &'static str {
22 match left.kind() {
23 L_PAREN | L_BRACK => return "",
24 L_CURLY => {
25 if let USE_TREE = right.kind() {
26 return "";
27 }
28 }
29 _ => (),
30 }
31 match right.kind() {
32 R_PAREN | R_BRACK => return "",
33 R_CURLY => {
34 if let USE_TREE = left.kind() {
35 return "";
36 }
37 }
38 DOT => return "",
39 _ => (),
40 }
41 " "
42}
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}
diff --git a/crates/ra_ide_api_light/src/lib.rs b/crates/ra_ide_api_light/src/lib.rs
index 40638eda8..bc9bee752 100644
--- a/crates/ra_ide_api_light/src/lib.rs
+++ b/crates/ra_ide_api_light/src/lib.rs
@@ -11,8 +11,10 @@ mod line_index_utils;
11mod structure; 11mod structure;
12#[cfg(test)] 12#[cfg(test)]
13mod test_utils; 13mod test_utils;
14mod join_lines;
14mod typing; 15mod typing;
15mod diagnostics; 16mod diagnostics;
17pub(crate) mod formatting;
16 18
17pub use self::{ 19pub use self::{
18 assists::LocalEdit, 20 assists::LocalEdit,
@@ -21,8 +23,10 @@ pub use self::{
21 line_index::{LineCol, LineIndex}, 23 line_index::{LineCol, LineIndex},
22 line_index_utils::translate_offset_with_edit, 24 line_index_utils::translate_offset_with_edit,
23 structure::{file_structure, StructureNode}, 25 structure::{file_structure, StructureNode},
24 typing::{join_lines, on_enter, on_dot_typed, on_eq_typed}, 26 diagnostics::diagnostics,
25 diagnostics::diagnostics 27 join_lines::join_lines,
28 typing::{on_enter, on_dot_typed, on_eq_typed},
29
26}; 30};
27use ra_text_edit::TextEditBuilder; 31use ra_text_edit::TextEditBuilder;
28use ra_syntax::{ 32use ra_syntax::{
diff --git a/crates/ra_ide_api_light/src/typing.rs b/crates/ra_ide_api_light/src/typing.rs
index d8177f245..5726209cc 100644
--- a/crates/ra_ide_api_light/src/typing.rs
+++ b/crates/ra_ide_api_light/src/typing.rs
@@ -1,62 +1,12 @@
1use std::mem;
2
3use itertools::Itertools;
4use ra_syntax::{ 1use ra_syntax::{
5 algo::{find_node_at_offset, find_covering_node, find_leaf_at_offset, LeafAtOffset}, 2 algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset},
6 ast, 3 ast,
7 AstNode, Direction, SourceFile, SyntaxKind, 4 AstNode, Direction, SourceFile, SyntaxKind::*,
8 SyntaxKind::*, 5 SyntaxNode, TextUnit,
9 SyntaxNode, TextRange, TextUnit,
10}; 6};
11 7
12use crate::{LocalEdit, TextEditBuilder}; 8use crate::{LocalEdit, TextEditBuilder};
13 9
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
60pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { 10pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
61 let comment = find_leaf_at_offset(file.syntax(), offset) 11 let comment = find_leaf_at_offset(file.syntax(), offset)
62 .left_biased() 12 .left_biased()
@@ -184,441 +134,11 @@ fn last_line_indent_in_whitespace(ws: &str) -> &str {
184 ws.split('\n').last().unwrap_or("") 134 ws.split('\n').last().unwrap_or("")
185} 135}
186 136
187fn remove_newline(
188 edit: &mut TextEditBuilder,
189 node: &SyntaxNode,
190 node_text: &str,
191 offset: TextUnit,
192) {
193 if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 {
194 // The node is either the first or the last in the file
195 let suff = &node_text[TextRange::from_to(
196 offset - node.range().start() + TextUnit::of_char('\n'),
197 TextUnit::of_str(node_text),
198 )];
199 let spaces = suff.bytes().take_while(|&b| b == b' ').count();
200
201 edit.replace(
202 TextRange::offset_len(offset, ((spaces + 1) as u32).into()),
203 " ".to_string(),
204 );
205 return;
206 }
207
208 // Special case that turns something like:
209 //
210 // ```
211 // my_function({<|>
212 // <some-expr>
213 // })
214 // ```
215 //
216 // into `my_function(<some-expr>)`
217 if join_single_expr_block(edit, node).is_some() {
218 return;
219 }
220 // ditto for
221 //
222 // ```
223 // use foo::{<|>
224 // bar
225 // };
226 // ```
227 if join_single_use_tree(edit, node).is_some() {
228 return;
229 }
230
231 // The node is between two other nodes
232 let prev = node.prev_sibling().unwrap();
233 let next = node.next_sibling().unwrap();
234 if is_trailing_comma(prev.kind(), next.kind()) {
235 // Removes: trailing comma, newline (incl. surrounding whitespace)
236 edit.delete(TextRange::from_to(prev.range().start(), node.range().end()));
237 } else if prev.kind() == COMMA && next.kind() == R_CURLY {
238 // Removes: comma, newline (incl. surrounding whitespace)
239 let space = if let Some(left) = prev.prev_sibling() {
240 compute_ws(left, next)
241 } else {
242 " "
243 };
244 edit.replace(
245 TextRange::from_to(prev.range().start(), node.range().end()),
246 space.to_string(),
247 );
248 } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) {
249 // Removes: newline (incl. surrounding whitespace), start of the next comment
250 edit.delete(TextRange::from_to(
251 node.range().start(),
252 next.syntax().range().start() + TextUnit::of_str(next.prefix()),
253 ));
254 } else {
255 // Remove newline but add a computed amount of whitespace characters
256 edit.replace(node.range(), compute_ws(prev, next).to_string());
257 }
258}
259
260fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool {
261 match (left, right) {
262 (COMMA, R_PAREN) | (COMMA, R_BRACK) => true,
263 _ => false,
264 }
265}
266
267fn join_single_expr_block(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
268 let block = ast::Block::cast(node.parent()?)?;
269 let block_expr = ast::BlockExpr::cast(block.syntax().parent()?)?;
270 let expr = single_expr(block)?;
271 edit.replace(
272 block_expr.syntax().range(),
273 expr.syntax().text().to_string(),
274 );
275 Some(())
276}
277
278fn single_expr(block: &ast::Block) -> Option<&ast::Expr> {
279 let mut res = None;
280 for child in block.syntax().children() {
281 if let Some(expr) = ast::Expr::cast(child) {
282 if expr.syntax().text().contains('\n') {
283 return None;
284 }
285 if mem::replace(&mut res, Some(expr)).is_some() {
286 return None;
287 }
288 } else {
289 match child.kind() {
290 WHITESPACE | L_CURLY | R_CURLY => (),
291 _ => return None,
292 }
293 }
294 }
295 res
296}
297
298fn join_single_use_tree(edit: &mut TextEditBuilder, node: &SyntaxNode) -> Option<()> {
299 let use_tree_list = ast::UseTreeList::cast(node.parent()?)?;
300 let (tree,) = use_tree_list.use_trees().collect_tuple()?;
301 edit.replace(
302 use_tree_list.syntax().range(),
303 tree.syntax().text().to_string(),
304 );
305 Some(())
306}
307
308fn compute_ws(left: &SyntaxNode, right: &SyntaxNode) -> &'static str {
309 match left.kind() {
310 L_PAREN | L_BRACK => return "",
311 L_CURLY => {
312 if let USE_TREE = right.kind() {
313 return "";
314 }
315 }
316 _ => (),
317 }
318 match right.kind() {
319 R_PAREN | R_BRACK => return "",
320 R_CURLY => {
321 if let USE_TREE = left.kind() {
322 return "";
323 }
324 }
325 DOT => return "",
326 _ => (),
327 }
328 " "
329}
330
331#[cfg(test)] 137#[cfg(test)]
332mod tests { 138mod tests {
333 use super::*; 139 use crate::test_utils::{add_cursor, assert_eq_text, extract_offset};
334 use crate::test_utils::{
335 add_cursor, assert_eq_text, check_action, extract_offset, extract_range,
336};
337
338 fn check_join_lines(before: &str, after: &str) {
339 check_action(before, after, |file, offset| {
340 let range = TextRange::offset_len(offset, 0.into());
341 let res = join_lines(file, range);
342 Some(res)
343 })
344 }
345 140
346 #[test] 141 use super::*;
347 fn test_join_lines_comma() {
348 check_join_lines(
349 r"
350fn foo() {
351 <|>foo(1,
352 )
353}
354",
355 r"
356fn foo() {
357 <|>foo(1)
358}
359",
360 );
361 }
362
363 #[test]
364 fn test_join_lines_lambda_block() {
365 check_join_lines(
366 r"
367pub fn reparse(&self, edit: &AtomTextEdit) -> File {
368 <|>self.incremental_reparse(edit).unwrap_or_else(|| {
369 self.full_reparse(edit)
370 })
371}
372",
373 r"
374pub fn reparse(&self, edit: &AtomTextEdit) -> File {
375 <|>self.incremental_reparse(edit).unwrap_or_else(|| self.full_reparse(edit))
376}
377",
378 );
379 }
380
381 #[test]
382 fn test_join_lines_block() {
383 check_join_lines(
384 r"
385fn foo() {
386 foo(<|>{
387 92
388 })
389}",
390 r"
391fn foo() {
392 foo(<|>92)
393}",
394 );
395 }
396
397 #[test]
398 fn test_join_lines_use_items_left() {
399 // No space after the '{'
400 check_join_lines(
401 r"
402<|>use ra_syntax::{
403 TextUnit, TextRange,
404};",
405 r"
406<|>use ra_syntax::{TextUnit, TextRange,
407};",
408 );
409 }
410
411 #[test]
412 fn test_join_lines_use_items_right() {
413 // No space after the '}'
414 check_join_lines(
415 r"
416use ra_syntax::{
417<|> TextUnit, TextRange
418};",
419 r"
420use ra_syntax::{
421<|> TextUnit, TextRange};",
422 );
423 }
424
425 #[test]
426 fn test_join_lines_use_items_right_comma() {
427 // No space after the '}'
428 check_join_lines(
429 r"
430use ra_syntax::{
431<|> TextUnit, TextRange,
432};",
433 r"
434use ra_syntax::{
435<|> TextUnit, TextRange};",
436 );
437 }
438
439 #[test]
440 fn test_join_lines_use_tree() {
441 check_join_lines(
442 r"
443use ra_syntax::{
444 algo::<|>{
445 find_leaf_at_offset,
446 },
447 ast,
448};",
449 r"
450use ra_syntax::{
451 algo::<|>find_leaf_at_offset,
452 ast,
453};",
454 );
455 }
456
457 #[test]
458 fn test_join_lines_normal_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_doc_comments() {
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_mod_comments() {
493 check_join_lines(
494 r"
495fn foo() {
496 //! Hello<|>
497 //! world!
498}
499",
500 r"
501fn foo() {
502 //! Hello<|> world!
503}
504",
505 );
506 }
507
508 #[test]
509 fn test_join_lines_multiline_comments_1() {
510 check_join_lines(
511 r"
512fn foo() {
513 // Hello<|>
514 /* world! */
515}
516",
517 r"
518fn foo() {
519 // Hello<|> world! */
520}
521",
522 );
523 }
524
525 #[test]
526 fn test_join_lines_multiline_comments_2() {
527 check_join_lines(
528 r"
529fn foo() {
530 // The<|>
531 /* quick
532 brown
533 fox! */
534}
535",
536 r"
537fn foo() {
538 // The<|> quick
539 brown
540 fox! */
541}
542",
543 );
544 }
545
546 fn check_join_lines_sel(before: &str, after: &str) {
547 let (sel, before) = extract_range(before);
548 let file = SourceFile::parse(&before);
549 let result = join_lines(&file, sel);
550 let actual = result.edit.apply(&before);
551 assert_eq_text!(after, &actual);
552 }
553
554 #[test]
555 fn test_join_lines_selection_fn_args() {
556 check_join_lines_sel(
557 r"
558fn foo() {
559 <|>foo(1,
560 2,
561 3,
562 <|>)
563}
564 ",
565 r"
566fn foo() {
567 foo(1, 2, 3)
568}
569 ",
570 );
571 }
572
573 #[test]
574 fn test_join_lines_selection_struct() {
575 check_join_lines_sel(
576 r"
577struct Foo <|>{
578 f: u32,
579}<|>
580 ",
581 r"
582struct Foo { f: u32 }
583 ",
584 );
585 }
586
587 #[test]
588 fn test_join_lines_selection_dot_chain() {
589 check_join_lines_sel(
590 r"
591fn foo() {
592 join(<|>type_params.type_params()
593 .filter_map(|it| it.name())
594 .map(|it| it.text())<|>)
595}",
596 r"
597fn foo() {
598 join(type_params.type_params().filter_map(|it| it.name()).map(|it| it.text()))
599}",
600 );
601 }
602
603 #[test]
604 fn test_join_lines_selection_lambda_block_body() {
605 check_join_lines_sel(
606 r"
607pub fn handle_find_matching_brace() {
608 params.offsets
609 .map(|offset| <|>{
610 world.analysis().matching_brace(&file, offset).unwrap_or(offset)
611 }<|>)
612 .collect();
613}",
614 r"
615pub fn handle_find_matching_brace() {
616 params.offsets
617 .map(|offset| world.analysis().matching_brace(&file, offset).unwrap_or(offset))
618 .collect();
619}",
620 );
621 }
622 142
623 #[test] 143 #[test]
624 fn test_on_eq_typed() { 144 fn test_on_eq_typed() {