diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index dd59d9e70e4..989a63c09c4 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -211,12 +211,29 @@ fn has_test_related_attribute(fn_def: &ast::Fn) -> bool { .any(|attribute_text| attribute_text.contains("test")) } +const RUSTDOC_FENCE: &str = "```"; +const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE: &[&str] = + &["", "rust", "should_panic", "edition2015", "edition2018"]; + fn has_runnable_doc_test(fn_def: &ast::Fn) -> bool { fn_def.doc_comment_text().map_or(false, |comments_text| { - comments_text.contains("```") - && !comments_text.contains("```ignore") - && !comments_text.contains("```no_run") - && !comments_text.contains("```compile_fail") + let mut in_code_block = false; + + for line in comments_text.lines() { + if let Some(header) = line.strip_prefix(RUSTDOC_FENCE) { + in_code_block = !in_code_block; + + if in_code_block + && header + .split(',') + .all(|sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE.contains(&sub.trim())) + { + return true; + } + } + } + + false }) } @@ -421,7 +438,21 @@ fn main() {} /// ``` /// let x = 5; /// ``` -fn foo() {} +fn should_have_runnable() {} + +/// ```edition2018 +/// let x = 5; +/// ``` +fn should_have_runnable_1() {} + +/// ``` +/// let z = 55; +/// ``` +/// +/// ```ignore +/// let z = 56; +/// ``` +fn should_have_runnable_2() {} /// ```no_run /// let z = 55; @@ -437,8 +468,27 @@ fn should_have_no_runnable_2() {} /// let z = 55; /// ``` fn should_have_no_runnable_3() {} + +/// ```text +/// arbitrary plain text +/// ``` +fn should_have_no_runnable_4() {} + +/// ```text +/// arbitrary plain text +/// ``` +/// +/// ```sh +/// $ shell code +/// ``` +fn should_have_no_runnable_5() {} + +/// ```rust,no_run +/// let z = 55; +/// ``` +fn should_have_no_runnable_6() {} "#, - &[&BIN, &DOCTEST], + &[&BIN, &DOCTEST, &DOCTEST, &DOCTEST], expect![[r#" [ Runnable { @@ -464,9 +514,9 @@ fn should_have_no_runnable_3() {} file_id: FileId( 1, ), - full_range: 15..57, + full_range: 15..74, focus_range: None, - name: "foo", + name: "should_have_runnable", kind: FN, container_name: None, description: None, @@ -474,7 +524,47 @@ fn should_have_no_runnable_3() {} }, kind: DocTest { test_id: Path( - "foo", + "should_have_runnable", + ), + }, + cfg_exprs: [], + }, + Runnable { + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 76..148, + focus_range: None, + name: "should_have_runnable_1", + kind: FN, + container_name: None, + description: None, + docs: None, + }, + kind: DocTest { + test_id: Path( + "should_have_runnable_1", + ), + }, + cfg_exprs: [], + }, + Runnable { + nav: NavigationTarget { + file_id: FileId( + 1, + ), + full_range: 150..254, + focus_range: None, + name: "should_have_runnable_2", + kind: FN, + container_name: None, + description: None, + docs: None, + }, + kind: DocTest { + test_id: Path( + "should_have_runnable_2", ), }, cfg_exprs: [], diff --git a/crates/rust-analyzer/src/markdown.rs b/crates/rust-analyzer/src/markdown.rs index 76bef45ccc2..968ea55f0cc 100644 --- a/crates/rust-analyzer/src/markdown.rs +++ b/crates/rust-analyzer/src/markdown.rs @@ -1,22 +1,32 @@ //! Transforms markdown +const RUSTDOC_FENCE: &str = "```"; +const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUST_SPECIFIC: &[&str] = + &["", "rust", "should_panic", "ignore", "no_run", "compile_fail", "edition2015", "edition2018"]; + pub(crate) fn format_docs(src: &str) -> String { let mut processed_lines = Vec::new(); let mut in_code_block = false; - for line in src.lines() { - if in_code_block && code_line_ignored_by_rustdoc(line) { + let mut is_rust = false; + + for mut line in src.lines() { + if in_code_block && is_rust && code_line_ignored_by_rustdoc(line) { continue; } - if line.starts_with("```") { - in_code_block ^= true - } + if let Some(header) = line.strip_prefix(RUSTDOC_FENCE) { + in_code_block ^= true; - let line = if in_code_block && line.starts_with("```") && !line.contains("rust") { - "```rust" - } else { - line - }; + if in_code_block { + is_rust = header + .split(',') + .all(|sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUST_SPECIFIC.contains(&sub.trim())); + + if is_rust { + line = "```rust"; + } + } + } processed_lines.push(line); } @@ -38,6 +48,30 @@ mod tests { assert_eq!(format_docs(comment), "```rust\nfn some_rust() {}\n```"); } + #[test] + fn test_format_docs_handles_plain_text() { + let comment = "```text\nthis is plain text\n```"; + assert_eq!(format_docs(comment), "```text\nthis is plain text\n```"); + } + + #[test] + fn test_format_docs_handles_non_rust() { + let comment = "```sh\nsupposedly shell code\n```"; + assert_eq!(format_docs(comment), "```sh\nsupposedly shell code\n```"); + } + + #[test] + fn test_format_docs_handles_rust_alias() { + let comment = "```ignore\nlet z = 55;\n```"; + assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); + } + + #[test] + fn test_format_docs_handles_complex_code_block_attrs() { + let comment = "```rust,no_run\nlet z = 55;\n```"; + assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); + } + #[test] fn test_format_docs_skips_comments_in_rust_block() { let comment = @@ -45,6 +79,16 @@ mod tests { assert_eq!(format_docs(comment), "```rust\n#stay1\nstay2\n```"); } + #[test] + fn test_format_docs_does_not_skip_lines_if_plain_text() { + let comment = + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```"; + assert_eq!( + format_docs(comment), + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```", + ); + } + #[test] fn test_format_docs_keeps_comments_outside_of_rust_block() { let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t"; @@ -72,4 +116,21 @@ let a = 1; "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```" ); } + + #[test] + fn test_code_blocks_in_comments_marked_as_text() { + let comment = r#"```text +filler +text +``` +Some comment. +``` +let a = 1; +```"#; + + assert_eq!( + format_docs(comment), + "```text\nfiller\ntext\n```\nSome comment.\n```rust\nlet a = 1;\n```" + ); + } }