use either::Either; use itertools::Itertools; use syntax::{ ast::{self, edit::IndentLevel, CommentPlacement, Whitespace}, AstToken, TextRange, }; use crate::{ handlers::convert_comment_block::{line_comment_text, relevant_line_comments}, utils::required_hashes, AssistContext, AssistId, AssistKind, Assists, }; // Assist: desugar_doc_comment // // Desugars doc-comments to the attribute form. // // ``` // /// Multi-line$0 // /// comment // ``` // -> // ``` // #[doc = r"Multi-line // comment"] // ``` pub(crate) fn desugar_doc_comment(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> { let comment = ctx.find_token_at_offset::()?; // Only allow doc comments let Some(placement) = comment.kind().doc else { return None; }; // Only allow comments which are alone on their line if let Some(prev) = comment.syntax().prev_token() { Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?; } let indentation = IndentLevel::from_token(comment.syntax()).to_string(); let (target, comments) = match comment.kind().shape { ast::CommentShape::Block => (comment.syntax().text_range(), Either::Left(comment)), ast::CommentShape::Line => { // Find all the comments we'll be desugaring let comments = relevant_line_comments(&comment); // Establish the target of our edit based on the comments we found ( TextRange::new( comments[0].syntax().text_range().start(), comments.last()?.syntax().text_range().end(), ), Either::Right(comments), ) } }; acc.add( AssistId("desugar_doc_comment", AssistKind::RefactorRewrite), "Desugar doc-comment to attribute macro", target, |edit| { let text = match comments { Either::Left(comment) => { let text = comment.text(); text[comment.prefix().len()..(text.len() - "*/".len())] .trim() .lines() .map(|l| l.strip_prefix(&indentation).unwrap_or(l)) .join("\n") } Either::Right(comments) => comments .into_iter() .map(|cm| line_comment_text(IndentLevel(0), cm)) .collect::>() .join("\n"), }; let hashes = "#".repeat(required_hashes(&text)); let prefix = match placement { CommentPlacement::Inner => "#!", CommentPlacement::Outer => "#", }; let output = format!(r#"{prefix}[doc = r{hashes}"{text}"{hashes}]"#); edit.replace(target, output) }, ) } #[cfg(test)] mod tests { use crate::tests::{check_assist, check_assist_not_applicable}; use super::*; #[test] fn single_line() { check_assist( desugar_doc_comment, r#" /// line$0 comment fn main() { foo(); } "#, r#" #[doc = r"line comment"] fn main() { foo(); } "#, ); check_assist( desugar_doc_comment, r#" //! line$0 comment fn main() { foo(); } "#, r#" #![doc = r"line comment"] fn main() { foo(); } "#, ); } #[test] fn single_line_indented() { check_assist( desugar_doc_comment, r#" fn main() { /// line$0 comment struct Foo; } "#, r#" fn main() { #[doc = r"line comment"] struct Foo; } "#, ); } #[test] fn multiline() { check_assist( desugar_doc_comment, r#" fn main() { /// above /// line$0 comment /// /// below struct Foo; } "#, r#" fn main() { #[doc = r"above line comment below"] struct Foo; } "#, ); } #[test] fn end_of_line() { check_assist_not_applicable( desugar_doc_comment, r#" fn main() { /// end-of-line$0 comment struct Foo; } "#, ); } #[test] fn single_line_different_kinds() { check_assist( desugar_doc_comment, r#" fn main() { //! different prefix /// line$0 comment /// below struct Foo; } "#, r#" fn main() { //! different prefix #[doc = r"line comment below"] struct Foo; } "#, ); } #[test] fn single_line_separate_chunks() { check_assist( desugar_doc_comment, r#" /// different chunk /// line$0 comment /// below "#, r#" /// different chunk #[doc = r"line comment below"] "#, ); } #[test] fn block_comment() { check_assist( desugar_doc_comment, r#" /** hi$0 there */ "#, r#" #[doc = r"hi there"] "#, ); } #[test] fn inner_doc_block() { check_assist( desugar_doc_comment, r#" /*! hi$0 there */ "#, r#" #![doc = r"hi there"] "#, ); } #[test] fn block_indent() { check_assist( desugar_doc_comment, r#" fn main() { /*! hi$0 there ``` code_sample ``` */ } "#, r#" fn main() { #![doc = r"hi there ``` code_sample ```"] } "#, ); } #[test] fn end_of_line_block() { check_assist_not_applicable( desugar_doc_comment, r#" fn main() { foo(); /** end-of-line$0 comment */ } "#, ); } #[test] fn regular_comment() { check_assist_not_applicable(desugar_doc_comment, r#"// some$0 comment"#); check_assist_not_applicable(desugar_doc_comment, r#"/* some$0 comment*/"#); } #[test] fn quotes_and_escapes() { check_assist( desugar_doc_comment, r###"/// some$0 "\ "## comment"###, r####"#[doc = r###"some "\ "## comment"###]"####, ); } }