fix(review): close all 2026-06-03 codebase-review findings (R1-R18)
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 32s
CI / cargo test (push) Successful in 38s
CI / editor keymaps (push) Successful in 1m33s

Resolves the 18 findings from the parallel codebase review, tracked in
development/vimwiki-gap.md.

Correctness / perf:
- Wiki.config -> Arc<WikiConfig> so cloning a Wiki is a refcount bump (R5)
- WorkspaceIndex::remove is no longer O(n^2): a per-source contributions
  map limits the scan to buckets the source actually wrote into (R6)
- render_color now expands color_tag_template (__STYLE__/__CONTENT__),
  consuming the previously-dead field; ColorNode documented as an
  extension point; 3 renderer tests added (R3/R4)
- wiki_root_for returns empty/nil on no-match instead of falling back to
  the first wiki (R2); auto_header honours links_space_char (R7)

Cleanup / dedup:
- Remove dead #[allow(dead_code)] stubs + uncalled pub helpers, narrow
  imports (R10/R11)
- Dedup span_of_inline x3 -> InlineNode::span() (R12)
- diary_step single read lock; page_captions single pass (R13)
- Lua auto_header loop -> wiki_list(); detect_current_symbol cleanup
  (R14/R18)

Client / docs:
- :VimwikiNormalizeLink Vim cmds -> <q-args> (R17); ftplugin header fix
  (R16); vars.vim multi-wiki limitation documented (R15)
- Document 19 config options in README.md + doc/nuwiki.txt; fix
  list_margin/shiftwidth doc and stale comments (R1/R9)
- R8 investigated, confirmed not a real bug (documented)

Verified: Neovim harness 307, Vim harness 301/18/21, Rust suite 568, all
0 failed; clippy clean; fresh parallel-agent audit found no regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 14:13:55 +00:00
parent e319b4a935
commit 3b92b11948
25 changed files with 410 additions and 236 deletions
+45 -30
View File
@@ -123,6 +123,18 @@ pub struct WorkspaceIndex {
/// the nav handlers (tag-as-anchor `definition`, `workspace/symbol`
/// inclusion) and the `nuwiki.tags.search` command.
pub tags_by_name: HashMap<String, Vec<TagOccurrence>>,
/// For each source URI, the backlink-bucket keys and tag-bucket names it
/// contributed. Lets `remove` touch only the affected buckets instead of
/// scanning every bucket in the workspace (O(touched), not O(total)).
contributions: HashMap<Url, SourceContributions>,
}
/// The bucket keys a single source page wrote into, so they can be undone
/// in `remove` without a full scan. See [`WorkspaceIndex::contributions`].
#[derive(Debug, Default)]
struct SourceContributions {
backlink_keys: Vec<String>,
tag_names: Vec<String>,
}
impl WorkspaceIndex {
@@ -175,15 +187,20 @@ impl WorkspaceIndex {
};
index_blocks(&ast.children, &mut page);
let mut contrib = SourceContributions::default();
let ext = self.file_extension.as_deref();
for link in &page.outgoing {
if let Some(tgt) = &link.target_page {
let key = canonical_link_name(tgt, &name, link.is_absolute, ext);
self.backlinks.entry(key).or_default().push(Backlink {
source_uri: uri.clone(),
source_span: link.span,
target_anchor: link.anchor.clone(),
});
self.backlinks
.entry(key.clone())
.or_default()
.push(Backlink {
source_uri: uri.clone(),
source_span: link.span,
target_anchor: link.anchor.clone(),
});
contrib.backlink_keys.push(key);
}
}
@@ -197,7 +214,9 @@ impl WorkspaceIndex {
span: tag.span,
scope: tag.scope,
});
contrib.tag_names.push(tag.name.clone());
}
self.contributions.insert(uri.clone(), contrib);
self.pages_by_name.insert(name, uri.clone());
self.pages_by_uri.insert(uri, page);
@@ -207,14 +226,26 @@ impl WorkspaceIndex {
if let Some(page) = self.pages_by_uri.remove(uri) {
self.pages_by_name.remove(&page.name);
}
for entries in self.backlinks.values_mut() {
entries.retain(|b| &b.source_uri != uri);
// Only revisit the buckets this source actually wrote into, instead
// of scanning every backlink/tag bucket in the workspace.
if let Some(contrib) = self.contributions.remove(uri) {
for key in contrib.backlink_keys {
if let Some(entries) = self.backlinks.get_mut(&key) {
entries.retain(|b| &b.source_uri != uri);
if entries.is_empty() {
self.backlinks.remove(&key);
}
}
}
for name in contrib.tag_names {
if let Some(entries) = self.tags_by_name.get_mut(&name) {
entries.retain(|t| &t.uri != uri);
if entries.is_empty() {
self.tags_by_name.remove(&name);
}
}
}
}
self.backlinks.retain(|_, v| !v.is_empty());
for entries in self.tags_by_name.values_mut() {
entries.retain(|t| &t.uri != uri);
}
self.tags_by_name.retain(|_, v| !v.is_empty());
}
/// All occurrences of a tag across the workspace. Empty slice when the
@@ -306,21 +337,6 @@ impl WorkspaceIndex {
}
}
/// Return a parsed [`DiaryDate`] when `uri` is a daily diary entry —
/// the path lives under `<root>/<diary_rel_path>/` and its stem parses
/// as `YYYY-MM-DD`. For weekly/monthly/yearly entries see
/// [`diary_period_for_uri`].
pub fn diary_date_for_uri(
uri: &Url,
root: Option<&Path>,
diary_rel_path: Option<&str>,
) -> Option<DiaryDate> {
match diary_period_for_uri(uri, root, diary_rel_path)? {
nuwiki_core::date::DiaryPeriod::Day(d) => Some(d),
_ => None,
}
}
/// Return a parsed [`DiaryPeriod`] when `uri` is a diary entry at any of
/// the four supported frequencies. Recognises:
///
@@ -329,9 +345,8 @@ pub fn diary_date_for_uri(
/// `YYYY-MM` → Month
/// `YYYY` → Year
///
/// As with `diary_date_for_uri`, the path must sit under
/// `<root>/<diary_rel_path>/` to be accepted; without a `root` we accept
/// any stem (so ad-hoc test setups still work).
/// The path must sit under `<root>/<diary_rel_path>/` to be accepted;
/// without a `root` we accept any stem (so ad-hoc test setups still work).
pub fn diary_period_for_uri(
uri: &Url,
root: Option<&Path>,