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