ide_assists/handlers/
convert_comment_block.rs1use itertools::Itertools;
2use syntax::{
3 AstToken, Direction, SyntaxElement, TextRange,
4 ast::{self, Comment, CommentKind, CommentShape, Whitespace, edit::IndentLevel},
5};
6
7use crate::{AssistContext, AssistId, Assists};
8
9pub(crate) fn convert_comment_block(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
25 let comment = ctx.find_token_at_offset::<ast::Comment>()?;
26 if let Some(prev) = comment.syntax().prev_token() {
28 Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
29 }
30
31 match comment.kind().shape {
32 ast::CommentShape::Block => block_to_line(acc, comment),
33 ast::CommentShape::Line => line_to_block(acc, comment),
34 }
35}
36
37fn block_to_line(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
38 let target = comment.syntax().text_range();
39
40 acc.add(
41 AssistId::refactor_rewrite("block_to_line"),
42 "Replace block comment with line comments",
43 target,
44 |edit| {
45 let indentation = IndentLevel::from_token(comment.syntax());
46 let line_prefix = CommentKind { shape: CommentShape::Line, ..comment.kind() }.prefix();
47
48 let text = comment.text();
49 let text = &text[comment.prefix().len()..(text.len() - "*/".len())].trim();
50
51 let lines = text.lines().peekable();
52
53 let indent_spaces = indentation.to_string();
54 let output = lines
55 .map(|line| {
56 let line = line.trim_start_matches(&indent_spaces);
57
58 if line.is_empty() {
60 line_prefix.to_owned()
61 } else {
62 format!("{line_prefix} {line}")
63 }
64 })
65 .join(&format!("\n{indent_spaces}"));
66
67 edit.replace(target, output)
68 },
69 )
70}
71
72fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
73 let comments = relevant_line_comments(&comment);
75
76 let target = TextRange::new(
78 comments[0].syntax().text_range().start(),
79 comments.last()?.syntax().text_range().end(),
80 );
81
82 acc.add(
83 AssistId::refactor_rewrite("line_to_block"),
84 "Replace line comments with a single block comment",
85 target,
86 |edit| {
87 let indentation = IndentLevel::from_token(comment.syntax());
91
92 let block_comment_body = comments
93 .into_iter()
94 .map(|c| line_comment_text(indentation, c))
95 .collect::<Vec<String>>()
96 .into_iter()
97 .join("\n");
98
99 let block_prefix =
100 CommentKind { shape: CommentShape::Block, ..comment.kind() }.prefix();
101
102 let output = format!("{block_prefix}\n{block_comment_body}\n{indentation}*/");
103
104 edit.replace(target, output)
105 },
106 )
107}
108
109pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
113 let prefix = comment.prefix();
115 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
116
117 let skippable = |not: &SyntaxElement| {
119 not.clone()
120 .into_token()
121 .and_then(Whitespace::cast)
122 .map(|w| !w.spans_multiple_lines())
123 .unwrap_or(false)
124 };
125
126 let prev_comments = comment
128 .syntax()
129 .siblings_with_tokens(Direction::Prev)
130 .filter(|s| !skippable(s))
131 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
132 .take_while(|opt_com| opt_com.is_some())
133 .flatten()
134 .skip(1); let next_comments = comment
137 .syntax()
138 .siblings_with_tokens(Direction::Next)
139 .filter(|s| !skippable(s))
140 .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
141 .take_while(|opt_com| opt_com.is_some())
142 .flatten();
143
144 let mut comments: Vec<_> = prev_comments.collect();
145 comments.reverse();
146 comments.extend(next_comments);
147 comments
148}
149
150pub(crate) fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
165 let text = comm.text();
166 let contents_without_prefix = text.strip_prefix(comm.prefix()).unwrap_or(text);
167 let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
168
169 if contents.is_empty() { contents.to_owned() } else { indentation.to_string() + contents }
171}
172
173#[cfg(test)]
174mod 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
185fn main() {
186 foo();
187}
188"#,
189 r#"
190/*
191line comment
192*/
193fn 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#"
205fn main() {
206 // line$0 comment
207 foo();
208}
209"#,
210 r#"
211fn 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#"
226fn main() {
227 // above
228 // line$0 comment
229 //
230 // below
231 foo();
232}
233"#,
234 r#"
235fn 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#"
253fn 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#"
265fn main() {
266 /// different prefix
267 // line$0 comment
268 // below
269 foo();
270}
271"#,
272 r#"
273fn 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#"
290fn main() {
291 // different chunk
292
293 // line$0 comment
294 // below
295 foo();
296}
297"#,
298 r#"
299fn 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#"
362fn main() {
363 /*!
364 hi$0 there
365
366 ```
367 code_sample
368 ```
369 */
370}
371"#,
372 r#"
373fn 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#"
389fn main() {
390 foo(); /* end-of-line$0 comment */
391}
392"#,
393 );
394 }
395}