diff --git a/crates/nuwiki-lsp/src/commands.rs b/crates/nuwiki-lsp/src/commands.rs index 823c568..e7b17ec 100644 --- a/crates/nuwiki-lsp/src/commands.rs +++ b/crates/nuwiki-lsp/src/commands.rs @@ -1886,16 +1886,19 @@ pub mod ops { // the existing content to remain, resulting in duplicated links // on subsequent runs. // - // New logic walks forward until it finds the next *level‑1* - // heading (the start of the next top‑level section) or reaches the - // end of the document. The span end is set to the start of that - // next heading (exclusive) or to the end of the last block. + // New logic walks forward until it finds the next heading at the + // same level as (or shallower than) the matched section heading — + // i.e. the next sibling/parent section — or reaches the end of the + // document. Using `<= section_level` (rather than a hard-coded 1) + // keeps deeper sub-headings (e.g. the `level+1` per-tag groups of a + // `Generated Tags` index) inside the replaced span at any configured + // `*_header_level`. The span end is the start of that next heading + // (exclusive) or the end of the last block. + let section_level = h.level; let mut end = h.span.end; for next_block in ast.children.iter().skip(i + 1) { - // If we encounter another level‑1 heading, the current section - // ends just before it. if let BlockNode::Heading(next_h) = next_block { - if next_h.level == 1 { + if next_h.level <= section_level { end = next_h.span.start; break; } diff --git a/crates/nuwiki-lsp/tests/link_health.rs b/crates/nuwiki-lsp/tests/link_health.rs index f413793..f74c637 100644 --- a/crates/nuwiki-lsp/tests/link_health.rs +++ b/crates/nuwiki-lsp/tests/link_health.rs @@ -305,6 +305,23 @@ fn build_toc_text_with_no_headings_is_just_the_heading() { assert_eq!(out, "= Contents =\n"); } +#[test] +fn find_section_range_stops_at_same_level_sibling() { + // A non-level-1 generated section (e.g. a level-2 tags index) must not + // swallow the following same-level section when regenerated. + let src = "== Foo ==\n- a\n== Bar ==\n- b\n"; + let ast = parse(src); + let (start, end) = ops::find_section_range(&ast, "Foo").expect("section found"); + let bar_off = src.find("== Bar ==").unwrap(); + assert_eq!(start.offset, 0); + assert!( + end.offset <= bar_off, + "section absorbed the sibling: end={} bar={}", + end.offset, + bar_off + ); +} + #[test] fn captions_honour_custom_header_and_level() { // toc_header="Table of Contents", level 2 → `== Table of Contents ==`.