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
9pub(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 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 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 line.replacen("/*", block_start, 1)
123 } else {
124 line.replacen("* ", "* ", 1)
127 }
128 })
129 .join("\n")
130 }
131 };
132 edit.replace(target, output)
133 },
134 )
135}
136
137fn can_be_doc_comment(comment: &ast::Comment) -> Option<CommentPlacement> {
180 use syntax::SyntaxKind::*;
181
182 match comment.syntax().prev_token() {
184 Some(prev) => {
185 Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
187 }
188 None => return Some(CommentPlacement::Inner),
190 }
191
192 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
200pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
204 let prefix = comment.prefix();
206 let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
207
208 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 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); 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}