diff options
author | John Renner <[email protected]> | 2021-02-24 19:25:10 +0000 |
---|---|---|
committer | John Renner <[email protected]> | 2021-02-25 01:13:00 +0000 |
commit | 9eecba4dbf8e18ddb9f7f906af468e35a11d28a4 (patch) | |
tree | 622bbdcad744b43cbc162bf280ca9daf71a9d87b /crates | |
parent | a307e4f31f3a66797628cee0645a38216398d9bd (diff) |
Implement line<->block comment assist
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ide_assists/src/handlers/convert_comment_block.rs | 416 | ||||
-rw-r--r-- | crates/ide_assists/src/lib.rs | 2 | ||||
-rw-r--r-- | crates/syntax/src/ast/edit.rs | 11 | ||||
-rw-r--r-- | crates/syntax/src/ast/token_ext.rs | 3 |
4 files changed, 427 insertions, 5 deletions
diff --git a/crates/ide_assists/src/handlers/convert_comment_block.rs b/crates/ide_assists/src/handlers/convert_comment_block.rs new file mode 100644 index 000000000..7cf5f3643 --- /dev/null +++ b/crates/ide_assists/src/handlers/convert_comment_block.rs | |||
@@ -0,0 +1,416 @@ | |||
1 | use ast::{Comment, CommentShape}; | ||
2 | use itertools::Itertools; | ||
3 | use std::convert::identity; | ||
4 | use syntax::{ | ||
5 | ast::{self, edit::IndentLevel, CommentKind, Whitespace}, | ||
6 | AstToken, Direction, SyntaxElement, TextRange, | ||
7 | }; | ||
8 | |||
9 | use crate::{AssistContext, AssistId, AssistKind, Assists}; | ||
10 | |||
11 | /// Assist: line_to_block | ||
12 | /// | ||
13 | /// Converts comments between block and single-line form | ||
14 | /// | ||
15 | /// ``` | ||
16 | /// // Multi-line | ||
17 | /// // comment | ||
18 | /// ``` | ||
19 | /// -> | ||
20 | /// ``` | ||
21 | /// /** | ||
22 | /// Multi-line | ||
23 | /// comment | ||
24 | /// */ | ||
25 | /// ``` | ||
26 | pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { | ||
27 | if let Some(comment) = ctx.find_token_at_offset::<ast::Comment>() { | ||
28 | // Only allow comments which are alone on their line | ||
29 | if let Some(prev) = comment.syntax().prev_token() { | ||
30 | if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() { | ||
31 | return None; | ||
32 | } | ||
33 | } | ||
34 | |||
35 | return match comment.kind().shape { | ||
36 | ast::CommentShape::Block => block_to_line(acc, comment), | ||
37 | ast::CommentShape::Line => line_to_block(acc, comment), | ||
38 | }; | ||
39 | } | ||
40 | |||
41 | return None; | ||
42 | } | ||
43 | |||
44 | fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> { | ||
45 | let indentation = IndentLevel::from_token(comment.syntax()); | ||
46 | let line_prefix = | ||
47 | comment_kind_prefix(CommentKind { shape: CommentShape::Line, ..comment.kind() }); | ||
48 | |||
49 | let text = comment.text(); | ||
50 | let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim(); | ||
51 | |||
52 | let lines = text.lines().peekable(); | ||
53 | |||
54 | let indent_spaces = indentation.to_string(); | ||
55 | let output = lines | ||
56 | .map(|l| l.trim_start_matches(&indent_spaces)) | ||
57 | .map(|l| { | ||
58 | // Don't introduce trailing whitespace | ||
59 | if l.is_empty() { | ||
60 | line_prefix.to_string() | ||
61 | } else { | ||
62 | format!("{} {}", line_prefix, l.trim_start_matches(&indent_spaces)) | ||
63 | } | ||
64 | }) | ||
65 | .join(&format!("\n{}", indent_spaces)); | ||
66 | |||
67 | let target = comment.syntax().text_range(); | ||
68 | acc.add( | ||
69 | AssistId("block_to_line", AssistKind::RefactorRewrite), | ||
70 | "Replace block comment with line comments", | ||
71 | target, | ||
72 | |edit| edit.replace(target, output), | ||
73 | ) | ||
74 | } | ||
75 | |||
76 | fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> { | ||
77 | // Find all the comments we'll be collapsing into a block | ||
78 | let comments = relevant_line_comments(&comment); | ||
79 | |||
80 | // Establish the target of our edit based on the comments we found | ||
81 | let target = TextRange::new( | ||
82 | comments[0].syntax().text_range().start(), | ||
83 | comments.last().unwrap().syntax().text_range().end(), | ||
84 | ); | ||
85 | |||
86 | // We pick a single indentation level for the whole block comment based on the | ||
87 | // comment where the assist was invoked. This will be prepended to the | ||
88 | // contents of each line comment when they're put into the block comment. | ||
89 | let indentation = IndentLevel::from_token(&comment.syntax()); | ||
90 | |||
91 | let block_comment_body = | ||
92 | comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n"); | ||
93 | |||
94 | let block_prefix = | ||
95 | comment_kind_prefix(CommentKind { shape: CommentShape::Block, ..comment.kind() }); | ||
96 | |||
97 | let output = format!("{}\n{}\n{}*/", block_prefix, block_comment_body, indentation.to_string()); | ||
98 | |||
99 | acc.add( | ||
100 | AssistId("line_to_block", AssistKind::RefactorRewrite), | ||
101 | "Replace line comments with a single block comment", | ||
102 | target, | ||
103 | |edit| edit.replace(target, output), | ||
104 | ) | ||
105 | } | ||
106 | |||
107 | /// The line -> block assist can be invoked from anywhere within a sequence of line comments. | ||
108 | /// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will | ||
109 | /// be joined. | ||
110 | fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> { | ||
111 | // The prefix identifies the kind of comment we're dealing with | ||
112 | let prefix = comment.prefix(); | ||
113 | let same_prefix = |c: &ast::Comment| c.prefix() == prefix; | ||
114 | |||
115 | // These tokens are allowed to exist between comments | ||
116 | let skippable = |not: &SyntaxElement| { | ||
117 | not.clone() | ||
118 | .into_token() | ||
119 | .and_then(Whitespace::cast) | ||
120 | .map(|w| !w.spans_multiple_lines()) | ||
121 | .unwrap_or(false) | ||
122 | }; | ||
123 | |||
124 | // Find all preceding comments (in reverse order) that have the same prefix | ||
125 | let prev_comments = comment | ||
126 | .syntax() | ||
127 | .siblings_with_tokens(Direction::Prev) | ||
128 | .filter(|s| !skippable(s)) | ||
129 | .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix)) | ||
130 | .take_while(|opt_com| opt_com.is_some()) | ||
131 | .filter_map(identity) | ||
132 | .skip(1); // skip the first element so we don't duplicate it in next_comments | ||
133 | |||
134 | let next_comments = comment | ||
135 | .syntax() | ||
136 | .siblings_with_tokens(Direction::Next) | ||
137 | .filter(|s| !skippable(s)) | ||
138 | .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix)) | ||
139 | .take_while(|opt_com| opt_com.is_some()) | ||
140 | .filter_map(identity); | ||
141 | |||
142 | let mut comments: Vec<_> = prev_comments.collect(); | ||
143 | comments.reverse(); | ||
144 | comments.extend(next_comments); | ||
145 | comments | ||
146 | } | ||
147 | |||
148 | // Line comments usually begin with a single space character following the prefix as seen here: | ||
149 | //^ | ||
150 | // But comments can also include indented text: | ||
151 | // > Hello there | ||
152 | // | ||
153 | // We handle this by stripping *AT MOST* one space character from the start of the line | ||
154 | // This has its own problems because it can cause alignment issues: | ||
155 | // | ||
156 | // /* | ||
157 | // a ----> a | ||
158 | //b ----> b | ||
159 | // */ | ||
160 | // | ||
161 | // But since such comments aren't idiomatic we're okay with this. | ||
162 | fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String { | ||
163 | let contents = trim_one(comm.text().strip_prefix(comm.prefix()).unwrap()).to_owned(); | ||
164 | |||
165 | // Don't add the indentation if the line is empty | ||
166 | if contents.is_empty() { | ||
167 | contents | ||
168 | } else { | ||
169 | indentation.to_string() + &contents | ||
170 | } | ||
171 | } | ||
172 | |||
173 | fn trim_one(text: &str) -> &str { | ||
174 | if text.starts_with(' ') { | ||
175 | &text[1..] | ||
176 | } else { | ||
177 | text | ||
178 | } | ||
179 | } | ||
180 | |||
181 | fn comment_kind_prefix(ck: ast::CommentKind) -> &'static str { | ||
182 | use ast::CommentPlacement::{Inner, Outer}; | ||
183 | use ast::CommentShape::{Block, Line}; | ||
184 | match (ck.shape, ck.doc) { | ||
185 | (Line, Some(Inner)) => "//!", | ||
186 | (Line, Some(Outer)) => "///", | ||
187 | (Line, None) => "//", | ||
188 | (Block, Some(Inner)) => "/*!", | ||
189 | (Block, Some(Outer)) => "/**", | ||
190 | (Block, None) => "/*", | ||
191 | } | ||
192 | } | ||
193 | |||
194 | #[cfg(test)] | ||
195 | mod tests { | ||
196 | use crate::tests::{check_assist, check_assist_not_applicable}; | ||
197 | |||
198 | use super::*; | ||
199 | |||
200 | #[test] | ||
201 | fn single_line_to_block() { | ||
202 | check_assist( | ||
203 | convert_comment_block, | ||
204 | r#" | ||
205 | // line$0 comment | ||
206 | fn main() { | ||
207 | foo(); | ||
208 | } | ||
209 | "#, | ||
210 | r#" | ||
211 | /* | ||
212 | line comment | ||
213 | */ | ||
214 | fn main() { | ||
215 | foo(); | ||
216 | } | ||
217 | "#, | ||
218 | ); | ||
219 | } | ||
220 | |||
221 | #[test] | ||
222 | fn single_line_to_block_indented() { | ||
223 | check_assist( | ||
224 | convert_comment_block, | ||
225 | r#" | ||
226 | fn main() { | ||
227 | // line$0 comment | ||
228 | foo(); | ||
229 | } | ||
230 | "#, | ||
231 | r#" | ||
232 | fn main() { | ||
233 | /* | ||
234 | line comment | ||
235 | */ | ||
236 | foo(); | ||
237 | } | ||
238 | "#, | ||
239 | ); | ||
240 | } | ||
241 | |||
242 | #[test] | ||
243 | fn multiline_to_block() { | ||
244 | check_assist( | ||
245 | convert_comment_block, | ||
246 | r#" | ||
247 | fn main() { | ||
248 | // above | ||
249 | // line$0 comment | ||
250 | // | ||
251 | // below | ||
252 | foo(); | ||
253 | } | ||
254 | "#, | ||
255 | r#" | ||
256 | fn main() { | ||
257 | /* | ||
258 | above | ||
259 | line comment | ||
260 | |||
261 | below | ||
262 | */ | ||
263 | foo(); | ||
264 | } | ||
265 | "#, | ||
266 | ); | ||
267 | } | ||
268 | |||
269 | #[test] | ||
270 | fn end_of_line_to_block() { | ||
271 | check_assist_not_applicable( | ||
272 | convert_comment_block, | ||
273 | r#" | ||
274 | fn main() { | ||
275 | foo(); // end-of-line$0 comment | ||
276 | } | ||
277 | "#, | ||
278 | ); | ||
279 | } | ||
280 | |||
281 | #[test] | ||
282 | fn single_line_different_kinds() { | ||
283 | check_assist( | ||
284 | convert_comment_block, | ||
285 | r#" | ||
286 | fn main() { | ||
287 | /// different prefix | ||
288 | // line$0 comment | ||
289 | // below | ||
290 | foo(); | ||
291 | } | ||
292 | "#, | ||
293 | r#" | ||
294 | fn main() { | ||
295 | /// different prefix | ||
296 | /* | ||
297 | line comment | ||
298 | below | ||
299 | */ | ||
300 | foo(); | ||
301 | } | ||
302 | "#, | ||
303 | ); | ||
304 | } | ||
305 | |||
306 | #[test] | ||
307 | fn single_line_separate_chunks() { | ||
308 | check_assist( | ||
309 | convert_comment_block, | ||
310 | r#" | ||
311 | fn main() { | ||
312 | // different chunk | ||
313 | |||
314 | // line$0 comment | ||
315 | // below | ||
316 | foo(); | ||
317 | } | ||
318 | "#, | ||
319 | r#" | ||
320 | fn main() { | ||
321 | // different chunk | ||
322 | |||
323 | /* | ||
324 | line comment | ||
325 | below | ||
326 | */ | ||
327 | foo(); | ||
328 | } | ||
329 | "#, | ||
330 | ); | ||
331 | } | ||
332 | |||
333 | #[test] | ||
334 | fn doc_block_comment_to_lines() { | ||
335 | check_assist( | ||
336 | convert_comment_block, | ||
337 | r#" | ||
338 | /** | ||
339 | hi$0 there | ||
340 | */ | ||
341 | "#, | ||
342 | r#" | ||
343 | /// hi there | ||
344 | "#, | ||
345 | ); | ||
346 | } | ||
347 | |||
348 | #[test] | ||
349 | fn block_comment_to_lines() { | ||
350 | check_assist( | ||
351 | convert_comment_block, | ||
352 | r#" | ||
353 | /* | ||
354 | hi$0 there | ||
355 | */ | ||
356 | "#, | ||
357 | r#" | ||
358 | // hi there | ||
359 | "#, | ||
360 | ); | ||
361 | } | ||
362 | |||
363 | #[test] | ||
364 | fn inner_doc_block_to_lines() { | ||
365 | check_assist( | ||
366 | convert_comment_block, | ||
367 | r#" | ||
368 | /*! | ||
369 | hi$0 there | ||
370 | */ | ||
371 | "#, | ||
372 | r#" | ||
373 | //! hi there | ||
374 | "#, | ||
375 | ); | ||
376 | } | ||
377 | |||
378 | #[test] | ||
379 | fn block_to_lines_indent() { | ||
380 | check_assist( | ||
381 | convert_comment_block, | ||
382 | r#" | ||
383 | fn main() { | ||
384 | /*! | ||
385 | hi$0 there | ||
386 | |||
387 | ``` | ||
388 | code_sample | ||
389 | ``` | ||
390 | */ | ||
391 | } | ||
392 | "#, | ||
393 | r#" | ||
394 | fn main() { | ||
395 | //! hi there | ||
396 | //! | ||
397 | //! ``` | ||
398 | //! code_sample | ||
399 | //! ``` | ||
400 | } | ||
401 | "#, | ||
402 | ); | ||
403 | } | ||
404 | |||
405 | #[test] | ||
406 | fn end_of_line_block_to_line() { | ||
407 | check_assist_not_applicable( | ||
408 | convert_comment_block, | ||
409 | r#" | ||
410 | fn main() { | ||
411 | foo(); /* end-of-line$0 comment */ | ||
412 | } | ||
413 | "#, | ||
414 | ); | ||
415 | } | ||
416 | } | ||
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs index 7067cf8b6..b9799b9d5 100644 --- a/crates/ide_assists/src/lib.rs +++ b/crates/ide_assists/src/lib.rs | |||
@@ -115,6 +115,7 @@ mod handlers { | |||
115 | mod auto_import; | 115 | mod auto_import; |
116 | mod change_visibility; | 116 | mod change_visibility; |
117 | mod convert_integer_literal; | 117 | mod convert_integer_literal; |
118 | mod convert_comment_block; | ||
118 | mod early_return; | 119 | mod early_return; |
119 | mod expand_glob_import; | 120 | mod expand_glob_import; |
120 | mod extract_function; | 121 | mod extract_function; |
@@ -176,6 +177,7 @@ mod handlers { | |||
176 | auto_import::auto_import, | 177 | auto_import::auto_import, |
177 | change_visibility::change_visibility, | 178 | change_visibility::change_visibility, |
178 | convert_integer_literal::convert_integer_literal, | 179 | convert_integer_literal::convert_integer_literal, |
180 | convert_comment_block::convert_comment_block, | ||
179 | early_return::convert_to_guarded_return, | 181 | early_return::convert_to_guarded_return, |
180 | expand_glob_import::expand_glob_import, | 182 | expand_glob_import::expand_glob_import, |
181 | move_module_to_file::move_module_to_file, | 183 | move_module_to_file::move_module_to_file, |
diff --git a/crates/syntax/src/ast/edit.rs b/crates/syntax/src/ast/edit.rs index 824ebf41c..0b3b76d4a 100644 --- a/crates/syntax/src/ast/edit.rs +++ b/crates/syntax/src/ast/edit.rs | |||
@@ -595,11 +595,14 @@ impl ops::Add<u8> for IndentLevel { | |||
595 | 595 | ||
596 | impl IndentLevel { | 596 | impl IndentLevel { |
597 | pub fn from_node(node: &SyntaxNode) -> IndentLevel { | 597 | pub fn from_node(node: &SyntaxNode) -> IndentLevel { |
598 | let first_token = match node.first_token() { | 598 | match node.first_token() { |
599 | Some(it) => it, | 599 | Some(it) => Self::from_token(&it), |
600 | None => return IndentLevel(0), | 600 | None => return IndentLevel(0), |
601 | }; | 601 | } |
602 | for ws in prev_tokens(first_token).filter_map(ast::Whitespace::cast) { | 602 | } |
603 | |||
604 | pub fn from_token(token: &SyntaxToken) -> IndentLevel { | ||
605 | for ws in prev_tokens(token.clone()).filter_map(ast::Whitespace::cast) { | ||
603 | let text = ws.syntax().text(); | 606 | let text = ws.syntax().text(); |
604 | if let Some(pos) = text.rfind('\n') { | 607 | if let Some(pos) = text.rfind('\n') { |
605 | let level = text[pos + 1..].chars().count() / 4; | 608 | let level = text[pos + 1..].chars().count() / 4; |
diff --git a/crates/syntax/src/ast/token_ext.rs b/crates/syntax/src/ast/token_ext.rs index 044e3e5e8..977eb8181 100644 --- a/crates/syntax/src/ast/token_ext.rs +++ b/crates/syntax/src/ast/token_ext.rs | |||
@@ -85,8 +85,9 @@ pub enum CommentPlacement { | |||
85 | } | 85 | } |
86 | 86 | ||
87 | impl CommentKind { | 87 | impl CommentKind { |
88 | const BY_PREFIX: [(&'static str, CommentKind); 8] = [ | 88 | const BY_PREFIX: [(&'static str, CommentKind); 9] = [ |
89 | ("/**/", CommentKind { shape: CommentShape::Block, doc: None }), | 89 | ("/**/", CommentKind { shape: CommentShape::Block, doc: None }), |
90 | ("/***", CommentKind { shape: CommentShape::Block, doc: None }), | ||
90 | ("////", CommentKind { shape: CommentShape::Line, doc: None }), | 91 | ("////", CommentKind { shape: CommentShape::Line, doc: None }), |
91 | ("///", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Outer) }), | 92 | ("///", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Outer) }), |
92 | ("//!", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Inner) }), | 93 | ("//!", CommentKind { shape: CommentShape::Line, doc: Some(CommentPlacement::Inner) }), |