]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/convert_comment_block.rs
Rollup merge of #98391 - joboet:sgx_parker, r=m-ou-se
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / convert_comment_block.rs
1 use itertools::Itertools;
2 use syntax::{
3     ast::{self, edit::IndentLevel, Comment, CommentKind, CommentShape, Whitespace},
4     AstToken, Direction, SyntaxElement, TextRange,
5 };
6
7 use crate::{AssistContext, AssistId, AssistKind, Assists};
8
9 // Assist: line_to_block
10 //
11 // Converts comments between block and single-line form.
12 //
13 // ```
14 //    // Multi-line$0
15 //    // comment
16 // ```
17 // ->
18 // ```
19 //   /*
20 //   Multi-line
21 //   comment
22 //   */
23 // ```
24 pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
25     let comment = ctx.find_token_at_offset::<ast::Comment>()?;
26     // Only allow comments which are alone on their line
27     if let Some(prev) = comment.syntax().prev_token() {
28         if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
29             return None;
30         }
31     }
32
33     match comment.kind().shape {
34         ast::CommentShape::Block => block_to_line(acc, comment),
35         ast::CommentShape::Line => line_to_block(acc, comment),
36     }
37 }
38
39 fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
40     let target = comment.syntax().text_range();
41
42     acc.add(
43         AssistId("block_to_line", AssistKind::RefactorRewrite),
44         "Replace block comment with line comments",
45         target,
46         |edit| {
47             let indentation = IndentLevel::from_token(comment.syntax());
48             let line_prefix = CommentKind { shape: CommentShape::Line, ..comment.kind() }.prefix();
49
50             let text = comment.text();
51             let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
52
53             let lines = text.lines().peekable();
54
55             let indent_spaces = indentation.to_string();
56             let output = lines
57                 .map(|line| {
58                     let line = line.trim_start_matches(&indent_spaces);
59
60                     // Don't introduce trailing whitespace
61                     if line.is_empty() {
62                         line_prefix.to_string()
63                     } else {
64                         format!("{line_prefix} {line}")
65                     }
66                 })
67                 .join(&format!("\n{indent_spaces}"));
68
69             edit.replace(target, output)
70         },
71     )
72 }
73
74 fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
75     // Find all the comments we'll be collapsing into a block
76     let comments = relevant_line_comments(&comment);
77
78     // Establish the target of our edit based on the comments we found
79     let target = TextRange::new(
80         comments[0].syntax().text_range().start(),
81         comments.last().unwrap().syntax().text_range().end(),
82     );
83
84     acc.add(
85         AssistId("line_to_block", AssistKind::RefactorRewrite),
86         "Replace line comments with a single block comment",
87         target,
88         |edit| {
89             // We pick a single indentation level for the whole block comment based on the
90             // comment where the assist was invoked. This will be prepended to the
91             // contents of each line comment when they're put into the block comment.
92             let indentation = IndentLevel::from_token(comment.syntax());
93
94             let block_comment_body =
95                 comments.into_iter().map(|c| line_comment_text(indentation, c)).join("\n");
96
97             let block_prefix =
98                 CommentKind { shape: CommentShape::Block, ..comment.kind() }.prefix();
99
100             let output = format!("{block_prefix}\n{block_comment_body}\n{indentation}*/");
101
102             edit.replace(target, output)
103         },
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         .flatten()
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         .flatten();
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_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
164     let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
165
166     // Don't add the indentation if the line is empty
167     if contents.is_empty() {
168         contents.to_owned()
169     } else {
170         indentation.to_string() + contents
171     }
172 }
173
174 #[cfg(test)]
175 mod tests {
176     use crate::tests::{check_assist, check_assist_not_applicable};
177
178     use super::*;
179
180     #[test]
181     fn single_line_to_block() {
182         check_assist(
183             convert_comment_block,
184             r#"
185 // line$0 comment
186 fn main() {
187     foo();
188 }
189 "#,
190             r#"
191 /*
192 line comment
193 */
194 fn main() {
195     foo();
196 }
197 "#,
198         );
199     }
200
201     #[test]
202     fn single_line_to_block_indented() {
203         check_assist(
204             convert_comment_block,
205             r#"
206 fn main() {
207     // line$0 comment
208     foo();
209 }
210 "#,
211             r#"
212 fn main() {
213     /*
214     line comment
215     */
216     foo();
217 }
218 "#,
219         );
220     }
221
222     #[test]
223     fn multiline_to_block() {
224         check_assist(
225             convert_comment_block,
226             r#"
227 fn main() {
228     // above
229     // line$0 comment
230     //
231     // below
232     foo();
233 }
234 "#,
235             r#"
236 fn main() {
237     /*
238     above
239     line comment
240
241     below
242     */
243     foo();
244 }
245 "#,
246         );
247     }
248
249     #[test]
250     fn end_of_line_to_block() {
251         check_assist_not_applicable(
252             convert_comment_block,
253             r#"
254 fn main() {
255     foo(); // end-of-line$0 comment
256 }
257 "#,
258         );
259     }
260
261     #[test]
262     fn single_line_different_kinds() {
263         check_assist(
264             convert_comment_block,
265             r#"
266 fn main() {
267     /// different prefix
268     // line$0 comment
269     // below
270     foo();
271 }
272 "#,
273             r#"
274 fn main() {
275     /// different prefix
276     /*
277     line comment
278     below
279     */
280     foo();
281 }
282 "#,
283         );
284     }
285
286     #[test]
287     fn single_line_separate_chunks() {
288         check_assist(
289             convert_comment_block,
290             r#"
291 fn main() {
292     // different chunk
293
294     // line$0 comment
295     // below
296     foo();
297 }
298 "#,
299             r#"
300 fn main() {
301     // different chunk
302
303     /*
304     line comment
305     below
306     */
307     foo();
308 }
309 "#,
310         );
311     }
312
313     #[test]
314     fn doc_block_comment_to_lines() {
315         check_assist(
316             convert_comment_block,
317             r#"
318 /**
319  hi$0 there
320 */
321 "#,
322             r#"
323 /// hi there
324 "#,
325         );
326     }
327
328     #[test]
329     fn block_comment_to_lines() {
330         check_assist(
331             convert_comment_block,
332             r#"
333 /*
334  hi$0 there
335 */
336 "#,
337             r#"
338 // hi there
339 "#,
340         );
341     }
342
343     #[test]
344     fn inner_doc_block_to_lines() {
345         check_assist(
346             convert_comment_block,
347             r#"
348 /*!
349  hi$0 there
350 */
351 "#,
352             r#"
353 //! hi there
354 "#,
355         );
356     }
357
358     #[test]
359     fn block_to_lines_indent() {
360         check_assist(
361             convert_comment_block,
362             r#"
363 fn main() {
364     /*!
365     hi$0 there
366
367     ```
368       code_sample
369     ```
370     */
371 }
372 "#,
373             r#"
374 fn main() {
375     //! hi there
376     //!
377     //! ```
378     //!   code_sample
379     //! ```
380 }
381 "#,
382         );
383     }
384
385     #[test]
386     fn end_of_line_block_to_line() {
387         check_assist_not_applicable(
388             convert_comment_block,
389             r#"
390 fn main() {
391     foo(); /* end-of-line$0 comment */
392 }
393 "#,
394         );
395     }
396 }