Unnamed repository; edit this file 'description' to name the repository.
-rw-r--r--crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs645
-rw-r--r--crates/ide-assists/src/lib.rs2
-rw-r--r--crates/ide-assists/src/tests/generated.rs15
3 files changed, 662 insertions, 0 deletions
diff --git a/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs b/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs
new file mode 100644
index 0000000000..1ac30d71f2
--- /dev/null
+++ b/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs
@@ -0,0 +1,645 @@
+use itertools::Itertools;
+use syntax::{
+ ast::{self, edit::IndentLevel, Comment, CommentPlacement, Whitespace},
+ AstToken, Direction, SyntaxElement, TextRange,
+};
+
+use crate::{AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: comment_to_doc
+//
+// Converts comments to documentation.
+//
+// ```
+// // Wow what $0a nice function
+// // I sure hope this shows up when I hover over it
+// ```
+// ->
+// ```
+// //! Wow what a nice function
+// //! I sure hope this shows up when I hover over it
+// ```
+pub(crate) fn convert_comment_from_or_to_doc(
+ acc: &mut Assists,
+ ctx: &AssistContext<'_>,
+) -> Option<()> {
+ let comment = ctx.find_token_at_offset::<ast::Comment>()?;
+
+ match comment.kind().doc {
+ Some(_) => doc_to_comment(acc, comment),
+ None => match can_be_doc_comment(&comment) {
+ Some(doc_comment_style) => comment_to_doc(acc, comment, doc_comment_style),
+ None => None,
+ },
+ }
+}
+
+fn doc_to_comment(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
+ let target = if comment.kind().shape.is_line() {
+ line_comments_text_range(&comment)?
+ } else {
+ comment.syntax().text_range()
+ };
+
+ acc.add(
+ AssistId("doc_to_comment", AssistKind::RefactorRewrite),
+ "Replace a comment with doc comment",
+ target,
+ |edit| {
+ // We need to either replace the first occurrence of /* with /***, or we need to replace
+ // the occurrences // at the start of each line with ///
+ let output = match comment.kind().shape {
+ ast::CommentShape::Line => {
+ let indentation = IndentLevel::from_token(comment.syntax());
+ let line_start = comment.prefix();
+ relevant_line_comments(&comment)
+ .iter()
+ .map(|comment| comment.text())
+ .flat_map(|text| text.lines())
+ .map(|line| indentation.to_string() + &line.replacen(line_start, "//", 1))
+ .join("\n")
+ }
+ ast::CommentShape::Block => {
+ let block_start = comment.prefix();
+ comment
+ .text()
+ .lines()
+ .enumerate()
+ .map(|(idx, line)| {
+ if idx == 0 {
+ line.replacen(block_start, "/*", 1)
+ } else {
+ line.replacen("* ", "* ", 1)
+ }
+ })
+ .join("\n")
+ }
+ };
+ edit.replace(target, output)
+ },
+ )
+}
+
+fn comment_to_doc(acc: &mut Assists, comment: ast::Comment, style: CommentPlacement) -> Option<()> {
+ let target = if comment.kind().shape.is_line() {
+ line_comments_text_range(&comment)?
+ } else {
+ comment.syntax().text_range()
+ };
+
+ acc.add(
+ AssistId("comment_to_doc", AssistKind::RefactorRewrite),
+ "Replace a doc comment with comment",
+ target,
+ |edit| {
+ // We need to either replace the first occurrence of /* with /***, or we need to replace
+ // the occurrences // at the start of each line with ///
+ let output = match comment.kind().shape {
+ ast::CommentShape::Line => {
+ let line_start = match style {
+ CommentPlacement::Inner => "//!",
+ CommentPlacement::Outer => "///",
+ };
+ let indentation = IndentLevel::from_token(comment.syntax());
+ relevant_line_comments(&comment)
+ .iter()
+ .map(|comment| comment.text())
+ .flat_map(|text| text.lines())
+ .map(|line| indentation.to_string() + &line.replacen("//", line_start, 1))
+ .join("\n")
+ }
+ ast::CommentShape::Block => {
+ let block_start = match style {
+ CommentPlacement::Inner => "/*!",
+ CommentPlacement::Outer => "/**",
+ };
+ comment
+ .text()
+ .lines()
+ .enumerate()
+ .map(|(idx, line)| {
+ if idx == 0 {
+ // On the first line we replace the comment start with a doc comment
+ // start.
+ line.replacen("/*", block_start, 1)
+ } else {
+ // put one extra space after each * since we moved the first line to
+ // the right by one column as well.
+ line.replacen("* ", "* ", 1)
+ }
+ })
+ .join("\n")
+ }
+ };
+ edit.replace(target, output)
+ },
+ )
+}
+
+/// Not all comments are valid candidates for conversion into doc comments. For example, the
+/// comments in the code:
+/// ```rust
+/// // foos the bar
+/// fn foo_bar(foo: Foo) -> Bar {
+/// // Bar the foo
+/// foo.into_bar()
+/// }
+///
+/// trait A {
+/// // The A trait
+/// }
+/// ```
+/// can be converted to doc comments. However, the comments in this example:
+/// ```rust
+/// fn foo_bar(foo: Foo /* not bar yet */) -> Bar {
+/// foo.into_bar()
+/// // Nicely done
+/// }
+/// // end of function
+///
+/// struct S {
+/// // The S struct
+/// }
+/// ```
+/// are not allowed to become doc comments.
+fn can_be_doc_comment(comment: &ast::Comment) -> Option<CommentPlacement> {
+ use syntax::SyntaxKind::*;
+
+ // if the comment is not on its own line, then we do not propose anything.
+ match comment.syntax().prev_token() {
+ Some(prev) => {
+ // There was a previous token, now check if it was a newline
+ Whitespace::cast(prev).filter(|w| w.text().contains('\n'))?;
+ }
+ // There is no previous token, this is the start of the file.
+ None => return Some(CommentPlacement::Inner),
+ }
+
+ // check if comment is followed by: `struct`, `trait`, `mod`, `fn`, `type`, `extern crate`, `use`, `const`
+ let parent = comment.syntax().parent();
+ let parent_kind = parent.as_ref().map(|parent| parent.kind());
+ if matches!(
+ parent_kind,
+ Some(STRUCT | TRAIT | MODULE | FN | TYPE_KW | EXTERN_CRATE | USE | CONST)
+ ) {
+ return Some(CommentPlacement::Outer);
+ }
+
+ // check if comment is preceded by: `fn f() {`, `trait T {`, `mod M {`:
+ let third_parent_kind = comment
+ .syntax()
+ .parent()
+ .and_then(|p| p.parent())
+ .and_then(|p| p.parent())
+ .map(|parent| parent.kind());
+ let is_first_item_in_parent = comment
+ .syntax()
+ .siblings_with_tokens(Direction::Prev)
+ .filter_map(|not| not.into_node())
+ .next()
+ .is_none();
+
+ if matches!(parent_kind, Some(STMT_LIST))
+ && is_first_item_in_parent
+ && matches!(third_parent_kind, Some(FN | TRAIT | MODULE))
+ {
+ return Some(CommentPlacement::Inner);
+ }
+
+ None
+}
+
+/// The line -> block assist can be invoked from anywhere within a sequence of line comments.
+/// relevant_line_comments crawls backwards and forwards finding the complete sequence of comments that will
+/// be joined.
+pub(crate) fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
+ // The prefix identifies the kind of comment we're dealing with
+ let prefix = comment.prefix();
+ let same_prefix = |c: &ast::Comment| c.prefix() == prefix;
+
+ // These tokens are allowed to exist between comments
+ let skippable = |not: &SyntaxElement| {
+ not.clone()
+ .into_token()
+ .and_then(Whitespace::cast)
+ .map(|w| !w.spans_multiple_lines())
+ .unwrap_or(false)
+ };
+
+ // Find all preceding comments (in reverse order) that have the same prefix
+ let prev_comments = comment
+ .syntax()
+ .siblings_with_tokens(Direction::Prev)
+ .filter(|s| !skippable(s))
+ .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
+ .take_while(|opt_com| opt_com.is_some())
+ .flatten()
+ .skip(1); // skip the first element so we don't duplicate it in next_comments
+
+ let next_comments = comment
+ .syntax()
+ .siblings_with_tokens(Direction::Next)
+ .filter(|s| !skippable(s))
+ .map(|not| not.into_token().and_then(Comment::cast).filter(same_prefix))
+ .take_while(|opt_com| opt_com.is_some())
+ .flatten();
+
+ let mut comments: Vec<_> = prev_comments.collect();
+ comments.reverse();
+ comments.extend(next_comments);
+ comments
+}
+
+fn line_comments_text_range(comment: &ast::Comment) -> Option<TextRange> {
+ let comments = relevant_line_comments(comment);
+ let first = comments.first()?;
+ let indentation = IndentLevel::from_token(first.syntax());
+ let start =
+ first.syntax().text_range().start().checked_sub((indentation.0 as u32 * 4).into())?;
+ let end = comments.last()?.syntax().text_range().end();
+ Some(TextRange::new(start, end))
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_assist, check_assist_not_applicable};
+
+ use super::*;
+
+ #[test]
+ fn module_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ // such a nice module$0
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+ //! such a nice module
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_line_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ // unseen$0 docs
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /// unseen docs
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_line_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ // unseen$0 docs
+ // make me seen!
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /// unseen docs
+ /// make me seen!
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /// visible$0 docs
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ // visible docs
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /// visible$0 docs
+ /// Hide me!
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ // visible docs
+ // Hide me!
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_line_block_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /* unseen$0 docs */
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /** unseen docs */
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_line_block_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /* unseen$0 docs
+ * make me seen!
+ */
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /** unseen docs
+ * make me seen!
+ */
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /** visible$0 docs */
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /* visible docs */
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+
+ /** visible$0 docs
+ * Hide me!
+ */
+ fn main() {
+ foo();
+ }
+ "#,
+ r#"
+
+ /* visible docs
+ * Hide me!
+ */
+ fn main() {
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ // unseen$0 docs
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ //! unseen docs
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ // unseen$0 docs
+ // make me seen!
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ //! unseen docs
+ //! make me seen!
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ //! visible$0 docs
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ // visible docs
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ //! visible$0 docs
+ //! Hide me!
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ // visible docs
+ // Hide me!
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_block_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ /* unseen$0 docs */
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ /*! unseen docs */
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_block_comment_to_doc() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ /* unseen$0 docs
+ * make me seen!
+ */
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ /*! unseen docs
+ * make me seen!
+ */
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ /*! visible$0 docs */
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ /* visible docs */
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ /*! visible$0 docs
+ * Hide me!
+ */
+ foo();
+ }
+ "#,
+ r#"
+ fn main() {
+ /* visible docs
+ * Hide me!
+ */
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn not_overeager() {
+ check_assist_not_applicable(
+ convert_comment_from_or_to_doc,
+ r#"
+ fn main() {
+ foo();
+ // $0well that settles main
+ }
+ // $1 nicely done
+ "#,
+ );
+ }
+}
diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs
index 0df5e913a5..cbaf03e4d1 100644
--- a/crates/ide-assists/src/lib.rs
+++ b/crates/ide-assists/src/lib.rs
@@ -116,6 +116,7 @@ mod handlers {
mod change_visibility;
mod convert_bool_then;
mod convert_comment_block;
+ mod convert_comment_from_or_to_doc;
mod convert_from_to_tryfrom;
mod convert_integer_literal;
mod convert_into_to_from;
@@ -239,6 +240,7 @@ mod handlers {
convert_bool_then::convert_bool_then_to_if,
convert_bool_then::convert_if_to_bool_then,
convert_comment_block::convert_comment_block,
+ convert_comment_from_or_to_doc::convert_comment_from_or_to_doc,
convert_from_to_tryfrom::convert_from_to_tryfrom,
convert_integer_literal::convert_integer_literal,
convert_into_to_from::convert_into_to_from,
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 937e78f8d7..bd841d72e6 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -346,6 +346,21 @@ pub(crate) fn frobnicate() {}
}
#[test]
+fn doctest_comment_to_doc() {
+ check_doc_test(
+ "comment_to_doc",
+ r#####"
+// Wow what $0a nice function
+// I sure hope this shows up when I hover over it
+"#####,
+ r#####"
+//! Wow what a nice function
+//! I sure hope this shows up when I hover over it
+"#####,
+ )
+}
+
+#[test]
fn doctest_convert_bool_then_to_if() {
check_doc_test(
"convert_bool_then_to_if",