ide_assists/handlers/
convert_comment_from_or_to_doc.rs

1use itertools::Itertools;
2use syntax::{
3    AstToken, Direction, SyntaxElement, TextRange,
4    ast::{self, Comment, CommentPlacement, Whitespace, edit::IndentLevel},
5};
6
7use crate::{AssistContext, AssistId, Assists};
8
9// Assist: comment_to_doc
10//
11// Converts comments to documentation.
12//
13// ```
14// // Wow what $0a nice module
15// // I sure hope this shows up when I hover over it
16// ```
17// ->
18// ```
19// //! Wow what a nice module
20// //! I sure hope this shows up when I hover over it
21// ```
22pub(crate) fn convert_comment_from_or_to_doc(
23    acc: &mut Assists,
24    ctx: &AssistContext<'_>,
25) -> Option<()> {
26    let comment = ctx.find_token_at_offset::<ast::Comment>()?;
27
28    match comment.kind().doc {
29        Some(_) => doc_to_comment(acc, comment),
30        None => can_be_doc_comment(&comment).and_then(|style| comment_to_doc(acc, comment, style)),
31    }
32}
33
34fn doc_to_comment(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
35    let target = if comment.kind().shape.is_line() {
36        line_comments_text_range(&comment)?
37    } else {
38        comment.syntax().text_range()
39    };
40
41    acc.add(
42        AssistId::refactor_rewrite("doc_to_comment"),
43        "Replace doc comment with comment",
44        target,
45        |edit| {
46            // We need to either replace the first occurrence of /* with /***, or we need to replace
47            // the occurrences // at the start of each line with ///
48            let output = match comment.kind().shape {
49                ast::CommentShape::Line => {
50                    let indentation = IndentLevel::from_token(comment.syntax());
51                    let line_start = comment.prefix();
52                    let prefix = format!("{indentation}//");
53                    relevant_line_comments(&comment)
54                        .iter()
55                        .map(|comment| comment.text())
56                        .flat_map(|text| text.lines())
57                        .map(|line| line.replacen(line_start, &prefix, 1))
58                        .join("\n")
59                }
60                ast::CommentShape::Block => {
61                    let block_start = comment.prefix();
62                    comment
63                        .text()
64                        .lines()
65                        .enumerate()
66                        .map(|(idx, line)| {
67                            if idx == 0 {
68                                line.replacen(block_start, "/*", 1)
69                            } else {
70                                line.replacen("*  ", "* ", 1)
71                            }
72                        })
73                        .join("\n")
74                }
75            };
76            edit.replace(target, output)
77        },
78    )
79}
80
81fn comment_to_doc(acc: &mut Assists, comment: ast::Comment, style: CommentPlacement) -> Option<()> {
82    let target = if comment.kind().shape.is_line() {
83        line_comments_text_range(&comment)?
84    } else {
85        comment.syntax().text_range()
86    };
87
88    acc.add(
89        AssistId::refactor_rewrite("comment_to_doc"),
90        "Replace comment with doc comment",
91        target,
92        |edit| {
93            // We need to either replace the first occurrence of /* with /***, or we need to replace
94            // the occurrences // at the start of each line with ///
95            let output = match comment.kind().shape {
96                ast::CommentShape::Line => {
97                    let indentation = IndentLevel::from_token(comment.syntax());
98                    let line_start = match style {
99                        CommentPlacement::Inner => format!("{indentation}//!"),
100                        CommentPlacement::Outer => format!("{indentation}///"),
101                    };
102                    relevant_line_comments(&comment)
103                        .iter()
104                        .map(|comment| comment.text())
105                        .flat_map(|text| text.lines())
106                        .map(|line| line.replacen("//", &line_start, 1))
107                        .join("\n")
108                }
109                ast::CommentShape::Block => {
110                    let block_start = match style {
111                        CommentPlacement::Inner => "/*!",
112                        CommentPlacement::Outer => "/**",
113                    };
114                    comment
115                        .text()
116                        .lines()
117                        .enumerate()
118                        .map(|(idx, line)| {
119                            if idx == 0 {
120                                // On the first line we replace the comment start with a doc comment
121                                // start.
122                                line.replacen("/*", block_start, 1)
123                            } else {
124                                // put one extra space after each * since we moved the first line to
125                                // the right by one column as well.
126                                line.replacen("* ", "*  ", 1)
127                            }
128                        })
129                        .join("\n")
130                }
131            };
132            edit.replace(target, output)
133        },
134    )
135}
136
137/// Not all comments are valid candidates for conversion into doc comments. For example, the
138/// comments in the code:
139/// ```ignore
140/// // Brilliant module right here
141///
142/// // Really good right
143/// fn good_function(foo: Foo) -> Bar {
144///     foo.into_bar()
145/// }
146///
147/// // So nice
148/// mod nice_module {}
149/// ```
150/// can be converted to doc comments. However, the comments in this example:
151/// ```ignore
152/// fn foo_bar(foo: Foo /* not bar yet */) -> Bar {
153///     foo.into_bar()
154///     // Nicely done
155/// }
156/// // end of function
157///
158/// struct S {
159///     // The S struct
160/// }
161/// ```
162/// are not allowed to become doc comments. Moreover, some comments _are_ allowed, but aren't common
163/// style in Rust. For example, the following comments are allowed to be doc comments, but it is not
164/// common style for them to be:
165/// ```ignore
166/// fn foo_bar(foo: Foo) -> Bar {
167///     // this could be an inner comment with //!
168///     foo.into_bar()
169/// }
170///
171/// trait T {
172///     // The T struct could also be documented from within
173/// }
174///
175/// mod mymod {
176///     // Modules only normally get inner documentation when they are defined as a separate file.
177/// }
178/// ```
179fn can_be_doc_comment(comment: &ast::Comment) -> Option<CommentPlacement> {
180    use syntax::SyntaxKind::*;
181
182    // if the comment is not on its own line, then we do not propose anything.
183    match comment.syntax().prev_token() {
184        Some(prev) => {
185            // There was a previous token, now check if it was a newline
186            Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
187        }
188        // There is no previous token, this is the start of the file.
189        None => return Some(CommentPlacement::Inner),
190    }
191
192    // check if comment is followed by: `struct`, `trait`, `mod`, `fn`, `type`, `extern crate`,
193    // `use` or `const`.
194    let parent = comment.syntax().parent();
195    let par_kind = parent.as_ref().map(|parent| parent.kind());
196    matches!(par_kind, Some(STRUCT | TRAIT | MODULE | FN | TYPE_ALIAS | EXTERN_CRATE | USE | CONST))
197        .then_some(CommentPlacement::Outer)
198}
199
200/// The line -> block assist can  be invoked from anywhere within a sequence of line comments.
201/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
202/// be joined.
203pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
204    // The prefix identifies the kind of comment we're dealing with
205    let prefix = comment.prefix();
206    let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
207
208    // These tokens are allowed to exist between comments
209    let skippable = |not: &SyntaxElement| {
210        not.clone()
211            .into_token()
212            .and_then(Whitespace::cast)
213            .map(|w| !w.spans_multiple_lines())
214            .unwrap_or(false)
215    };
216
217    // Find all preceding comments (in reverse order) that have the same prefix
218    let prev_comments = comment
219        .syntax()
220        .siblings_with_tokens(Direction::Prev)
221        .filter(|s| !skippable(s))
222        .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
223        .take_while(|opt_com| opt_com.is_some())
224        .flatten()
225        .skip(1); // skip the first element so we don't duplicate it in next_comments
226
227    let next_comments = comment
228        .syntax()
229        .siblings_with_tokens(Direction::Next)
230        .filter(|s| !skippable(s))
231        .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
232        .take_while(|opt_com| opt_com.is_some())
233        .flatten();
234
235    let mut comments: Vec<_> = prev_comments.collect();
236    comments.reverse();
237    comments.extend(next_comments);
238    comments
239}
240
241fn line_comments_text_range(comment: &ast::Comment) -> Option<TextRange> {
242    let comments = relevant_line_comments(comment);
243    let first = comments.first()?;
244    let indentation = IndentLevel::from_token(first.syntax());
245    let start =
246        first.syntax().text_range().start().checked_sub((indentation.0 as u32 * 4).into())?;
247    let end = comments.last()?.syntax().text_range().end();
248    Some(TextRange::new(start, end))
249}
250
251#[cfg(test)]
252mod tests {
253    use crate::tests::{check_assist, check_assist_not_applicable};
254
255    use super::*;
256
257    #[test]
258    fn module_comment_to_doc() {
259        check_assist(
260            convert_comment_from_or_to_doc,
261            r#"
262            // such a nice module$0
263            fn main() {
264                foo();
265            }
266            "#,
267            r#"
268            //! such a nice module
269            fn main() {
270                foo();
271            }
272            "#,
273        );
274    }
275
276    #[test]
277    fn single_line_comment_to_doc() {
278        check_assist(
279            convert_comment_from_or_to_doc,
280            r#"
281
282            // unseen$0 docs
283            fn main() {
284                foo();
285            }
286            "#,
287            r#"
288
289            /// unseen docs
290            fn main() {
291                foo();
292            }
293            "#,
294        );
295    }
296
297    #[test]
298    fn multi_line_comment_to_doc() {
299        check_assist(
300            convert_comment_from_or_to_doc,
301            r#"
302
303            // unseen$0 docs
304            // make me seen!
305            fn main() {
306                foo();
307            }
308            "#,
309            r#"
310
311            /// unseen docs
312            /// make me seen!
313            fn main() {
314                foo();
315            }
316            "#,
317        );
318    }
319
320    #[test]
321    fn single_line_doc_to_comment() {
322        check_assist(
323            convert_comment_from_or_to_doc,
324            r#"
325
326            /// visible$0 docs
327            fn main() {
328                foo();
329            }
330            "#,
331            r#"
332
333            // visible docs
334            fn main() {
335                foo();
336            }
337            "#,
338        );
339    }
340
341    #[test]
342    fn multi_line_doc_to_comment() {
343        check_assist(
344            convert_comment_from_or_to_doc,
345            r#"
346
347            /// visible$0 docs
348            /// Hide me!
349            fn main() {
350                foo();
351            }
352            "#,
353            r#"
354
355            // visible docs
356            // Hide me!
357            fn main() {
358                foo();
359            }
360            "#,
361        );
362    }
363
364    #[test]
365    fn single_line_block_comment_to_doc() {
366        check_assist(
367            convert_comment_from_or_to_doc,
368            r#"
369
370            /* unseen$0 docs */
371            fn main() {
372                foo();
373            }
374            "#,
375            r#"
376
377            /** unseen docs */
378            fn main() {
379                foo();
380            }
381            "#,
382        );
383    }
384
385    #[test]
386    fn multi_line_block_comment_to_doc() {
387        check_assist(
388            convert_comment_from_or_to_doc,
389            r#"
390
391            /* unseen$0 docs
392            *  make me seen!
393            */
394            fn main() {
395                foo();
396            }
397            "#,
398            r#"
399
400            /** unseen docs
401            *   make me seen!
402            */
403            fn main() {
404                foo();
405            }
406            "#,
407        );
408    }
409
410    #[test]
411    fn single_line_block_doc_to_comment() {
412        check_assist(
413            convert_comment_from_or_to_doc,
414            r#"
415
416            /** visible$0 docs */
417            fn main() {
418                foo();
419            }
420            "#,
421            r#"
422
423            /* visible docs */
424            fn main() {
425                foo();
426            }
427            "#,
428        );
429    }
430
431    #[test]
432    fn multi_line_block_doc_to_comment() {
433        check_assist(
434            convert_comment_from_or_to_doc,
435            r#"
436
437            /** visible$0 docs
438            *   Hide me!
439            */
440            fn main() {
441                foo();
442            }
443            "#,
444            r#"
445
446            /* visible docs
447            *  Hide me!
448            */
449            fn main() {
450                foo();
451            }
452            "#,
453        );
454    }
455
456    #[test]
457    fn single_inner_line_comment_to_doc() {
458        check_assist_not_applicable(
459            convert_comment_from_or_to_doc,
460            r#"
461            mod mymod {
462                // unseen$0 docs
463                foo();
464            }
465            "#,
466        );
467    }
468
469    #[test]
470    fn single_inner_line_doc_to_comment() {
471        check_assist(
472            convert_comment_from_or_to_doc,
473            r#"
474            mod mymod {
475                //! visible$0 docs
476                foo();
477            }
478            "#,
479            r#"
480            mod mymod {
481                // visible docs
482                foo();
483            }
484            "#,
485        );
486    }
487
488    #[test]
489    fn multi_inner_line_doc_to_comment() {
490        check_assist(
491            convert_comment_from_or_to_doc,
492            r#"
493            mod mymod {
494                //! visible$0 docs
495                //! Hide me!
496                foo();
497            }
498            "#,
499            r#"
500            mod mymod {
501                // visible docs
502                // Hide me!
503                foo();
504            }
505            "#,
506        );
507        check_assist(
508            convert_comment_from_or_to_doc,
509            r#"
510            mod mymod {
511                /// visible$0 docs
512                /// Hide me!
513                foo();
514            }
515            "#,
516            r#"
517            mod mymod {
518                // visible docs
519                // Hide me!
520                foo();
521            }
522            "#,
523        );
524    }
525
526    #[test]
527    fn single_inner_line_block_doc_to_comment() {
528        check_assist(
529            convert_comment_from_or_to_doc,
530            r#"
531            mod mymod {
532                /*! visible$0 docs */
533                type Int = i32;
534            }
535            "#,
536            r#"
537            mod mymod {
538                /* visible docs */
539                type Int = i32;
540            }
541            "#,
542        );
543    }
544
545    #[test]
546    fn multi_inner_line_block_doc_to_comment() {
547        check_assist(
548            convert_comment_from_or_to_doc,
549            r#"
550            mod mymod {
551                /*! visible$0 docs
552                *   Hide me!
553                */
554                type Int = i32;
555            }
556            "#,
557            r#"
558            mod mymod {
559                /* visible docs
560                *  Hide me!
561                */
562                type Int = i32;
563            }
564            "#,
565        );
566    }
567
568    #[test]
569    fn not_overeager() {
570        check_assist_not_applicable(
571            convert_comment_from_or_to_doc,
572            r#"
573            fn main() {
574                foo();
575                // $0well that settles main
576            }
577            // $1 nicely done
578            "#,
579        );
580    }
581
582    #[test]
583    fn all_possible_items() {
584        check_assist(
585            convert_comment_from_or_to_doc,
586            r#"mod m {
587                /* Nice struct$0 */
588                struct S {}
589            }"#,
590            r#"mod m {
591                /** Nice struct */
592                struct S {}
593            }"#,
594        );
595        check_assist(
596            convert_comment_from_or_to_doc,
597            r#"mod m {
598                /* Nice trait$0 */
599                trait T {}
600            }"#,
601            r#"mod m {
602                /** Nice trait */
603                trait T {}
604            }"#,
605        );
606        check_assist(
607            convert_comment_from_or_to_doc,
608            r#"mod m {
609                /* Nice module$0 */
610                mod module {}
611            }"#,
612            r#"mod m {
613                /** Nice module */
614                mod module {}
615            }"#,
616        );
617        check_assist(
618            convert_comment_from_or_to_doc,
619            r#"mod m {
620                /* Nice function$0 */
621                fn function() {}
622            }"#,
623            r#"mod m {
624                /** Nice function */
625                fn function() {}
626            }"#,
627        );
628        check_assist(
629            convert_comment_from_or_to_doc,
630            r#"mod m {
631                /* Nice type$0 */
632                type Type Int = i32;
633            }"#,
634            r#"mod m {
635                /** Nice type */
636                type Type Int = i32;
637            }"#,
638        );
639        check_assist(
640            convert_comment_from_or_to_doc,
641            r#"mod m {
642                /* Nice crate$0 */
643                extern crate rust_analyzer;
644            }"#,
645            r#"mod m {
646                /** Nice crate */
647                extern crate rust_analyzer;
648            }"#,
649        );
650        check_assist(
651            convert_comment_from_or_to_doc,
652            r#"mod m {
653                /* Nice import$0 */
654                use ide_assists::convert_comment_from_or_to_doc::tests
655            }"#,
656            r#"mod m {
657                /** Nice import */
658                use ide_assists::convert_comment_from_or_to_doc::tests
659            }"#,
660        );
661        check_assist(
662            convert_comment_from_or_to_doc,
663            r#"mod m {
664                /* Nice constant$0 */
665                const CONST: &str = "very const";
666            }"#,
667            r#"mod m {
668                /** Nice constant */
669                const CONST: &str = "very const";
670            }"#,
671        );
672    }
673
674    #[test]
675    fn no_inner_comments() {
676        check_assist_not_applicable(
677            convert_comment_from_or_to_doc,
678            r#"
679            mod mymod {
680                // aaa$0aa
681            }
682            "#,
683        );
684    }
685}