From 4b1614af1ff8c6cb062210646c78edb6a3075c02 Mon Sep 17 00:00:00 2001 From: Park Jong Hyun <81476595+fhrmrk@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:29:16 +0900 Subject: [PATCH] fix(spell): close a Markdown code fence only on a matching fence (#986) Any ``` line closed an open fence, so a ```` block showing a ``` block ended early and the inner code was spell checked as prose. Fences now close as in CommonMark: the same character, at least as long as the opening fence, and no info string. Co-authored-by: fhrmrk <81476595+fhrmrk@users.noreply.github.com> --- crates/ide-spell/src/lib.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/ide-spell/src/lib.rs b/crates/ide-spell/src/lib.rs index 330a55a68..0593ec591 100644 --- a/crates/ide-spell/src/lib.rs +++ b/crates/ide-spell/src/lib.rs @@ -422,15 +422,22 @@ fn capitalize(s: &str) -> String { /// blocks and inline code spans. pub fn prose_regions(text: &str) -> Vec { let mut out = Vec::new(); - let mut fence: Option<&str> = None; + // The open fence: its character and length. + let mut fence: Option<(char, usize)> = None; let mut offset = 0; for line in text.split_inclusive('\n') { let start = offset; offset += line.len(); let trimmed = line.trim_start(); - let marker = ["```", "~~~"].into_iter().find(|m| trimmed.starts_with(m)); + let marker = ['`', '~'].into_iter().find_map(|c| { + let n = trimmed.len() - trimmed.trim_start_matches(c).len(); + (n >= 3).then_some((c, n)) + }); match (fence, marker) { - (Some(open), Some(m)) if m == open => { + // CommonMark: a closing fence uses the same character, is at least as + // long as the opening one and has no info string, so a ```` fence can + // show a ``` block. + (Some((open, open_len)), Some((c, n))) if c == open && n >= open_len && trimmed[n..].trim().is_empty() => { fence = None; continue; } @@ -721,6 +728,14 @@ mod tests { assert_eq!(spans, ["Some prose ", " here\n", "more\n"]); } + #[test] + fn markdown_fences_close_on_a_matching_fence_only() { + let text = "Example:\n````markdown\n```rust\nfn wrng() {}\n```\n````\nmore\n~~~\n```\nnot clsed\n~~~~\nend\n"; + let regions = prose_regions(text); + let spans: Vec<_> = regions.iter().map(|r| &text[r.start..r.end]).collect(); + assert_eq!(spans, ["Example:\n", "more\n", "end\n"]); + } + #[test] fn cancellation() { let s = Speller::default();