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