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
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:
@@ -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;
|
// level‑1 headings were considered, which caused `VimwikiGenerateTagLinks`
|
||||||
}
|
// to miss existing tag sections if they were not level‑1 (e.g. `== Tag:`).
|
||||||
|
// Removing the level check makes the edit idempotent across heading
|
||||||
|
// levels.
|
||||||
|
// NOTE: All callers (TOC, Links, Tag links) expect a top‑level 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
|
||||||
|
// level‑1.
|
||||||
|
// 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
|
||||||
};
|
// full‑tags index (`= Tags =`) the section contains multiple
|
||||||
|
// sub‑headings and lists, so the next block after the heading is a
|
||||||
|
// level‑2 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 *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.
|
||||||
|
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 {
|
||||||
|
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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user