parity(1): vimwiki per-wiki config keys, table colspan/rowspan,
named link commands, mouse maps
Parity audit Cluster 1 — small wins.
WikiConfig (server) extended with 14 vimwiki globals:
- `index` (`g:vimwiki_index`), wired into `:VimwikiIndex` so users
with a custom index page stem (e.g. `home`) get the right file.
- `diary_frequency`, `diary_start_week_day`, `diary_caption_level`,
`diary_sort`, `diary_header` — diary keys (weekly/monthly/yearly
semantics land in Cluster 4; the keys parse now so config
migration doesn't need to wait).
- `maxhi`, `listsyms`, `listsyms_propagate`, `list_margin`,
`links_space_char`, `nested_syntaxes`, `auto_toc` — list +
highlight knobs the renderer/handlers can consult.
All keys threaded through `RawWiki` → `WikiConfig::from(RawWiki)`,
defaults centralised in `wiki_defaults()` so `empty()`, `from_root()`,
the legacy single-wiki path, and the `RawWiki` impl stay in sync.
Lua front-door (`lua/nuwiki/config.lua`) now documents the multi-wiki
`wikis = {…}` shape with every accepted per-wiki key — users who
prefer Lua tables no longer have to round-trip through
`init_options` raw JSON.
HTML renderer (`crates/nuwiki-core/src/render/html.rs`):
- New `table_spans()` pre-pass resolves vimwiki's `>` (col_span) and
`\\/` (row_span) continuation markers into proper HTML
`colspan="N"` / `rowspan="N"` attributes on the lead cell. Continuation
cells emit nothing, matching what browsers expect.
- The old behaviour (emitting `class="col-span"` / `class="row-span"`
as visual markers) is gone — replaced with real merging so the
rendered HTML matches the source semantics.
Named ex-commands:
- `:VimwikiNextLink` / `:VimwikiPrevLink` — were keymapped only
(`<Tab>`/`<S-Tab>`); now have explicit `:` commands in both editor
paths, dispatching to new `nuwiki.commands.link_next` / `link_prev`
pure-Vim/Lua helpers.
- `:VimwikiBaddLink` — adds the link target's file to the buffer
list without switching focus. Goes through
`textDocument/definition` and runs `:badd <fname>` on the resolved
URI.
Mouse maps (opt-in via `mappings.mouse = true` in Lua or
`g:nuwiki_mouse_mappings = 1` in Vim):
- `<2-LeftMouse>` follows, `<S-2-LeftMouse>` / `<C-2-LeftMouse>`
follow in split/vsplit, `<MiddleMouse>` adds to buflist,
`<RightMouse>` goes back. Mirrors upstream vimwiki's defaults.
Tests: 7 new in `parity_cluster_1.rs` covering the per-wiki config
defaults + raw JSON parse, the legacy-single-wiki path, colspan
folding into a lead cell with no leftover class markers, rowspan
across rows, no-attrs when the table has no spans, and a sanity
paragraph render. Total 421 Rust tests pass; clippy clean; both
keymap harnesses still green (20 nvim + 12 vim).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+193
-19
@@ -27,6 +27,10 @@ pub struct WikiConfig {
|
||||
pub root: PathBuf,
|
||||
pub file_extension: String,
|
||||
pub syntax: String,
|
||||
/// Stem of the wiki's index page (vimwiki's `index`, default
|
||||
/// `"index"`). `<root>/<index><file_extension>` is what
|
||||
/// `:VimwikiIndex` opens.
|
||||
pub index: String,
|
||||
/// Subdirectory under `root` where diary entries live. Matches
|
||||
/// vimwiki's `g:vimwiki_diary_rel_path` (default `"diary"`).
|
||||
pub diary_rel_path: String,
|
||||
@@ -34,6 +38,42 @@ pub struct WikiConfig {
|
||||
/// Matches vimwiki's `g:vimwiki_diary_index` (default `"diary"`).
|
||||
/// The full path is `<root>/<diary_rel_path>/<diary_index>.wiki`.
|
||||
pub diary_index: String,
|
||||
/// `daily`/`weekly`/`monthly`/`yearly`. Picks the date format the
|
||||
/// diary commands use. v1.1 ships `daily`; the others fall through
|
||||
/// to vimwiki-style names but the commands are no-ops until
|
||||
/// Cluster 4 lands.
|
||||
pub diary_frequency: String,
|
||||
/// First day of the week (`monday`..`sunday`). Used by weekly
|
||||
/// diary entry naming.
|
||||
pub diary_start_week_day: String,
|
||||
/// Caption level for the diary index page headings (1 = h1).
|
||||
pub diary_caption_level: u8,
|
||||
/// Newest-first (`desc`) or oldest-first (`asc`) in the diary
|
||||
/// index.
|
||||
pub diary_sort: String,
|
||||
/// H1 text for the diary index page.
|
||||
pub diary_header: String,
|
||||
/// Maximum heading level treated as a *navigation* anchor — beyond
|
||||
/// it the LSP doesn't synthesise an anchor symbol. Matches
|
||||
/// vimwiki's `g:vimwiki_maxhi` (default `1`).
|
||||
pub maxhi: u8,
|
||||
/// Vimwiki's `g:vimwiki_listsyms`. The 5-character string maps
|
||||
/// fractional progress states for checkboxes; the i-th char (0..=4)
|
||||
/// is the rendered marker for state `i / 4`. Default: `" .oOX"`.
|
||||
pub listsyms: String,
|
||||
/// When toggling a parent list item, also cascade to descendants.
|
||||
pub listsyms_propagate: bool,
|
||||
/// `-1` keeps tight lists; positive integers indent list items by
|
||||
/// that many extra columns. Layout hint for the renderer.
|
||||
pub list_margin: i32,
|
||||
/// Character used in place of literal spaces inside wikilink
|
||||
/// targets when writing to disk (`_` by default).
|
||||
pub links_space_char: String,
|
||||
/// `nested_syntaxes` — code-block language → editor filetype map
|
||||
/// used for syntax-highlighting fenced blocks. Empty by default.
|
||||
pub nested_syntaxes: std::collections::HashMap<String, String>,
|
||||
/// Rebuild the page's TOC on save when set.
|
||||
pub auto_toc: bool,
|
||||
/// Phase 17 HTML export options. See SPEC §12.8.
|
||||
pub html: HtmlConfig,
|
||||
}
|
||||
@@ -119,19 +159,34 @@ impl WikiConfig {
|
||||
/// `initializationOptions` nor a workspace root. `name` is empty;
|
||||
/// callers shouldn't depend on it for routing.
|
||||
pub fn empty() -> Self {
|
||||
let d = wiki_defaults();
|
||||
Self {
|
||||
name: String::new(),
|
||||
root: PathBuf::new(),
|
||||
file_extension: ".wiki".into(),
|
||||
syntax: "vimwiki".into(),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
index: d.index,
|
||||
diary_rel_path: d.diary_rel_path,
|
||||
diary_index: d.diary_index,
|
||||
diary_frequency: d.diary_frequency,
|
||||
diary_start_week_day: d.diary_start_week_day,
|
||||
diary_caption_level: d.diary_caption_level,
|
||||
diary_sort: d.diary_sort,
|
||||
diary_header: d.diary_header,
|
||||
maxhi: d.maxhi,
|
||||
listsyms: d.listsyms,
|
||||
listsyms_propagate: d.listsyms_propagate,
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
nested_syntaxes: d.nested_syntaxes,
|
||||
auto_toc: d.auto_toc,
|
||||
html: HtmlConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_root(root: PathBuf) -> Self {
|
||||
let html = HtmlConfig::from_root(&root);
|
||||
let d = wiki_defaults();
|
||||
Self {
|
||||
name: root
|
||||
.file_name()
|
||||
@@ -140,8 +195,21 @@ impl WikiConfig {
|
||||
root,
|
||||
file_extension: ".wiki".into(),
|
||||
syntax: "vimwiki".into(),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
index: d.index,
|
||||
diary_rel_path: d.diary_rel_path,
|
||||
diary_index: d.diary_index,
|
||||
diary_frequency: d.diary_frequency,
|
||||
diary_start_week_day: d.diary_start_week_day,
|
||||
diary_caption_level: d.diary_caption_level,
|
||||
diary_sort: d.diary_sort,
|
||||
diary_header: d.diary_header,
|
||||
maxhi: d.maxhi,
|
||||
listsyms: d.listsyms,
|
||||
listsyms_propagate: d.listsyms_propagate,
|
||||
list_margin: d.list_margin,
|
||||
links_space_char: d.links_space_char,
|
||||
nested_syntaxes: d.nested_syntaxes,
|
||||
auto_toc: d.auto_toc,
|
||||
html,
|
||||
}
|
||||
}
|
||||
@@ -174,6 +242,75 @@ fn default_diary_index() -> String {
|
||||
"diary".to_string()
|
||||
}
|
||||
|
||||
fn default_index() -> String {
|
||||
"index".to_string()
|
||||
}
|
||||
|
||||
fn default_diary_frequency() -> String {
|
||||
"daily".to_string()
|
||||
}
|
||||
|
||||
fn default_diary_start_week_day() -> String {
|
||||
"monday".to_string()
|
||||
}
|
||||
|
||||
fn default_diary_sort() -> String {
|
||||
"desc".to_string()
|
||||
}
|
||||
|
||||
fn default_diary_header() -> String {
|
||||
"Diary".to_string()
|
||||
}
|
||||
|
||||
fn default_listsyms() -> String {
|
||||
" .oOX".to_string()
|
||||
}
|
||||
|
||||
fn default_links_space_char() -> String {
|
||||
" ".to_string()
|
||||
}
|
||||
|
||||
/// Fill in the v1.1 per-wiki keys that none of the constructors care
|
||||
/// about. Centralising the defaults here keeps `empty()`, `from_root()`,
|
||||
/// the legacy single-wiki path, and `From<RawWiki>` in sync.
|
||||
fn wiki_defaults() -> WikiDefaults {
|
||||
WikiDefaults {
|
||||
index: default_index(),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
diary_frequency: default_diary_frequency(),
|
||||
diary_start_week_day: default_diary_start_week_day(),
|
||||
diary_caption_level: 1,
|
||||
diary_sort: default_diary_sort(),
|
||||
diary_header: default_diary_header(),
|
||||
maxhi: 1,
|
||||
listsyms: default_listsyms(),
|
||||
listsyms_propagate: true,
|
||||
list_margin: -1,
|
||||
links_space_char: default_links_space_char(),
|
||||
nested_syntaxes: std::collections::HashMap::new(),
|
||||
auto_toc: false,
|
||||
}
|
||||
}
|
||||
|
||||
struct WikiDefaults {
|
||||
index: String,
|
||||
diary_rel_path: String,
|
||||
diary_index: String,
|
||||
diary_frequency: String,
|
||||
diary_start_week_day: String,
|
||||
diary_caption_level: u8,
|
||||
diary_sort: String,
|
||||
diary_header: String,
|
||||
maxhi: u8,
|
||||
listsyms: String,
|
||||
listsyms_propagate: bool,
|
||||
list_margin: i32,
|
||||
links_space_char: String,
|
||||
nested_syntaxes: std::collections::HashMap<String, String>,
|
||||
auto_toc: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Build a `Config` from `InitializeParams`.
|
||||
///
|
||||
@@ -194,19 +331,14 @@ impl Config {
|
||||
let wikis = if let Some(list) = raw.wikis {
|
||||
list.into_iter().map(WikiConfig::from).collect()
|
||||
} else if let Some(root) = raw.wiki_root.as_deref().and_then(expand_path) {
|
||||
let html = HtmlConfig::from_root(&root);
|
||||
vec![WikiConfig {
|
||||
name: root
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
root,
|
||||
file_extension: raw.file_extension.unwrap_or_else(|| ".wiki".into()),
|
||||
syntax: raw.syntax.unwrap_or_else(|| "vimwiki".into()),
|
||||
diary_rel_path: default_diary_rel_path(),
|
||||
diary_index: default_diary_index(),
|
||||
html,
|
||||
}]
|
||||
let mut wc = WikiConfig::from_root(root);
|
||||
if let Some(ext) = raw.file_extension {
|
||||
wc.file_extension = ext;
|
||||
}
|
||||
if let Some(s) = raw.syntax {
|
||||
wc.syntax = s;
|
||||
}
|
||||
vec![wc]
|
||||
} else if let Some(folder) = first_workspace_folder(params) {
|
||||
vec![WikiConfig::from_root(folder)]
|
||||
} else {
|
||||
@@ -289,6 +421,34 @@ struct RawWiki {
|
||||
exclude_files: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
color_dic: Option<std::collections::HashMap<String, String>>,
|
||||
// v1.1 per-wiki keys mirroring vimwiki globals. All optional so
|
||||
// existing single-wiki configs migrate without touching anything.
|
||||
#[serde(default)]
|
||||
index: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_frequency: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_start_week_day: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_caption_level: Option<u8>,
|
||||
#[serde(default)]
|
||||
diary_sort: Option<String>,
|
||||
#[serde(default)]
|
||||
diary_header: Option<String>,
|
||||
#[serde(default)]
|
||||
maxhi: Option<u8>,
|
||||
#[serde(default)]
|
||||
listsyms: Option<String>,
|
||||
#[serde(default)]
|
||||
listsyms_propagate: Option<bool>,
|
||||
#[serde(default)]
|
||||
list_margin: Option<i32>,
|
||||
#[serde(default)]
|
||||
links_space_char: Option<String>,
|
||||
#[serde(default)]
|
||||
nested_syntaxes: Option<std::collections::HashMap<String, String>>,
|
||||
#[serde(default)]
|
||||
auto_toc: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<RawWiki> for WikiConfig {
|
||||
@@ -329,13 +489,27 @@ impl From<RawWiki> for WikiConfig {
|
||||
if let Some(v) = r.color_dic {
|
||||
html.color_dic = v;
|
||||
}
|
||||
let d = wiki_defaults();
|
||||
WikiConfig {
|
||||
name: r.name.unwrap_or(derived_name),
|
||||
root,
|
||||
file_extension: r.file_extension.unwrap_or_else(|| ".wiki".into()),
|
||||
syntax: r.syntax.unwrap_or_else(|| "vimwiki".into()),
|
||||
diary_rel_path: r.diary_rel_path.unwrap_or_else(default_diary_rel_path),
|
||||
diary_index: r.diary_index.unwrap_or_else(default_diary_index),
|
||||
index: r.index.unwrap_or(d.index),
|
||||
diary_rel_path: r.diary_rel_path.unwrap_or(d.diary_rel_path),
|
||||
diary_index: r.diary_index.unwrap_or(d.diary_index),
|
||||
diary_frequency: r.diary_frequency.unwrap_or(d.diary_frequency),
|
||||
diary_start_week_day: r.diary_start_week_day.unwrap_or(d.diary_start_week_day),
|
||||
diary_caption_level: r.diary_caption_level.unwrap_or(d.diary_caption_level),
|
||||
diary_sort: r.diary_sort.unwrap_or(d.diary_sort),
|
||||
diary_header: r.diary_header.unwrap_or(d.diary_header),
|
||||
maxhi: r.maxhi.unwrap_or(d.maxhi),
|
||||
listsyms: r.listsyms.unwrap_or(d.listsyms),
|
||||
listsyms_propagate: r.listsyms_propagate.unwrap_or(d.listsyms_propagate),
|
||||
list_margin: r.list_margin.unwrap_or(d.list_margin),
|
||||
links_space_char: r.links_space_char.unwrap_or(d.links_space_char),
|
||||
nested_syntaxes: r.nested_syntaxes.unwrap_or(d.nested_syntaxes),
|
||||
auto_toc: r.auto_toc.unwrap_or(d.auto_toc),
|
||||
html,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user