diff options
author | bors[bot] <bors[bot]@users.noreply.github.com> | 2018-10-11 16:43:34 +0100 |
---|---|---|
committer | bors[bot] <bors[bot]@users.noreply.github.com> | 2018-10-11 16:43:34 +0100 |
commit | 77e9bf9b5fdcd4262e2d9badb42b912f5728d90b (patch) | |
tree | 3f318ed358723e229dc6101404d7b607bbeabd51 | |
parent | 9b155c89764b8413df6b32edfde94fce1d9c15ec (diff) | |
parent | 6fe77db41307da8ead8a0b0355488221b61c0349 (diff) |
Merge #116
116: Collapse comments upon join r=matklad a=aochagavia
Todo:
- [x] Write tests
- [x] Resolve fixmes
- [x] Implement `comment_start_length` using the parser
I left a bunch of questions as fixmes. Can someone take a look at them? Also, I would love to use the parser to calculate the length of the leading characters in a comment (`//`, `///`, `//!`, `/*`), so any hints are greatly appreciated.
Co-authored-by: Adolfo OchagavĂa <[email protected]>
Co-authored-by: Adolfo OchagavĂa <[email protected]>
-rw-r--r-- | crates/ra_editor/src/typing.rs | 195 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/generated.rs | 18 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/mod.rs | 43 | ||||
-rw-r--r-- | crates/ra_syntax/src/grammar.ron | 1 |
4 files changed, 199 insertions, 58 deletions
diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_editor/src/typing.rs index 3384389d1..1dc658f9b 100644 --- a/crates/ra_editor/src/typing.rs +++ b/crates/ra_editor/src/typing.rs | |||
@@ -30,6 +30,7 @@ pub fn join_lines(file: &File, range: TextRange) -> LocalEdit { | |||
30 | } else { | 30 | } else { |
31 | range | 31 | range |
32 | }; | 32 | }; |
33 | |||
33 | let node = find_covering_node(file.syntax(), range); | 34 | let node = find_covering_node(file.syntax(), range); |
34 | let mut edit = EditBuilder::new(); | 35 | let mut edit = EditBuilder::new(); |
35 | for node in node.descendants() { | 36 | for node in node.descendants() { |
@@ -57,14 +58,19 @@ pub fn join_lines(file: &File, range: TextRange) -> LocalEdit { | |||
57 | } | 58 | } |
58 | 59 | ||
59 | pub fn on_enter(file: &File, offset: TextUnit) -> Option<LocalEdit> { | 60 | pub fn on_enter(file: &File, offset: TextUnit) -> Option<LocalEdit> { |
60 | let comment = find_leaf_at_offset(file.syntax(), offset).left_biased().filter(|it| it.kind() == COMMENT)?; | 61 | let comment = find_leaf_at_offset(file.syntax(), offset).left_biased().and_then(|it| ast::Comment::cast(it))?; |
61 | let prefix = comment_preffix(comment)?; | 62 | |
62 | if offset < comment.range().start() + TextUnit::of_str(prefix) { | 63 | if let ast::CommentFlavor::Multiline = comment.flavor() { |
64 | return None; | ||
65 | } | ||
66 | |||
67 | let prefix = comment.prefix(); | ||
68 | if offset < comment.syntax().range().start() + TextUnit::of_str(prefix) + TextUnit::from(1) { | ||
63 | return None; | 69 | return None; |
64 | } | 70 | } |
65 | 71 | ||
66 | let indent = node_indent(file, comment)?; | 72 | let indent = node_indent(file, comment.syntax())?; |
67 | let inserted = format!("\n{}{}", indent, prefix); | 73 | let inserted = format!("\n{}{} ", indent, prefix); |
68 | let cursor_position = offset + TextUnit::of_str(&inserted); | 74 | let cursor_position = offset + TextUnit::of_str(&inserted); |
69 | let mut edit = EditBuilder::new(); | 75 | let mut edit = EditBuilder::new(); |
70 | edit.insert(offset, inserted); | 76 | edit.insert(offset, inserted); |
@@ -74,20 +80,6 @@ pub fn on_enter(file: &File, offset: TextUnit) -> Option<LocalEdit> { | |||
74 | }) | 80 | }) |
75 | } | 81 | } |
76 | 82 | ||
77 | fn comment_preffix(comment: SyntaxNodeRef) -> Option<&'static str> { | ||
78 | let text = comment.leaf_text().unwrap(); | ||
79 | let res = if text.starts_with("///") { | ||
80 | "/// " | ||
81 | } else if text.starts_with("//!") { | ||
82 | "//! " | ||
83 | } else if text.starts_with("//") { | ||
84 | "// " | ||
85 | } else { | ||
86 | return None; | ||
87 | }; | ||
88 | Some(res) | ||
89 | } | ||
90 | |||
91 | fn node_indent<'a>(file: &'a File, node: SyntaxNodeRef) -> Option<&'a str> { | 83 | fn node_indent<'a>(file: &'a File, node: SyntaxNodeRef) -> Option<&'a str> { |
92 | let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) { | 84 | let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) { |
93 | LeafAtOffset::Between(l, r) => { | 85 | LeafAtOffset::Between(l, r) => { |
@@ -139,41 +131,60 @@ fn remove_newline( | |||
139 | node_text: &str, | 131 | node_text: &str, |
140 | offset: TextUnit, | 132 | offset: TextUnit, |
141 | ) { | 133 | ) { |
142 | if node.kind() == WHITESPACE && node_text.bytes().filter(|&b| b == b'\n').count() == 1 { | 134 | if node.kind() != WHITESPACE || node_text.bytes().filter(|&b| b == b'\n').count() != 1 { |
143 | if join_single_expr_block(edit, node).is_some() { | 135 | // The node is either the first or the last in the file |
144 | return | 136 | let suff = &node_text[TextRange::from_to( |
145 | } | 137 | offset - node.range().start() + TextUnit::of_char('\n'), |
146 | match (node.prev_sibling(), node.next_sibling()) { | 138 | TextUnit::of_str(node_text), |
147 | (Some(prev), Some(next)) => { | 139 | )]; |
148 | let range = TextRange::from_to(prev.range().start(), node.range().end()); | 140 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); |
149 | if is_trailing_comma(prev.kind(), next.kind()) { | 141 | |
150 | edit.delete(range); | 142 | edit.replace( |
151 | } else if no_space_required(prev.kind(), next.kind()) { | 143 | TextRange::offset_len(offset, ((spaces + 1) as u32).into()), |
152 | edit.delete(node.range()); | 144 | " ".to_string(), |
153 | } else if prev.kind() == COMMA && next.kind() == R_CURLY { | 145 | ); |
154 | edit.replace(range, " ".to_string()); | 146 | return; |
155 | } else { | ||
156 | edit.replace( | ||
157 | node.range(), | ||
158 | compute_ws(prev, next).to_string(), | ||
159 | ); | ||
160 | } | ||
161 | return; | ||
162 | } | ||
163 | _ => (), | ||
164 | } | ||
165 | } | 147 | } |
166 | 148 | ||
167 | let suff = &node_text[TextRange::from_to( | 149 | // Special case that turns something like: |
168 | offset - node.range().start() + TextUnit::of_char('\n'), | 150 | // |
169 | TextUnit::of_str(node_text), | 151 | // ``` |
170 | )]; | 152 | // my_function({<|> |
171 | let spaces = suff.bytes().take_while(|&b| b == b' ').count(); | 153 | // <some-expr> |
154 | // }) | ||
155 | // ``` | ||
156 | // | ||
157 | // into `my_function(<some-expr>)` | ||
158 | if join_single_expr_block(edit, node).is_some() { | ||
159 | return | ||
160 | } | ||
172 | 161 | ||
173 | edit.replace( | 162 | // The node is between two other nodes |
174 | TextRange::offset_len(offset, ((spaces + 1) as u32).into()), | 163 | let prev = node.prev_sibling().unwrap(); |
175 | " ".to_string(), | 164 | let next = node.next_sibling().unwrap(); |
176 | ); | 165 | if is_trailing_comma(prev.kind(), next.kind()) { |
166 | // Removes: trailing comma, newline (incl. surrounding whitespace) | ||
167 | edit.delete(TextRange::from_to(prev.range().start(), node.range().end())); | ||
168 | } else if prev.kind() == COMMA && next.kind() == R_CURLY { | ||
169 | // Removes: comma, newline (incl. surrounding whitespace) | ||
170 | // Adds: a single whitespace | ||
171 | edit.replace( | ||
172 | TextRange::from_to(prev.range().start(), node.range().end()), | ||
173 | " ".to_string() | ||
174 | ); | ||
175 | } else if let (Some(_), Some(next)) = (ast::Comment::cast(prev), ast::Comment::cast(next)) { | ||
176 | // Removes: newline (incl. surrounding whitespace), start of the next comment | ||
177 | edit.delete(TextRange::from_to( | ||
178 | node.range().start(), | ||
179 | next.syntax().range().start() + TextUnit::of_str(next.prefix()) | ||
180 | )); | ||
181 | } else { | ||
182 | // Remove newline but add a computed amount of whitespace characters | ||
183 | edit.replace( | ||
184 | node.range(), | ||
185 | compute_ws(prev, next).to_string(), | ||
186 | ); | ||
187 | } | ||
177 | } | 188 | } |
178 | 189 | ||
179 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | 190 | fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { |
@@ -183,13 +194,6 @@ fn is_trailing_comma(left: SyntaxKind, right: SyntaxKind) -> bool { | |||
183 | } | 194 | } |
184 | } | 195 | } |
185 | 196 | ||
186 | fn no_space_required(left: SyntaxKind, right: SyntaxKind) -> bool { | ||
187 | match (left, right) { | ||
188 | (_, DOT) => true, | ||
189 | _ => false | ||
190 | } | ||
191 | } | ||
192 | |||
193 | fn join_single_expr_block( | 197 | fn join_single_expr_block( |
194 | edit: &mut EditBuilder, | 198 | edit: &mut EditBuilder, |
195 | node: SyntaxNodeRef, | 199 | node: SyntaxNodeRef, |
@@ -231,6 +235,7 @@ fn compute_ws(left: SyntaxNodeRef, right: SyntaxNodeRef) -> &'static str { | |||
231 | } | 235 | } |
232 | match right.kind() { | 236 | match right.kind() { |
233 | R_PAREN | R_BRACK => return "", | 237 | R_PAREN | R_BRACK => return "", |
238 | DOT => return "", | ||
234 | _ => (), | 239 | _ => (), |
235 | } | 240 | } |
236 | " " | 241 | " " |
@@ -291,6 +296,80 @@ fn foo() { | |||
291 | }"); | 296 | }"); |
292 | } | 297 | } |
293 | 298 | ||
299 | #[test] | ||
300 | fn test_join_lines_normal_comments() { | ||
301 | check_join_lines(r" | ||
302 | fn foo() { | ||
303 | // Hello<|> | ||
304 | // world! | ||
305 | } | ||
306 | ", r" | ||
307 | fn foo() { | ||
308 | // Hello<|> world! | ||
309 | } | ||
310 | "); | ||
311 | } | ||
312 | |||
313 | #[test] | ||
314 | fn test_join_lines_doc_comments() { | ||
315 | check_join_lines(r" | ||
316 | fn foo() { | ||
317 | /// Hello<|> | ||
318 | /// world! | ||
319 | } | ||
320 | ", r" | ||
321 | fn foo() { | ||
322 | /// Hello<|> world! | ||
323 | } | ||
324 | "); | ||
325 | } | ||
326 | |||
327 | #[test] | ||
328 | fn test_join_lines_mod_comments() { | ||
329 | check_join_lines(r" | ||
330 | fn foo() { | ||
331 | //! Hello<|> | ||
332 | //! world! | ||
333 | } | ||
334 | ", r" | ||
335 | fn foo() { | ||
336 | //! Hello<|> world! | ||
337 | } | ||
338 | "); | ||
339 | } | ||
340 | |||
341 | #[test] | ||
342 | fn test_join_lines_multiline_comments_1() { | ||
343 | check_join_lines(r" | ||
344 | fn foo() { | ||
345 | // Hello<|> | ||
346 | /* world! */ | ||
347 | } | ||
348 | ", r" | ||
349 | fn foo() { | ||
350 | // Hello<|> world! */ | ||
351 | } | ||
352 | "); | ||
353 | } | ||
354 | |||
355 | #[test] | ||
356 | fn test_join_lines_multiline_comments_2() { | ||
357 | check_join_lines(r" | ||
358 | fn foo() { | ||
359 | // The<|> | ||
360 | /* quick | ||
361 | brown | ||
362 | fox! */ | ||
363 | } | ||
364 | ", r" | ||
365 | fn foo() { | ||
366 | // The<|> quick | ||
367 | brown | ||
368 | fox! */ | ||
369 | } | ||
370 | "); | ||
371 | } | ||
372 | |||
294 | fn check_join_lines_sel(before: &str, after: &str) { | 373 | fn check_join_lines_sel(before: &str, after: &str) { |
295 | let (sel, before) = extract_range(before); | 374 | let (sel, before) = extract_range(before); |
296 | let file = File::parse(&before); | 375 | let file = File::parse(&before); |
diff --git a/crates/ra_syntax/src/ast/generated.rs b/crates/ra_syntax/src/ast/generated.rs index f27093291..ef7b5b1a1 100644 --- a/crates/ra_syntax/src/ast/generated.rs +++ b/crates/ra_syntax/src/ast/generated.rs | |||
@@ -231,6 +231,24 @@ impl<'a> AstNode<'a> for CastExpr<'a> { | |||
231 | 231 | ||
232 | impl<'a> CastExpr<'a> {} | 232 | impl<'a> CastExpr<'a> {} |
233 | 233 | ||
234 | // Comment | ||
235 | #[derive(Debug, Clone, Copy)] | ||
236 | pub struct Comment<'a> { | ||
237 | syntax: SyntaxNodeRef<'a>, | ||
238 | } | ||
239 | |||
240 | impl<'a> AstNode<'a> for Comment<'a> { | ||
241 | fn cast(syntax: SyntaxNodeRef<'a>) -> Option<Self> { | ||
242 | match syntax.kind() { | ||
243 | COMMENT => Some(Comment { syntax }), | ||
244 | _ => None, | ||
245 | } | ||
246 | } | ||
247 | fn syntax(self) -> SyntaxNodeRef<'a> { self.syntax } | ||
248 | } | ||
249 | |||
250 | impl<'a> Comment<'a> {} | ||
251 | |||
234 | // Condition | 252 | // Condition |
235 | #[derive(Debug, Clone, Copy)] | 253 | #[derive(Debug, Clone, Copy)] |
236 | pub struct Condition<'a> { | 254 | pub struct Condition<'a> { |
diff --git a/crates/ra_syntax/src/ast/mod.rs b/crates/ra_syntax/src/ast/mod.rs index c1570b868..10dac72e5 100644 --- a/crates/ra_syntax/src/ast/mod.rs +++ b/crates/ra_syntax/src/ast/mod.rs | |||
@@ -99,6 +99,49 @@ impl<'a> Lifetime<'a> { | |||
99 | } | 99 | } |
100 | } | 100 | } |
101 | 101 | ||
102 | impl<'a> Comment<'a> { | ||
103 | pub fn text(&self) -> SmolStr { | ||
104 | self.syntax().leaf_text().unwrap().clone() | ||
105 | } | ||
106 | |||
107 | pub fn flavor(&self) -> CommentFlavor { | ||
108 | let text = self.text(); | ||
109 | if text.starts_with("///") { | ||
110 | CommentFlavor::Doc | ||
111 | } else if text.starts_with("//!") { | ||
112 | CommentFlavor::ModuleDoc | ||
113 | } else if text.starts_with("//") { | ||
114 | CommentFlavor::Line | ||
115 | } else { | ||
116 | CommentFlavor::Multiline | ||
117 | } | ||
118 | } | ||
119 | |||
120 | pub fn prefix(&self) -> &'static str { | ||
121 | self.flavor().prefix() | ||
122 | } | ||
123 | } | ||
124 | |||
125 | #[derive(Debug)] | ||
126 | pub enum CommentFlavor { | ||
127 | Line, | ||
128 | Doc, | ||
129 | ModuleDoc, | ||
130 | Multiline | ||
131 | } | ||
132 | |||
133 | impl CommentFlavor { | ||
134 | pub fn prefix(&self) -> &'static str { | ||
135 | use self::CommentFlavor::*; | ||
136 | match *self { | ||
137 | Line => "//", | ||
138 | Doc => "///", | ||
139 | ModuleDoc => "//!", | ||
140 | Multiline => "/*" | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | |||
102 | impl<'a> Name<'a> { | 145 | impl<'a> Name<'a> { |
103 | pub fn text(&self) -> SmolStr { | 146 | pub fn text(&self) -> SmolStr { |
104 | let ident = self.syntax().first_child() | 147 | let ident = self.syntax().first_child() |
diff --git a/crates/ra_syntax/src/grammar.ron b/crates/ra_syntax/src/grammar.ron index 4b990fd8d..9da0c2c13 100644 --- a/crates/ra_syntax/src/grammar.ron +++ b/crates/ra_syntax/src/grammar.ron | |||
@@ -537,5 +537,6 @@ Grammar( | |||
537 | "PathSegment": ( | 537 | "PathSegment": ( |
538 | options: [ "NameRef" ] | 538 | options: [ "NameRef" ] |
539 | ), | 539 | ), |
540 | "Comment": (), | ||
540 | }, | 541 | }, |
541 | ) | 542 | ) |