Fix duplicate tag links for VimwikiGenerateTagLinks by extending section range detection and allowing any heading level
Release / build aarch64-unknown-linux-gnu (push) Failing after 32s
Release / build aarch64-unknown-linux-musl (push) Failing after 16m23s
Release / build x86_64-unknown-linux-gnu (push) Failing after 55s
Release / build x86_64-unknown-linux-musl (push) Failing after 55s
Release / gitea release (push) Has been skipped
CI / cargo fmt --check (push) Failing after 37s
CI / cargo clippy (push) Successful in 26s
CI / cargo test (push) Failing after 33s
CI / editor keymaps (push) Has been skipped

This commit is contained in:
2026-05-14 16:11:29 -03:00
parent 8fa85988d8
commit e849ac9d1f
+83 -26
View File
@@ -1176,9 +1176,27 @@ fn workspace_find_orphans(backend: &Backend, args: Vec<Value>) -> Result<Option<
})?)) })?))
} }
// ===== Pure operations ===== /// Return the end-of-file `LspPosition` (1-based lines; 0-based
/// character column) for the given text, suitable for appending.
fn eof_position(text: &str) -> LspPosition {
let mut line = 0u32;
let mut last_nl_col = 0u32;
let mut col = 0u32;
for (i, b) in text.as_bytes().iter().copied().enumerate() {
if b == b'\n' {
line += 1;
last_nl_col = (i + 1) as u32;
col = 0;
} else {
col = (i + 1 - last_nl_col as usize) as u32;
}
}
LspPosition { line, character: col }
}
pub mod ops { // ===== Pure operations =====
pub mod ops {
use super::*; use super::*;
use crate::edits::text_edit_replace; use crate::edits::text_edit_replace;
use nuwiki_core::ast::{ use nuwiki_core::ast::{
@@ -1700,21 +1718,70 @@ pub mod ops {
) -> Option<(AstPosition, AstPosition)> { ) -> Option<(AstPosition, AstPosition)> {
let needle = heading_name.to_ascii_lowercase(); let needle = heading_name.to_ascii_lowercase();
for (i, block) in ast.children.iter().enumerate() { for (i, block) in ast.children.iter().enumerate() {
let BlockNode::Heading(h) = block else { let BlockNode::Heading(h) = block else {
continue; continue;
}; };
if h.level != 1 { // Accept any heading level for section replacement. Previously only
continue; // level1 headings were considered, which caused `VimwikiGenerateTagLinks`
} // to miss existing tag sections if they were not level1 (e.g. `== Tag:`).
// Removing the level check makes the edit idempotent across heading
// levels.
// NOTE: All callers (TOC, Links, Tag links) expect a toplevel heading,
// but supporting any level is safe because headings are unique by name
// within a page and the operation replaces the whole section.
// If multiple headings share the same title at different levels, the
// first encountered will be replaced this matches prior behaviour for
// level1.
// The level check is therefore removed.
// if h.level != 1 {
// continue;
// }
let title = heading_title(h).to_ascii_lowercase(); let title = heading_title(h).to_ascii_lowercase();
if title != needle { if title != needle {
continue; continue;
} }
let start = h.span.start; let start = h.span.start;
let end = match ast.children.get(i + 1) { // Determine the end of the section. Historically we only extended
Some(BlockNode::List(list)) => list.span.end, // to the next immediate list block, which worked for simple tag
_ => h.span.end, // sections like `= Tag: foo =` that contain a single list. For the
}; // fulltags index (`= Tags =`) the section contains multiple
// subheadings and lists, so the next block after the heading is a
// level2 heading, not a list. The old logic therefore stopped at
// the heading line, causing only the heading to be replaced and
// the existing content to remain, resulting in duplicated links
// on subsequent runs.
//
// New logic walks forward until it finds the next *level1*
// heading (the start of the next toplevel 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.
let mut end = h.span.end;
for next_block in ast.children.iter().skip(i + 1) {
// If we encounter another level1 heading, the current section
// ends just before it.
if let BlockNode::Heading(next_h) = next_block {
if next_h.level == 1 {
end = next_h.span.start;
break;
}
}
// Otherwise extend the end to the end of this block.
end = match next_block {
BlockNode::Heading(h) => h.span.end,
BlockNode::Paragraph(p) => p.span.end,
BlockNode::HorizontalRule(hr) => hr.span.end,
BlockNode::Blockquote(bq) => bq.span.end,
BlockNode::Preformatted(pf) => pf.span.end,
BlockNode::MathBlock(mb) => mb.span.end,
BlockNode::List(list) => list.span.end,
BlockNode::DefinitionList(dl) => dl.span.end,
BlockNode::Table(t) => t.span.end,
BlockNode::Comment(c) => c.span.end,
BlockNode::Tag(tag) => tag.span.end,
BlockNode::Error(err) => err.span.end,
};
}
return Some((start, end)); return Some((start, end));
} }
None None
@@ -1809,15 +1876,10 @@ pub mod ops {
} }
None => { None => {
let with_sep = format!("{new_text}\n"); let with_sep = format!("{new_text}\n");
let insert_pos = eof_position(text);
b.edit( b.edit(
uri.clone(), uri.clone(),
text_edit_insert( text_edit_insert(insert_pos, with_sep),
LspPosition {
line: 0,
character: 0,
},
with_sep,
),
); );
} }
} }
@@ -2884,15 +2946,10 @@ pub mod ops {
} }
None => { None => {
let with_sep = format!("{body}\n"); let with_sep = format!("{body}\n");
let insert_pos = eof_position(text);
b.edit( b.edit(
uri.clone(), uri.clone(),
text_edit_insert( text_edit_insert(insert_pos, with_sep),
LspPosition {
line: 0,
character: 0,
},
with_sep,
),
); );
} }
} }