about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-01-16 19:11:19 +0000
committerbors <bors@rust-lang.org>2023-01-16 19:11:19 +0000
commit455ef0c806b96420f9fcda053cc0b1d707800b0c (patch)
treefc532abd1fba62056ef8ab5d8354e29545d11cc7
parent1d02474cd2a6b72578c3a1dd98114ed43ae53505 (diff)
parentec06313a6d318771d2bc1f3be81e9b5991cc6ea1 (diff)
downloadrust-455ef0c806b96420f9fcda053cc0b1d707800b0c.tar.gz
rust-455ef0c806b96420f9fcda053cc0b1d707800b0c.zip
Auto merge of #13935 - ModProg:assist_desugar_doc_comment, r=Veykril
Assist: desugar doc-comment

My need for this arose due to wanting to do feature dependent documentation and therefor convert parts of my doc-comments to attributes.

Not sure about the pub-making of the other handlers functions, but I didn't think it made much sense to reimplement them.
-rw-r--r--crates/ide-assists/src/handlers/convert_comment_block.rs4
-rw-r--r--crates/ide-assists/src/handlers/desugar_doc_comment.rs312
-rw-r--r--crates/ide-assists/src/handlers/raw_string.rs23
-rw-r--r--crates/ide-assists/src/lib.rs2
-rw-r--r--crates/ide-assists/src/tests/generated.rs15
-rw-r--r--crates/ide-assists/src/utils.rs21
6 files changed, 353 insertions, 24 deletions
diff --git a/crates/ide-assists/src/handlers/convert_comment_block.rs b/crates/ide-assists/src/handlers/convert_comment_block.rs
index 312cb65abd2..1acd5ee9728 100644
--- a/crates/ide-assists/src/handlers/convert_comment_block.rs
+++ b/crates/ide-assists/src/handlers/convert_comment_block.rs
@@ -107,7 +107,7 @@ fn line_to_block(acc: &mut Assists, comment: ast::Comment) -> Option<()> {
 /// 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.
-fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
+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;
@@ -159,7 +159,7 @@ fn relevant_line_comments(comment: &ast::Comment) -> Vec<Comment> {
 //              */
 //
 // But since such comments aren't idiomatic we're okay with this.
-fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
+pub(crate) fn line_comment_text(indentation: IndentLevel, comm: ast::Comment) -> String {
     let contents_without_prefix = comm.text().strip_prefix(comm.prefix()).unwrap();
     let contents = contents_without_prefix.strip_prefix(' ').unwrap_or(contents_without_prefix);
 
diff --git a/crates/ide-assists/src/handlers/desugar_doc_comment.rs b/crates/ide-assists/src/handlers/desugar_doc_comment.rs
new file mode 100644
index 00000000000..226a5dd9fa8
--- /dev/null
+++ b/crates/ide-assists/src/handlers/desugar_doc_comment.rs
@@ -0,0 +1,312 @@
+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::<ast::Comment>()?;
+    // 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() {
+        if Whitespace::cast(prev).filter(|w| w.text().contains('\n')).is_none() {
+            return None;
+        }
+    }
+
+    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().unwrap().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(|c| line_comment_text(IndentLevel(0), c)).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"###]"####,
+        );
+    }
+}
diff --git a/crates/ide-assists/src/handlers/raw_string.rs b/crates/ide-assists/src/handlers/raw_string.rs
index c9bc25b27a5..01420430bb4 100644
--- a/crates/ide-assists/src/handlers/raw_string.rs
+++ b/crates/ide-assists/src/handlers/raw_string.rs
@@ -2,7 +2,7 @@ use std::borrow::Cow;
 
 use syntax::{ast, ast::IsString, AstToken, TextRange, TextSize};
 
-use crate::{AssistContext, AssistId, AssistKind, Assists};
+use crate::{utils::required_hashes, AssistContext, AssistId, AssistKind, Assists};
 
 // Assist: make_raw_string
 //
@@ -155,16 +155,6 @@ pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<
     })
 }
 
-fn required_hashes(s: &str) -> usize {
-    let mut res = 0usize;
-    for idx in s.match_indices('"').map(|(i, _)| i) {
-        let (_, sub) = s.split_at(idx + 1);
-        let n_hashes = sub.chars().take_while(|c| *c == '#').count();
-        res = res.max(n_hashes + 1)
-    }
-    res
-}
-
 #[cfg(test)]
 mod tests {
     use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
@@ -172,17 +162,6 @@ mod tests {
     use super::*;
 
     #[test]
-    fn test_required_hashes() {
-        assert_eq!(0, required_hashes("abc"));
-        assert_eq!(0, required_hashes("###"));
-        assert_eq!(1, required_hashes("\""));
-        assert_eq!(2, required_hashes("\"#abc"));
-        assert_eq!(0, required_hashes("#abc"));
-        assert_eq!(3, required_hashes("#ab\"##c"));
-        assert_eq!(5, required_hashes("#ab\"##\"####c"));
-    }
-
-    #[test]
     fn make_raw_string_target() {
         check_assist_target(
             make_raw_string,
diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs
index 7813c9f9cbe..546ef96260f 100644
--- a/crates/ide-assists/src/lib.rs
+++ b/crates/ide-assists/src/lib.rs
@@ -126,6 +126,7 @@ mod handlers {
     mod convert_to_guarded_return;
     mod convert_two_arm_bool_match_to_matches_macro;
     mod convert_while_to_loop;
+    mod desugar_doc_comment;
     mod destructure_tuple_binding;
     mod expand_glob_import;
     mod extract_expressions_from_format_string;
@@ -231,6 +232,7 @@ mod handlers {
             convert_tuple_struct_to_named_struct::convert_tuple_struct_to_named_struct,
             convert_two_arm_bool_match_to_matches_macro::convert_two_arm_bool_match_to_matches_macro,
             convert_while_to_loop::convert_while_to_loop,
+            desugar_doc_comment::desugar_doc_comment,
             destructure_tuple_binding::destructure_tuple_binding,
             expand_glob_import::expand_glob_import,
             extract_expressions_from_format_string::extract_expressions_from_format_string,
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 006ae4b3034..16a06b60de9 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -598,6 +598,21 @@ fn main() {
 }
 
 #[test]
+fn doctest_desugar_doc_comment() {
+    check_doc_test(
+        "desugar_doc_comment",
+        r#####"
+/// Multi-line$0
+/// comment
+"#####,
+        r#####"
+#[doc = r"Multi-line
+comment"]
+"#####,
+    )
+}
+
+#[test]
 fn doctest_expand_glob_import() {
     check_doc_test(
         "expand_glob_import",
diff --git a/crates/ide-assists/src/utils.rs b/crates/ide-assists/src/utils.rs
index 7add6606492..63f467bd308 100644
--- a/crates/ide-assists/src/utils.rs
+++ b/crates/ide-assists/src/utils.rs
@@ -758,3 +758,24 @@ pub(crate) fn convert_param_list_to_arg_list(list: ast::ParamList) -> ast::ArgLi
     }
     make::arg_list(args)
 }
+
+/// Calculate the number of hashes required for a raw string containing `s`
+pub(crate) fn required_hashes(s: &str) -> usize {
+    let mut res = 0usize;
+    for idx in s.match_indices('"').map(|(i, _)| i) {
+        let (_, sub) = s.split_at(idx + 1);
+        let n_hashes = sub.chars().take_while(|c| *c == '#').count();
+        res = res.max(n_hashes + 1)
+    }
+    res
+}
+#[test]
+fn test_required_hashes() {
+    assert_eq!(0, required_hashes("abc"));
+    assert_eq!(0, required_hashes("###"));
+    assert_eq!(1, required_hashes("\""));
+    assert_eq!(2, required_hashes("\"#abc"));
+    assert_eq!(0, required_hashes("#abc"));
+    assert_eq!(3, required_hashes("#ab\"##c"));
+    assert_eq!(5, required_hashes("#ab\"##\"####c"));
+}