Unnamed repository; edit this file 'description' to name the repository.
Auto merge of #17253 - ThouCheese:master, r=Veykril
Implement assist to switch between doc and normal comments Hey first PR to rust-analyzer to get my feet wet with the code base. It's an assist to switch a normal comment to a doc comment and back, something I've found myself doing by hand a couple of times. I shamelessly stole `relevant_line_comments` from `convert_comment_block`, because I didn't see any inter-assist imports happening in the files I peeked at so I thought this would be preferable.
bors 2024-05-27
parent 16fd9aa · parent 9e5ff0d · commit f59ca41
-rw-r--r--crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs685
-rw-r--r--crates/ide-assists/src/lib.rs2
-rw-r--r--crates/ide-assists/src/tests/generated.rs15
3 files changed, 702 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..953119fd1f
--- /dev/null
+++ b/crates/ide-assists/src/handlers/convert_comment_from_or_to_doc.rs
@@ -0,0 +1,685 @@
+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 module
+// // I sure hope this shows up when I hover over it
+// ```
+// ->
+// ```
+// //! Wow what a nice module
+// //! 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 => can_be_doc_comment(&comment).and_then(|style| comment_to_doc(acc, comment, style)),
+ }
+}
+
+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 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();
+ let prefix = format!("{indentation}//");
+ relevant_line_comments(&comment)
+ .iter()
+ .map(|comment| comment.text())
+ .flat_map(|text| text.lines())
+ .map(|line| line.replacen(line_start, &prefix, 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 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 indentation = IndentLevel::from_token(comment.syntax());
+ let line_start = match style {
+ CommentPlacement::Inner => format!("{indentation}//!"),
+ CommentPlacement::Outer => format!("{indentation}///"),
+ };
+ relevant_line_comments(&comment)
+ .iter()
+ .map(|comment| comment.text())
+ .flat_map(|text| text.lines())
+ .map(|line| 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
+/// // Brilliant module right here
+///
+/// // Really good right
+/// fn good_function(foo: Foo) -> Bar {
+/// foo.into_bar()
+/// }
+///
+/// // So nice
+/// mod nice_module {}
+/// ```
+/// 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. Moreover, some comments _are_ allowed, but aren't common
+/// style in Rust. For example, the following comments are allowed to be doc comments, but it is not
+/// common style for them to be:
+/// ```rust
+/// fn foo_bar(foo: Foo) -> Bar {
+/// // this could be an inner comment with //!
+/// foo.into_bar()
+/// }
+///
+/// trait T {
+/// // The T struct could also be documented from within
+/// }
+///
+/// mod mymod {
+/// // Modules only normally get inner documentation when they are defined as a separate file.
+/// }
+/// ```
+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` or `const`.
+ let parent = comment.syntax().parent();
+ let par_kind = parent.as_ref().map(|parent| parent.kind());
+ matches!(par_kind, Some(STRUCT | TRAIT | MODULE | FN | TYPE_ALIAS | EXTERN_CRATE | USE | CONST))
+ .then_some(CommentPlacement::Outer)
+}
+
+/// 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_not_applicable(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ // unseen$0 docs
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ //! visible$0 docs
+ foo();
+ }
+ "#,
+ r#"
+ mod mymod {
+ // visible docs
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ //! visible$0 docs
+ //! Hide me!
+ foo();
+ }
+ "#,
+ r#"
+ mod mymod {
+ // visible docs
+ // Hide me!
+ foo();
+ }
+ "#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ /// visible$0 docs
+ /// Hide me!
+ foo();
+ }
+ "#,
+ r#"
+ mod mymod {
+ // visible docs
+ // Hide me!
+ foo();
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn single_inner_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ /*! visible$0 docs */
+ type Int = i32;
+ }
+ "#,
+ r#"
+ mod mymod {
+ /* visible docs */
+ type Int = i32;
+ }
+ "#,
+ );
+ }
+
+ #[test]
+ fn multi_inner_line_block_doc_to_comment() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ /*! visible$0 docs
+ * Hide me!
+ */
+ type Int = i32;
+ }
+ "#,
+ r#"
+ mod mymod {
+ /* visible docs
+ * Hide me!
+ */
+ type Int = i32;
+ }
+ "#,
+ );
+ }
+
+ #[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
+ "#,
+ );
+ }
+
+ #[test]
+ fn all_possible_items() {
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice struct$0 */
+ struct S {}
+ }"#,
+ r#"mod m {
+ /** Nice struct */
+ struct S {}
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice trait$0 */
+ trait T {}
+ }"#,
+ r#"mod m {
+ /** Nice trait */
+ trait T {}
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice module$0 */
+ mod module {}
+ }"#,
+ r#"mod m {
+ /** Nice module */
+ mod module {}
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice function$0 */
+ fn function() {}
+ }"#,
+ r#"mod m {
+ /** Nice function */
+ fn function() {}
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice type$0 */
+ type Type Int = i32;
+ }"#,
+ r#"mod m {
+ /** Nice type */
+ type Type Int = i32;
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice crate$0 */
+ extern crate rust_analyzer;
+ }"#,
+ r#"mod m {
+ /** Nice crate */
+ extern crate rust_analyzer;
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice import$0 */
+ use ide_assists::convert_comment_from_or_to_doc::tests
+ }"#,
+ r#"mod m {
+ /** Nice import */
+ use ide_assists::convert_comment_from_or_to_doc::tests
+ }"#,
+ );
+ check_assist(
+ convert_comment_from_or_to_doc,
+ r#"mod m {
+ /* Nice constant$0 */
+ const CONST: &str = "very const";
+ }"#,
+ r#"mod m {
+ /** Nice constant */
+ const CONST: &str = "very const";
+ }"#,
+ );
+ }
+
+ #[test]
+ fn no_inner_comments() {
+ check_assist_not_applicable(
+ convert_comment_from_or_to_doc,
+ r#"
+ mod mymod {
+ // aaa$0aa
+ }
+ "#,
+ );
+ }
+}
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..5ecce3cbb6 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 module
+// I sure hope this shows up when I hover over it
+"#####,
+ r#####"
+//! Wow what a nice module
+//! 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",