Skip to content

Commit 455ef0c

Browse files
committed
Auto merge of rust-lang#13935 - ModProg:assist_desugar_doc_comment, r=Veykril
Assist: desugar doc-comment My need for this arose due to wanting to do feature dependent documentation and therefor convert parts of my doc-comments to attributes. Not sure about the pub-making of the other handlers functions, but I didn't think it made much sense to reimplement them.
2 parents 1d02474 + ec06313 commit 455ef0c

File tree

6 files changed

+353
-24
lines changed

6 files changed

+353
-24
lines changed

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

+2-2
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(crate) 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(crate) 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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
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::{
9+
handlers::convert_comment_block::{line_comment_text, relevant_line_comments},
10+
utils::required_hashes,
11+
AssistContext, AssistId, AssistKind, Assists,
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+
// ```
27+
pub(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 Some(placement) = comment.kind().doc else { return None; };
31+
32+
// Only allow comments which are alone on their line
33+
if let Some(prev) = comment.syntax().prev_token() {
34+
if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
35+
return None;
36+
}
37+
}
38+
39+
let indentation = IndentLevel::from_token(comment.syntax()).to_string();
40+
41+
let (target, comments) = match comment.kind().shape {
42+
ast::CommentShape::Block => (comment.syntax().text_range(), Either::Left(comment)),
43+
ast::CommentShape::Line => {
44+
// Find all the comments we'll be desugaring
45+
let comments = relevant_line_comments(&comment);
46+
47+
// Establish the target of our edit based on the comments we found
48+
(
49+
TextRange::new(
50+
comments[0].syntax().text_range().start(),
51+
comments.last().unwrap().syntax().text_range().end(),
52+
),
53+
Either::Right(comments),
54+
)
55+
}
56+
};
57+
58+
acc.add(
59+
AssistId("desugar_doc_comment", AssistKind::RefactorRewrite),
60+
"Desugar doc-comment to attribute macro",
61+
target,
62+
|edit| {
63+
let text = match comments {
64+
Either::Left(comment) => {
65+
let text = comment.text();
66+
text[comment.prefix().len()..(text.len() - "*/".len())]
67+
.trim()
68+
.lines()
69+
.map(|l| l.strip_prefix(&indentation).unwrap_or(l))
70+
.join("\n")
71+
}
72+
Either::Right(comments) => {
73+
comments.into_iter().map(|c| line_comment_text(IndentLevel(0), c)).join("\n")
74+
}
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)]
92+
mod 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
103+
fn main() {
104+
foo();
105+
}
106+
"#,
107+
r#"
108+
#[doc = r"line comment"]
109+
fn main() {
110+
foo();
111+
}
112+
"#,
113+
);
114+
check_assist(
115+
desugar_doc_comment,
116+
r#"
117+
//! line$0 comment
118+
fn main() {
119+
foo();
120+
}
121+
"#,
122+
r#"
123+
#![doc = r"line comment"]
124+
fn main() {
125+
foo();
126+
}
127+
"#,
128+
);
129+
}
130+
131+
#[test]
132+
fn single_line_indented() {
133+
check_assist(
134+
desugar_doc_comment,
135+
r#"
136+
fn main() {
137+
/// line$0 comment
138+
struct Foo;
139+
}
140+
"#,
141+
r#"
142+
fn 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#"
155+
fn main() {
156+
/// above
157+
/// line$0 comment
158+
///
159+
/// below
160+
struct Foo;
161+
}
162+
"#,
163+
r#"
164+
fn main() {
165+
#[doc = r"above
166+
line comment
167+
168+
below"]
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#"
180+
fn 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#"
192+
fn main() {
193+
//! different prefix
194+
/// line$0 comment
195+
/// below
196+
struct Foo;
197+
}
198+
"#,
199+
r#"
200+
fn main() {
201+
//! different prefix
202+
#[doc = r"line comment
203+
below"]
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
224+
below"]
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#"
264+
fn main() {
265+
/*!
266+
hi$0 there
267+
268+
```
269+
code_sample
270+
```
271+
*/
272+
}
273+
"#,
274+
r#"
275+
fn 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#"
291+
fn 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+
}

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

+1-22
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::borrow::Cow;
22

33
use syntax::{ast, ast::IsString, AstToken, TextRange, TextSize};
44

5-
use crate::{AssistContext, AssistId, AssistKind, Assists};
5+
use crate::{utils::required_hashes, AssistContext, AssistId, AssistKind, Assists};
66

77
// Assist: make_raw_string
88
//
@@ -155,33 +155,12 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
155155
})
156156
}
157157

158-
fn required_hashes(s: &str) -> usize {
159-
let mut res = 0usize;
160-
for idx in s.match_indices('"').map(|(i, _)| i) {
161-
let (_, sub) = s.split_at(idx + 1);
162-
let n_hashes = sub.chars().take_while(|c| *c == '#').count();
163-
res = res.max(n_hashes + 1)
164-
}
165-
res
166-
}
167-
168158
#[cfg(test)]
169159
mod tests {
170160
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
171161

172162
use super::*;
173163

174-
#[test]
175-
fn test_required_hashes() {
176-
assert_eq!(0, required_hashes("abc"));
177-
assert_eq!(0, required_hashes("###"));
178-
assert_eq!(1, required_hashes("\""));
179-
assert_eq!(2, required_hashes("\"#abc"));
180-
assert_eq!(0, required_hashes("#abc"));
181-
assert_eq!(3, required_hashes("#ab\"##c"));
182-
assert_eq!(5, required_hashes("#ab\"##\"####c"));
183-
}
184-
185164
#[test]
186165
fn make_raw_string_target() {
187166
check_assist_target(

crates/ide-assists/src/lib.rs

+2
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,

0 commit comments

Comments
 (0)