diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ca3e7ec..059249d 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -120,3 +120,7 @@ jobs: run: ./development/tests/test-calendar-vim.sh - name: run Vim vars-shim harness run: ./development/tests/test-vars-vim.sh + - name: run Vim vimwiki-config compat harness + run: ./development/tests/test-vimwiki-compat-vim.sh + - name: run Neovim global-shorthand harness + run: ./development/tests/test-global-shorthand.sh diff --git a/README.md b/README.md index 23571c1..2ce1536 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,14 @@ The `w` prefix in the `wiki_prefix`, `links`, and `html_export` rows is #### Per-wiki keys (`wikis[]`) +Most display/generation keys below can also be set **once at the top level** +as a default for every wiki (vimwiki-style global shorthand) — via `setup({ … })` +or `g:nuwiki_` — and a per-wiki value overrides it. The keys that honour +this are `toc_header`, `toc_header_level`, `toc_link_format`, `links_header`, +`links_header_level`, `tags_header`, `tags_header_level`, +`html_header_numbering`, `html_header_numbering_sym`, `links_space_char`, +`list_margin`, `listsyms`, `listsym_rejected`, and the `auto_*` toggles. + | Key | Type | Default | Accepted values | |-----|------|---------|-----------------| | `name` | string | — | any label | @@ -660,8 +668,18 @@ match vimwiki. To migrate: 1. Drop the original `vimwiki` plugin from your config. 2. Install nuwiki. -3. Point `wiki_root` (or each `wikis[i].root`) at your existing - directory. +3. Keep your existing config, or point `wiki_root` / `wikis[i].root` at + your directory. + +**Existing `g:vimwiki_list` config works as-is (Vim).** When you haven't +configured nuwiki natively (`g:nuwiki_wikis` / `g:nuwiki_*`), the Vim +client reads your upstream `g:vimwiki_list` and `g:vimwiki_*` globals and +translates them: each wiki's `path`/`path_html`/`template_*`/`auto_export` +plus globals like `toc_header_level`, `html_header_numbering`, +`html_header_numbering_sym`, `links_space_char`, and `list_margin`. +Native `g:nuwiki_*` config always takes precedence. (Settings nuwiki +implements differently — see [`known-issues.md`](./known-issues.md) — are +ignored.) Existing pages, diary entries, tags, and templates work without modification. The first workspace scan after launch may show an empty diff --git a/autoload/nuwiki/lsp.vim b/autoload/nuwiki/lsp.vim index 2e6a9f3..e3d7434 100644 --- a/autoload/nuwiki/lsp.vim +++ b/autoload/nuwiki/lsp.vim @@ -12,6 +12,97 @@ let s:plugin_root = fnamemodify(resolve(expand(':p')), ':h:h:h') let s:started = 0 +" ===== Upstream vimwiki config compatibility ===== +" +" nuwiki is a drop-in vimwiki replacement, so a user who keeps their original +" `g:vimwiki_list` + `g:vimwiki_*` globals shouldn't have to rewrite anything. +" These tables translate the upstream names into the server's config schema. +" nuwiki-native config (`g:nuwiki_wikis` / `g:nuwiki_*`) always wins. + +" Per-wiki dict keys: upstream `g:vimwiki_list[i]` key -> server key. +let s:vimwiki_wiki_map = { + \ 'path': 'root', + \ 'path_html': 'html_path', + \ 'template_path': 'template_path', + \ 'template_default': 'template_default', + \ 'template_ext': 'template_ext', + \ 'css_name': 'css_name', + \ 'auto_export': 'auto_export', + \ 'auto_toc': 'auto_toc', + \ 'syntax': 'syntax', + \ 'ext': 'file_extension', + \ 'index': 'index', + \ 'diary_rel_path': 'diary_rel_path', + \ 'diary_index': 'diary_index', + \ 'name': 'name', + \ } + +" Global scalar settings vimwiki applies to every wiki (via wikilocal +" defaults): upstream suffix (`g:vimwiki_`) -> server key. The server +" reads these per-wiki, so we fold them into each wiki entry (and into the +" single-wiki shorthand payload). +let s:scalar_global_map = { + \ 'toc_header': 'toc_header', + \ 'toc_header_level': 'toc_header_level', + \ 'toc_link_format': 'toc_link_format', + \ 'links_header': 'links_header', + \ 'links_header_level': 'links_header_level', + \ 'tags_header': 'tags_header', + \ 'tags_header_level': 'tags_header_level', + \ 'html_header_numbering': 'html_header_numbering', + \ 'html_header_numbering_sym': 'html_header_numbering_sym', + \ 'links_space_char': 'links_space_char', + \ 'list_margin': 'list_margin', + \ 'listsyms': 'listsyms', + \ 'listsym_rejected': 'listsym_rejected', + \ 'auto_export': 'auto_export', + \ 'auto_toc': 'auto_toc', + \ 'auto_generate_links': 'auto_generate_links', + \ 'auto_generate_tags': 'auto_generate_tags', + \ 'auto_diary_index': 'auto_diary_index', + \ } + +" Resolve the per-wiki scalar globals: vimwiki values first (compat), then +" nuwiki-native `g:nuwiki_` overrides on top. +function! s:scalar_globals() abort + let l:g = {} + for [l:src, l:dst] in items(s:scalar_global_map) + if exists('g:vimwiki_' . l:src) + let l:g[l:dst] = get(g:, 'vimwiki_' . l:src) + endif + endfor + for l:dst in values(s:scalar_global_map) + if exists('g:nuwiki_' . l:dst) + let l:g[l:dst] = get(g:, 'nuwiki_' . l:dst) + endif + endfor + return l:g +endfunction + +" Build nuwiki's `wikis` list from an upstream `g:vimwiki_list`, folding the +" global scalar settings into each entry. Returns [] when unset/malformed. +function! s:wikis_from_vimwiki() abort + if !exists('g:vimwiki_list') || type(g:vimwiki_list) != type([]) || empty(g:vimwiki_list) + return [] + endif + let l:globals = s:scalar_globals() + let l:wikis = [] + for l:vw in g:vimwiki_list + if type(l:vw) != type({}) | continue | endif + let l:w = copy(l:globals) + for [l:src, l:dst] in items(s:vimwiki_wiki_map) + if has_key(l:vw, l:src) + let l:w[l:dst] = l:vw[l:src] + endif + endfor + " A wiki needs a root; skip entries without one. + if has_key(l:w, 'root') + call add(l:wikis, l:w) + endif + endfor + return l:wikis +endfunction + function! nuwiki#lsp#start() abort if s:started return @@ -89,12 +180,29 @@ function! s:settings() abort \ 'link_severity': get(g:, 'nuwiki_link_severity', 'warn'), \ }, \ } - " Include the per-wiki list when configured via g:nuwiki_wikis so the - " server knows each wiki's root, diary path, etc. Without this it only - " sees wiki_root and resolves links relative to that directory instead - " of the individual wiki's root. + " Global scalar settings (g:vimwiki_* then g:nuwiki_* overrides) act as + " defaults applied to every wiki, vimwiki-style. A per-wiki value always + " wins. Resolved once and folded into whichever wiki shape we send. + let l:globals = s:scalar_globals() + + " Wiki list resolution, in priority order: + " 1. g:nuwiki_wikis (native multi-wiki config) + " 2. g:vimwiki_list (upstream vimwiki config — drop-in compat) + " 3. wiki_root shorthand (single wiki) + " Without a list the server only sees wiki_root and resolves links relative + " to that directory instead of each individual wiki's root. if exists('g:nuwiki_wikis') && !empty(g:nuwiki_wikis) - let l:cfg['wikis'] = g:nuwiki_wikis + let l:cfg['wikis'] = map(copy(g:nuwiki_wikis), + \ {_, w -> extend(copy(l:globals), w, 'force')}) + else + let l:vw_wikis = s:wikis_from_vimwiki() + if !empty(l:vw_wikis) + let l:cfg['wikis'] = l:vw_wikis + else + " Single-wiki shorthand: fold the global scalars into the top-level + " payload so the server's single-wiki desugaring honours them. + call extend(l:cfg, l:globals, 'force') + endif endif return l:cfg endfunction diff --git a/crates/nuwiki-lsp/tests/index_and_config.rs b/crates/nuwiki-lsp/tests/index_and_config.rs index 96e0dbb..811e6eb 100644 --- a/crates/nuwiki-lsp/tests/index_and_config.rs +++ b/crates/nuwiki-lsp/tests/index_and_config.rs @@ -674,3 +674,20 @@ fn legacy_single_wiki_shape_picks_up_defaults() { assert_eq!(cfg.wikis[0].index, "index"); assert_eq!(cfg.wikis[0].diary_frequency, "daily"); } + +#[test] +fn vimwiki_compat_payload_sets_toc_level_per_wiki() { + // The exact shape the Vim client emits when translating g:vimwiki_list + + // g:vimwiki_toc_header_level=2 (drop-in vimwiki compat). + let cfg = config_from_json(json!({ + "wiki_root": "~/vimwiki", + "wikis": [ + { "root": "/tmp/personal", "toc_header_level": 2, "html_header_numbering": 2 }, + { "root": "/tmp/ifood", "toc_header_level": 2 }, + ], + })); + assert_eq!(cfg.wikis.len(), 2); + assert_eq!(cfg.wikis[0].toc_header_level, 2); + assert_eq!(cfg.wikis[0].html.html_header_numbering, 2); + assert_eq!(cfg.wikis[1].toc_header_level, 2); +} diff --git a/development/tests/test-global-shorthand.lua b/development/tests/test-global-shorthand.lua new file mode 100644 index 0000000..863b433 --- /dev/null +++ b/development/tests/test-global-shorthand.lua @@ -0,0 +1,54 @@ +-- development/tests/test-global-shorthand.lua — Neovim client global +-- shorthand: a per-wiki setting given once at the top level (setup() or +-- g:nuwiki_) is folded into every wiki as a default, and a per-wiki +-- value overrides it. Mirror of the Vim test-vimwiki-compat checks. +-- +-- Writes PASS/FAIL lines to $NUWIKI_GS_OUT; the wrapper fails on any FAIL. + +local config = require('nuwiki.config') +local lsp = require('nuwiki.lsp') + +local out = {} +local function check(desc, cond) + table.insert(out, (cond and 'PASS ' or 'FAIL ') .. desc) +end + +-- ----- setup() top-level scalar folds into every wiki; per-wiki wins ----- +config.apply({ + toc_header_level = 2, + html_header_numbering = 2, + wikis = { + { root = '/tmp/one', name = 'One' }, + { root = '/tmp/two', name = 'Two', toc_header_level = 3 }, + }, +}) +local io1 = lsp.init_options() +check('w0 inherits global level', io1.wikis[1].toc_header_level == 2) +check('w0 inherits numbering', io1.wikis[1].html_header_numbering == 2) +check('w1 per-wiki override wins', io1.wikis[2].toc_header_level == 3) +check('w1 still inherits numbering', io1.wikis[2].html_header_numbering == 2) + +-- The user's setup table must not be mutated by the fold. +check('no mutation of user wikis', config.user.wikis[1].toc_header_level == nil) + +-- ----- built-in defaults are NOT folded (only explicit user values) ----- +config.apply({ + wikis = { { root = '/tmp/x', name = 'X' } }, +}) +local io2 = lsp.init_options() +check('default toc_header not folded', io2.wikis[1].toc_header == nil) +check('default level not folded', io2.wikis[1].toc_header_level == nil) + +-- ----- g:nuwiki_ acts as a global too ----- +config.apply({ + wikis = { { root = '/tmp/y', name = 'Y' } }, +}) +vim.g.nuwiki_toc_header_level = 5 +local io3 = lsp.init_options() +check('g:nuwiki_ global folds', io3.wikis[1].toc_header_level == 5) +vim.g.nuwiki_toc_header_level = nil + +local f = assert(io.open(os.getenv('NUWIKI_GS_OUT'), 'w')) +f:write(table.concat(out, '\n') .. '\n') +f:close() +vim.cmd('qa!') diff --git a/development/tests/test-global-shorthand.sh b/development/tests/test-global-shorthand.sh new file mode 100755 index 0000000..025d079 --- /dev/null +++ b/development/tests/test-global-shorthand.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# development/tests/test-global-shorthand.sh — Neovim client global-shorthand +# folding (vimwiki-style per-wiki defaults). Runs test-global-shorthand.lua +# under headless Neovim; that script asserts the init_options payload folds a +# top-level / g:nuwiki_* setting into every wiki, lets a per-wiki value +# override it, and does NOT fold built-in defaults. +# +# Exit code: 0 when every check passes, 1 on any FAIL or missing output. +# Skips (exit 0) when nvim is not installed. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +log() { printf '\033[1;34m[global-shorthand]\033[0m %s\n' "$*"; } + +if ! command -v nvim >/dev/null 2>&1; then + log 'nvim not installed — skipping' + exit 0 +fi + +TMP="$(mktemp -d -t nuwiki-gs-test-XXXXXX)" +trap 'rm -rf "$TMP"' EXIT + +OUT="$TMP/gs.out" +INIT="$TMP/init.lua" +cat > "$INIT" <"$TMP/nvim.log" 2>&1 || true + +if [[ ! -f "$OUT" ]]; then + echo 'global-shorthand harness produced no output' >&2 + echo '--- nvim log ---' >&2 + cat "$TMP/nvim.log" >&2 || true + exit 1 +fi + +cat "$OUT" +PASSED="$(grep -c '^PASS ' "$OUT" || true)" +FAILED="$(grep -c '^FAIL ' "$OUT" || true)" +echo "SUMMARY: ${PASSED} passed, ${FAILED} failed" + +if [[ "$FAILED" -ne 0 ]]; then + exit 1 +fi +log 'global shorthand OK ✓' +exit 0 diff --git a/development/tests/test-vimwiki-compat-vim.sh b/development/tests/test-vimwiki-compat-vim.sh new file mode 100755 index 0000000..ba186c9 --- /dev/null +++ b/development/tests/test-vimwiki-compat-vim.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# development/tests/test-vimwiki-compat-vim.sh — upstream-vimwiki config +# drop-in compatibility for the Vim client. +# +# Runs test-vimwiki-compat-vim.vim under headless Vim; that script sets an +# upstream `g:vimwiki_list` + `g:vimwiki_*` config and asserts +# nuwiki#lsp#settings() translates it into the server's schema (per-wiki key +# remap + global scalar settings folded into each wiki). Also checks that +# nuwiki-native config takes precedence. +# +# Exit code: 0 when every check passes, 1 on any FAIL or missing output. +# Skips (exit 0) when vim is not installed. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +log() { printf '\033[1;34m[vimwiki-compat]\033[0m %s\n' "$*"; } + +if ! command -v vim >/dev/null 2>&1; then + log 'vim not installed — skipping' + exit 0 +fi + +TMP="$(mktemp -d -t nuwiki-vwc-test-XXXXXX)" +trap 'rm -rf "$TMP"' EXIT + +OUT="$TMP/vwc.out" + +VIMRC="$TMP/vimrc" +cat > "$VIMRC" <"$TMP/vim.log" 2>&1 || true + +if [[ ! -f "$OUT" ]]; then + echo 'vimwiki-compat harness produced no output' >&2 + echo '--- vim log ---' >&2 + cat "$TMP/vim.log" >&2 || true + exit 1 +fi + +cat "$OUT" +PASSED="$(grep -c '^PASS ' "$OUT" || true)" +FAILED="$(grep -c '^FAIL ' "$OUT" || true)" +echo "SUMMARY: ${PASSED} passed, ${FAILED} failed" + +if [[ "$FAILED" -ne 0 ]]; then + exit 1 +fi +log 'vimwiki config compat OK ✓' +exit 0 diff --git a/development/tests/test-vimwiki-compat-vim.vim b/development/tests/test-vimwiki-compat-vim.vim new file mode 100644 index 0000000..18364c7 --- /dev/null +++ b/development/tests/test-vimwiki-compat-vim.vim @@ -0,0 +1,78 @@ +" development/tests/test-vimwiki-compat-vim.vim — upstream-vimwiki config +" drop-in compatibility for the Vim client. +" +" Sourced by test-vimwiki-compat-vim.sh under headless Vim. Sets an upstream +" `g:vimwiki_list` + `g:vimwiki_*` config and asserts nuwiki#lsp#settings() +" translates it into the server's schema. Writes PASS/FAIL to +" $NUWIKI_VWC_OUT; the wrapper fails on any FAIL. + +let s:out = [] +function! s:check(desc, cond) abort + call add(s:out, (a:cond ? 'PASS ' : 'FAIL ') . a:desc) +endfunction + +" ----- upstream vimwiki config (no nuwiki-native vars set) ----- +let g:vimwiki_toc_header_level = 2 +let g:vimwiki_html_header_numbering = 2 +let g:vimwiki_html_header_numbering_sym = ' -' +let s:personal = {} +let s:personal.path = '~/.vimwiki/personal_wiki' +let s:personal.path_html = '~/public_html/personal_wiki' +let s:personal.template_default = 'default' +let s:personal.template_ext = '.tpl' +let s:personal.auto_export = 1 +let s:ifood = {} +let s:ifood.path = '~/.vimwiki/ifood_wiki' +let g:vimwiki_list = [s:personal, s:ifood] + +let s:cfg = nuwiki#lsp#settings() + +call s:check('has wikis list', has_key(s:cfg, 'wikis')) +call s:check('two wikis', has_key(s:cfg, 'wikis') && len(s:cfg.wikis) == 2) +let s:w0 = get(s:cfg, 'wikis', [{}])[0] +let s:w1 = get(s:cfg, 'wikis', [{},{}])[1] +call s:check('w0 path->root', get(s:w0, 'root', '') ==# '~/.vimwiki/personal_wiki') +call s:check('w0 path_html->html_path', get(s:w0, 'html_path', '') ==# '~/public_html/personal_wiki') +call s:check('w0 template_default', get(s:w0, 'template_default', '') ==# 'default') +call s:check('w0 auto_export', get(s:w0, 'auto_export', 0) == 1) +call s:check('w0 toc_header_level=2', get(s:w0, 'toc_header_level', 0) == 2) +call s:check('w0 html_numbering=2', get(s:w0, 'html_header_numbering', 0) == 2) +call s:check('w0 numbering_sym', get(s:w0, 'html_header_numbering_sym', '') ==# ' -') +call s:check('w1 root', get(s:w1, 'root', '') ==# '~/.vimwiki/ifood_wiki') +call s:check('w1 globals applied', get(s:w1, 'toc_header_level', 0) == 2) + +" ----- nuwiki-native config wins over vimwiki ----- +let g:nuwiki_wikis = [{'root': '~/native', 'name': 'native'}] +let s:cfg2 = nuwiki#lsp#settings() +call s:check('native g:nuwiki_wikis wins', + \ len(get(s:cfg2, 'wikis', [])) == 1 + \ && get(s:cfg2.wikis[0], 'root', '') ==# '~/native') +unlet g:nuwiki_wikis + +" ----- nuwiki-native scalar overrides the vimwiki global ----- +let g:vimwiki_list = [] +let g:nuwiki_toc_header_level = 4 +let s:cfg3 = nuwiki#lsp#settings() +" No wikis list now (vimwiki_list empty) → scalar folded into top-level. +call s:check('native scalar override', get(s:cfg3, 'toc_header_level', 0) == 4) +unlet g:nuwiki_toc_header_level + +" ----- global shorthand on native g:nuwiki_wikis (vimwiki-style) ----- +" A global default applies to every wiki; a per-wiki value overrides it. +let g:nuwiki_toc_header_level = 2 +let g:nuwiki_html_header_numbering = 2 +let g:nuwiki_wikis = [ + \ {'root': '~/w1', 'name': 'One'}, + \ {'root': '~/w2', 'name': 'Two', 'toc_header_level': 3}, + \ ] +let s:cfg4 = nuwiki#lsp#settings() +let s:g0 = s:cfg4.wikis[0] +let s:g1 = s:cfg4.wikis[1] +call s:check('global folds into wiki 0', get(s:g0, 'toc_header_level', 0) == 2) +call s:check('global numbering folds', get(s:g0, 'html_header_numbering', 0) == 2) +call s:check('per-wiki override wins', get(s:g1, 'toc_header_level', 0) == 3) +call s:check('global still folds elsewhere', get(s:g1, 'html_header_numbering', 0) == 2) +" The user's original dict must not be mutated by the fold. +call s:check('no mutation of g:nuwiki_wikis', !has_key(g:nuwiki_wikis[0], 'toc_header_level')) + +call writefile(s:out, $NUWIKI_VWC_OUT) diff --git a/known-issues.md b/known-issues.md index aa685f9..4ad086d 100644 --- a/known-issues.md +++ b/known-issues.md @@ -128,6 +128,15 @@ correct as-is; the deferral is a deliberate risk/reward call. ## Notes +- **Upstream `g:vimwiki_list` config (Vim).** When you haven't configured + nuwiki natively, the Vim client reads your upstream `g:vimwiki_list` + + `g:vimwiki_*` globals and translates them into nuwiki's schema (per-wiki + `path`/`path_html`/`template_*`/`auto_export`, plus globals `toc_header`, + `toc_header_level`, `html_header_numbering`, `html_header_numbering_sym`, + `links_space_char`, `list_margin`). `g:nuwiki_*` always wins. Globals nuwiki + doesn't model (e.g. `automatic_nested_syntaxes`) are ignored — see the + divergence list above. **Limitation:** this drop-in translation is currently + **Vim-only**; on Neovim, configure via `setup()` / `g:nuwiki_wikis`. - **Third-party plugin compatibility.** A shim at `autoload/vimwiki/vars.vim` exposes the subset of `vimwiki#vars#get_wikilocal` that plugins like vimwiki-sync and vim-zettel call (`path`, `ext`/`extension`, `syntax`, diff --git a/lua/nuwiki/config.lua b/lua/nuwiki/config.lua index 05936d9..2b9f71c 100644 --- a/lua/nuwiki/config.lua +++ b/lua/nuwiki/config.lua @@ -103,7 +103,14 @@ M.defaults = { M.options = vim.deepcopy(M.defaults) +-- The raw table the user passed to setup(), kept separate from the +-- defaults-merged `options`. Used to tell an explicitly-set value apart from +-- a built-in default — e.g. so the LSP client only treats a per-wiki key as a +-- global default when the user actually set it. +M.user = {} + function M.apply(user) + M.user = user or {} M.options = vim.tbl_deep_extend('force', M.defaults, user or {}) end diff --git a/lua/nuwiki/lsp.lua b/lua/nuwiki/lsp.lua index 6338907..19fdbc1 100644 --- a/lua/nuwiki/lsp.lua +++ b/lua/nuwiki/lsp.lua @@ -14,6 +14,39 @@ local function command() return { install.expected_path() } end +-- Per-wiki settings that double as wiki-wide defaults (vimwiki-style global +-- shorthand): set once at the top level — via setup() or `g:nuwiki_` — +-- and every wiki inherits it; a per-wiki value overrides. Mirrors the Vim +-- client's `s:scalar_global_map`. +local SCALAR_GLOBALS = { + 'toc_header', 'toc_header_level', 'toc_link_format', + 'links_header', 'links_header_level', + 'tags_header', 'tags_header_level', + 'html_header_numbering', 'html_header_numbering_sym', + 'links_space_char', 'list_margin', + 'listsyms', 'listsym_rejected', + 'auto_export', 'auto_toc', + 'auto_generate_links', 'auto_generate_tags', 'auto_diary_index', +} + +-- Resolve the global scalar defaults from config the user *explicitly* set: +-- the raw setup() table first, then the `g:nuwiki_` global. Built-in +-- defaults are deliberately excluded so they don't get folded into every wiki +-- (that would diverge from the Vim client and override per-wiki values with a +-- default). +local function scalar_globals() + local user = config.user or {} + local g = {} + for _, key in ipairs(SCALAR_GLOBALS) do + if user[key] ~= nil then + g[key] = user[key] + elseif vim.g['nuwiki_' .. key] ~= nil then + g[key] = vim.g['nuwiki_' .. key] + end + end + return g +end + -- Merge g:nuwiki_wikis (VimL config) into opts.wikis when the user has not -- passed wikis through setup(). This lets users who configure via a .vim -- file avoid duplicating the list in their Lua setup() call. @@ -22,6 +55,23 @@ local function resolved_opts() if not opts.wikis and vim.g.nuwiki_wikis then opts = vim.tbl_extend('force', opts, { wikis = vim.g.nuwiki_wikis }) end + local globals = scalar_globals() + if vim.tbl_isempty(globals) then + return opts + end + if opts.wikis then + -- Fold the globals into each wiki as defaults (per-wiki value wins), + -- without mutating the user's setup table. + local folded = {} + for i, w in ipairs(opts.wikis) do + folded[i] = vim.tbl_extend('force', globals, w) + end + opts = vim.tbl_extend('force', opts, { wikis = folded }) + else + -- Single-wiki shorthand: ensure the globals (incl. the g:nuwiki_* form) + -- sit at the top level for the server's single-wiki desugaring. + opts = vim.tbl_extend('keep', opts, globals) + end return opts end