Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/ide-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ either.workspace = true
itertools.workspace = true
arrayvec.workspace = true
memchr = "2.7.5"
pulldown-cmark.workspace = true
salsa.workspace = true
salsa-macros.workspace = true
triomphe.workspace = true
Expand Down
100 changes: 75 additions & 25 deletions crates/ide-db/src/rust_doc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Rustdoc specific doc comment handling

use std::ops::Range;

use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag};

use crate::documentation::Documentation;

// stripped down version of https://github.com/rust-lang/rust/blob/392ba2ba1a7d6c542d2459fb8133bebf62a4a423/src/librustdoc/html/markdown.rs#L810-L933
Expand Down Expand Up @@ -31,45 +35,79 @@ pub fn is_rust_fence(s: &str) -> bool {
!seen_other_tags || seen_rust_tags
}

const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"];

pub fn format_docs(src: &Documentation<'_>) -> String {
format_docs_(src.as_str())
}

fn format_docs_(src: &str) -> String {
let mut processed_lines = Vec::new();
let mut in_code_block = false;
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 let Some(header) = RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence))
{
in_code_block ^= true;
// Use `pulldown-cmark` to delimit fenced code blocks per the CommonMark
// spec, so that mixed fence characters (``` vs ~~~) and variable fence
// lengths are handled correctly. Text outside fenced code blocks is
// emitted byte-for-byte; inside, the info string is rewritten to `rust`
// for Rust-flavored fences, hidden `#` lines are stripped, and `##` is
// de-escaped to `#`.
let blocks = collect_fenced_blocks(src);
let mut out = String::with_capacity(src.len());
let mut cursor: usize = 0;
for FencedBlock { range, is_rust } in &blocks {
out.push_str(&src[cursor..range.start]);
write_block(&src[range.clone()], *is_rust, &mut out);
cursor = range.end;
}
out.push_str(&src[cursor..]);
out
}

if in_code_block {
is_rust = is_rust_fence(header);
struct FencedBlock {
range: Range<usize>,
is_rust: bool,
}

if is_rust {
line = "```rust";
fn collect_fenced_blocks(src: &str) -> Vec<FencedBlock> {
let mut blocks = Vec::new();
let mut pending: Option<(usize, bool)> = None;
for (event, range) in Parser::new(src).into_offset_iter() {
match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
pending = Some((range.start, is_rust_fence(&info)));
}
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(_))) => {
if let Some((start, is_rust)) = pending.take() {
blocks.push(FencedBlock { range: start..range.end, is_rust });
}
}
_ => {}
}
}
blocks
}

if in_code_block {
let trimmed = line.trim_start();
if is_rust && trimmed.starts_with("##") {
line = &trimmed[1..];
}
fn write_block(block: &str, is_rust: bool, out: &mut String) {
let mut lines = block.split('\n');
if let Some(first_line) = lines.next() {
let fence_run_len = first_line.bytes().take_while(|&b| b == b'`' || b == b'~').count();
if is_rust && fence_run_len > 0 {
out.push_str(&first_line[..fence_run_len]);
out.push_str("rust");
} else {
out.push_str(first_line);
}
}

processed_lines.push(line);
for line in lines {
if is_rust && code_line_ignored_by_rustdoc(line) {
continue;
}
out.push('\n');
let trimmed = line.trim_start();
if is_rust && trimmed.starts_with("##") {
let leading_ws_len = line.len() - trimmed.len();
out.push_str(&line[..leading_ws_len]);
out.push_str(&trimmed[1..]);
} else {
out.push_str(line);
}
}
processed_lines.join("\n")
}

fn code_line_ignored_by_rustdoc(line: &str) -> bool {
Expand Down Expand Up @@ -196,4 +234,16 @@ let s = "foo
```"#;
assert_eq!(format_docs_(comment), "```markdown\n## A second-level heading\n```");
}

#[test]
fn test_format_docs_preserves_tilde_inside_backtick_fence() {
let comment = "```text\n~~~\n```";
assert_eq!(format_docs_(comment), "```text\n~~~\n```");
}

#[test]
fn test_format_docs_preserves_backtick_inside_tilde_fence() {
let comment = "~~~text\n```\n~~~";
assert_eq!(format_docs_(comment), "~~~text\n```\n~~~");
}
}
Loading