ide_assists/handlers/
desugar_doc_comment.rs

1use either::Either;
2use itertools::Itertools;
3use syntax::{
4    AstToken, TextRange,
5    ast::{self, CommentPlacement, Whitespace, edit::IndentLevel},
6};
7
8use crate::{
9    AssistContext, AssistId, Assists,
10    handlers::convert_comment_block::{line_comment_text, relevant_line_comments},
11    utils::required_hashes,
12};
13
14// Assist: desugar_doc_comment
15//
16// Desugars doc-comments to the attribute form.
17//
18// ```
19// /// Multi-line$0
20// /// comment
21// ```
22// ->
23// ```
24// #[doc = r"Multi-line
25// comment"]
26// ```
27pub(crate) fn desugar_doc_comment(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
28    let comment = ctx.find_token_at_offset::<ast::Comment>()?;
29    // Only allow doc comments
30    let placement = comment.kind().doc?;
31
32    // Only allow comments which are alone on their line
33    if let Some(prev) = comment.syntax().prev_token() {
34        Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
35    }
36
37    let indentation = IndentLevel::from_token(comment.syntax()).to_string();
38
39    let (target, comments) = match comment.kind().shape {
40        ast::CommentShape::Block => (comment.syntax().text_range(), Either::Left(comment)),
41        ast::CommentShape::Line => {
42            // Find all the comments we'll be desugaring
43            let comments = relevant_line_comments(&comment);
44
45            // Establish the target of our edit based on the comments we found
46            (
47                TextRange::new(
48                    comments[0].syntax().text_range().start(),
49                    comments.last()?.syntax().text_range().end(),
50                ),
51                Either::Right(comments),
52            )
53        }
54    };
55
56    acc.add(
57        AssistId::refactor_rewrite("desugar_doc_comment"),
58        "Desugar doc-comment to attribute macro",
59        target,
60        |edit| {
61            let text = match comments {
62                Either::Left(comment) => {
63                    let text = comment.text();
64                    text[comment.prefix().len()..(text.len() - "*/".len())]
65                        .trim()
66                        .lines()
67                        .map(|l| l.strip_prefix(&indentation).unwrap_or(l))
68                        .join("\n")
69                }
70                Either::Right(comments) => comments
71                    .into_iter()
72                    .map(|cm| line_comment_text(IndentLevel(0), cm))
73                    .collect::<Vec<_>>()
74                    .join("\n"),
75            };
76
77            let hashes = "#".repeat(required_hashes(&text));
78
79            let prefix = match placement {
80                CommentPlacement::Inner => "#!",
81                CommentPlacement::Outer => "#",
82            };
83
84            let output = format!(r#"{prefix}[doc = r{hashes}"{text}"{hashes}]"#);
85
86            edit.replace(target, output)
87        },
88    )
89}
90
91#[cfg(test)]
92mod tests {
93    use crate::tests::{check_assist, check_assist_not_applicable};
94
95    use super::*;
96
97    #[test]
98    fn single_line() {
99        check_assist(
100            desugar_doc_comment,
101            r#"
102/// line$0 comment
103fn main() {
104    foo();
105}
106"#,
107            r#"
108#[doc = r"line comment"]
109fn main() {
110    foo();
111}
112"#,
113        );
114        check_assist(
115            desugar_doc_comment,
116            r#"
117//! line$0 comment
118fn main() {
119    foo();
120}
121"#,
122            r#"
123#![doc = r"line comment"]
124fn main() {
125    foo();
126}
127"#,
128        );
129    }
130
131    #[test]
132    fn single_line_indented() {
133        check_assist(
134            desugar_doc_comment,
135            r#"
136fn main() {
137    /// line$0 comment
138    struct Foo;
139}
140"#,
141            r#"
142fn main() {
143    #[doc = r"line comment"]
144    struct Foo;
145}
146"#,
147        );
148    }
149
150    #[test]
151    fn multiline() {
152        check_assist(
153            desugar_doc_comment,
154            r#"
155fn main() {
156    /// above
157    /// line$0 comment
158    ///
159    /// below
160    struct Foo;
161}
162"#,
163            r#"
164fn main() {
165    #[doc = r"above
166line comment
167
168below"]
169    struct Foo;
170}
171"#,
172        );
173    }
174
175    #[test]
176    fn end_of_line() {
177        check_assist_not_applicable(
178            desugar_doc_comment,
179            r#"
180fn main() { /// end-of-line$0 comment
181    struct Foo;
182}
183"#,
184        );
185    }
186
187    #[test]
188    fn single_line_different_kinds() {
189        check_assist(
190            desugar_doc_comment,
191            r#"
192fn main() {
193    //! different prefix
194    /// line$0 comment
195    /// below
196    struct Foo;
197}
198"#,
199            r#"
200fn main() {
201    //! different prefix
202    #[doc = r"line comment
203below"]
204    struct Foo;
205}
206"#,
207        );
208    }
209
210    #[test]
211    fn single_line_separate_chunks() {
212        check_assist(
213            desugar_doc_comment,
214            r#"
215/// different chunk
216
217/// line$0 comment
218/// below
219"#,
220            r#"
221/// different chunk
222
223#[doc = r"line comment
224below"]
225"#,
226        );
227    }
228
229    #[test]
230    fn block_comment() {
231        check_assist(
232            desugar_doc_comment,
233            r#"
234/**
235 hi$0 there
236*/
237"#,
238            r#"
239#[doc = r"hi there"]
240"#,
241        );
242    }
243
244    #[test]
245    fn inner_doc_block() {
246        check_assist(
247            desugar_doc_comment,
248            r#"
249/*!
250 hi$0 there
251*/
252"#,
253            r#"
254#![doc = r"hi there"]
255"#,
256        );
257    }
258
259    #[test]
260    fn block_indent() {
261        check_assist(
262            desugar_doc_comment,
263            r#"
264fn main() {
265    /*!
266    hi$0 there
267
268    ```
269      code_sample
270    ```
271    */
272}
273"#,
274            r#"
275fn main() {
276    #![doc = r"hi there
277
278```
279  code_sample
280```"]
281}
282"#,
283        );
284    }
285
286    #[test]
287    fn end_of_line_block() {
288        check_assist_not_applicable(
289            desugar_doc_comment,
290            r#"
291fn main() {
292    foo(); /** end-of-line$0 comment */
293}
294"#,
295        );
296    }
297
298    #[test]
299    fn regular_comment() {
300        check_assist_not_applicable(desugar_doc_comment, r#"// some$0 comment"#);
301        check_assist_not_applicable(desugar_doc_comment, r#"/* some$0 comment*/"#);
302    }
303
304    #[test]
305    fn quotes_and_escapes() {
306        check_assist(
307            desugar_doc_comment,
308            r###"/// some$0 "\ "## comment"###,
309            r####"#[doc = r###"some "\ "## comment"###]"####,
310        );
311    }
312}