fix(lsp): section-range termination is level-aware (caption audit follow-up)
The post-captions audit found find_section_range terminated a generated section only at the next *level-1* heading. With a custom non-1 *_header_level (e.g. a level-2 Generated Tags index) a following same-level section could be absorbed into the replaced span. Terminate at the next heading of level <= the matched section's level instead, which keeps the deeper `level+1` sub-groups inside while stopping at a sibling/parent. Test: find_section_range_stops_at_same_level_sibling. 0 failures, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1886,16 +1886,19 @@ pub mod ops {
|
|||||||
// the existing content to remain, resulting in duplicated links
|
// the existing content to remain, resulting in duplicated links
|
||||||
// on subsequent runs.
|
// on subsequent runs.
|
||||||
//
|
//
|
||||||
// New logic walks forward until it finds the next *level‑1*
|
// New logic walks forward until it finds the next heading at the
|
||||||
// heading (the start of the next top‑level section) or reaches the
|
// same level as (or shallower than) the matched section heading —
|
||||||
// end of the document. The span end is set to the start of that
|
// i.e. the next sibling/parent section — or reaches the end of the
|
||||||
// next heading (exclusive) or to the end of the last block.
|
// 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;
|
let mut end = h.span.end;
|
||||||
for next_block in ast.children.iter().skip(i + 1) {
|
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 let BlockNode::Heading(next_h) = next_block {
|
||||||
if next_h.level == 1 {
|
if next_h.level <= section_level {
|
||||||
end = next_h.span.start;
|
end = next_h.span.start;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,23 @@ fn build_toc_text_with_no_headings_is_just_the_heading() {
|
|||||||
assert_eq!(out, "= Contents =\n");
|
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]
|
#[test]
|
||||||
fn captions_honour_custom_header_and_level() {
|
fn captions_honour_custom_header_and_level() {
|
||||||
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
// toc_header="Table of Contents", level 2 → `== Table of Contents ==`.
|
||||||
|
|||||||
Reference in New Issue
Block a user