Skip to content

Commit 6f201cf

Browse files
committed
Assist: desugar doc-comment
1 parent 09aceea commit 6f201cf

File tree

5 files changed

+333
-3
lines changed

5 files changed

+333
-3
lines changed

crates/ide-assists/src/handlers/convert_comment_block.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
107107
/// The line -> block assist can be invoked from anywhere within a sequence of line comments.
108108
/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
109109
/// be joined.
110-
fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
110+
pub fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
111111
// The prefix identifies the kind of comment we're dealing with
112112
let prefix = comment.prefix();
113113
let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
@@ -159,7 +159,7 @@ fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
159159
// */
160160
//
161161
// But since such comments aren't idiomatic we're okay with this.
162-
fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
162+
pub fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
163163
let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
164164
let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
165165

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

crates/ide-assists/src/handlers/raw_string.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
155155
})
156156
}
157157

158-
fn required_hashes(s: &str) -> usize {
158+
pub fn required_hashes(s: &str) -> usize {
159159
let mut res = 0usize;
160160
for idx in s.match_indices('"').map(|(i, _)| i) {
161161
let (_, sub) = s.split_at(idx + 1);

crates/ide-assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ mod handlers {
126126
mod convert_to_guarded_return;
127127
mod convert_two_arm_bool_match_to_matches_macro;
128128
mod convert_while_to_loop;
129+
mod desugar_doc_comment;
129130
mod destructure_tuple_binding;
130131
mod expand_glob_import;
131132
mod extract_expressions_from_format_string;
@@ -231,6 +232,7 @@ mod handlers {
231232
convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
232233
convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro,
233234
convert_while_to_loop::convert_while_to_loop,
235+
desugar_doc_comment::desugar_doc_comment,
234236
destructure_tuple_binding::destructure_tuple_binding,
235237
expand_glob_import::expand_glob_import,
236238
extract_expressions_from_format_string::extract_expressions_from_format_string,

crates/ide-assists/src/tests/generated.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,21 @@ fn main() {
597597
)
598598
}
599599

600+
#[test]
601+
fn doctest_desugar_doc_comment() {
602+
check_doc_test(
603+
"desugar_doc_comment",
604+
r#####"
605+
/// Multi-line$0
606+
/// comment
607+
"#####,
608+
r#####"
609+
#[doc = r"Multi-line
610+
comment"]
611+
"#####,
612+
)
613+
}
614+
600615
#[test]
601616
fn doctest_expand_glob_import() {
602617
check_doc_test(

0 commit comments

Comments
 (0)