test(keymaps): Vim path harness + fix 25 broken one-liner functions
CI / cargo fmt --check (push) Successful in 19s
CI / cargo clippy (push) Successful in 1m10s
CI / cargo test (push) Successful in 1m20s
CI / editor keymaps (push) Has been cancelled

User asked about the Vim path of the keymap suite. Building it
surfaced a real bug: 25 of our autoload functions were written as
`function! foo() abort | call bar() | endfunction` one-liners, which
isn't valid Vim syntax — `function!` requires a multi-line body, and
Vim parses the `|` after `abort` as an unexpected trailing character
(E488). Most invocations of the buggy autoload functions errored
out the moment Vim tried to parse them, which is why the user saw
broken keymaps and confusing diagnostics.

Rewrote all 25 one-liners (`autoload/nuwiki/commands.vim`) into the
standard three-line form. Affected groups: `diary_*`, `toggle_list_item`,
`cycle_list_item`, `reject_list_item`, `heading_add`, `heading_remove`,
`toc_generate`, `links_generate`, `export_current` / `_all` /
`_all_force` / `_rss`, and the §13.1 deferred stubs (`list_change_lvl`,
`list_remove_done`, `table_*`, `colorize`, `paste_link`, `paste_url`).

Added `scripts/test-keymaps-vim.{sh,vim}` — a Vim-side counterpart of
the Neovim harness covering the pure-VimL bindings (header nav,
link nav, `o`/`O` bullet continuation, `<CR>` wrap step). The
LSP-roundtrip bindings (`<C-Space>`, `=`, `-`, …) stay on the Neovim
side because vim-lsp's async layer uses timers that don't fire
inside `vim -e -s` headless mode — their server-side codepath is
already exercised by the Neovim harness and the cargo test suite.

Vim harness covers 12 cases: filetype + 2 command-presence smoke
tests, 4 header-nav (`]]`/`[[`/`]=`/`]u`), `<Tab>` link-nav, `<CR>`
wrap-on-first-press, and 3 bullet-continuation flows.

CI: extended the existing `keymaps` job to install both nvim + vim
and run both harnesses. Verified locally:

  Neovim harness:  19 passed, 0 failed
  Vim harness:     12 passed, 0 failed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 12:41:46 +00:00
parent 3b6693129c
commit 5fdd7a842e
4 changed files with 311 additions and 32 deletions
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
#
# scripts/test-keymaps-vim.sh — Vim counterpart of test-keymaps.sh.
#
# Pure-VimL portion only. The LSP-roundtrip keymaps (`<C-Space>`,
# `=`, `-`, …) talk to the server via vim-lsp's timer-driven async
# layer, which doesn't fire reliably inside `vim -e -s`. Those have
# Neovim coverage in scripts/test-keymaps.lua, exercising the same
# Lua command layer + server binary.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP="$(mktemp -d -t nuwiki-keymap-vim-test-XXXXXX)"
trap 'rm -rf "$TMP"' EXIT
log() { printf '\033[1;34m[keymap-vim]\033[0m %s\n' "$*"; }
if ! command -v vim >/dev/null 2>&1; then
log 'vim not installed — skipping'
exit 0
fi
# Don't need the LSP binary for the pure-VimL cases, but make sure
# the autoload file `expand('<sfile>')` resolves correctly when the
# script-load-time plugin_root capture runs.
mkdir -p "$TMP/wiki"
SEED="$TMP/wiki/index.wiki"
echo "= seed =" > "$SEED"
VIMRC="$TMP/vimrc"
cat > "$VIMRC" <<EOF
set nocompatible
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
filetype plugin indent on
syntax enable
" ftdetect/nuwiki.vim already maps *.wiki → vimwiki via \`set filetype\`.
EOF
RESULTS="$TMP/results.txt"
log "running harness…"
NUWIKI_KEYMAP_RESULTS="$RESULTS" \
vim -e -s -u "$VIMRC" -c "edit $SEED" -c "source $REPO_ROOT/scripts/test-keymaps-vim.vim" \
>"$TMP/vim.log" 2>&1 || true
if [[ ! -f "$RESULTS" ]]; then
echo 'keymap test harness produced no output' >&2
echo '--- vim log ---' >&2
cat "$TMP/vim.log" >&2 || true
exit 1
fi
cat "$RESULTS"
if grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$RESULTS"; then
exit 0
fi
exit 1
+170
View File
@@ -0,0 +1,170 @@
" scripts/test-keymaps-vim.vim — driven by scripts/test-keymaps-vim.sh.
"
" Pure-VimL keymap regression suite. Plain Vim's LSP path
" (vim-lsp + async.vim) talks to the server via timers, which won't
" reliably fire inside `vim -e -s` headless mode — so the cases here
" are restricted to the bindings whose RHS is implemented entirely in
" VimL (header/link nav, `o`/`O` bullet continuation, the wrap step
" of `<CR>`). LSP-roundtrip bindings (`<C-Space>`, `=`, `-`, …) get
" their coverage from the Neovim harness in scripts/test-keymaps.lua
" — same Lua codepath, same server.
let s:results = []
let s:pass = 0
let s:fail = 0
let s:out = $NUWIKI_KEYMAP_RESULTS
function! s:record(ok, name, detail) abort
let l:line = (a:ok ? 'PASS' : 'FAIL') . ' ' . a:name
if a:detail !=# ''
let l:line .= ' — ' . a:detail
endif
call add(s:results, l:line)
if a:ok
let s:pass += 1
else
let s:fail += 1
endif
endfunction
function! s:set_buf(lines) abort
silent %delete _
call setline(1, a:lines)
endfunction
function! s:lines() abort
return getline(1, '$')
endfunction
function! s:run(name, opts) abort
try
call s:set_buf(a:opts.lines)
let l:cursor = get(a:opts, 'cursor', [1, 1])
call cursor(l:cursor[0], l:cursor[1])
" feedkeys with 'tx' — t: typed (respects maps), x: execute now.
let l:keys = a:opts.keys
call feedkeys(l:keys, 'tx')
let l:detail = ''
if has_key(a:opts, 'expect_lines')
if s:lines() !=# a:opts.expect_lines
let l:detail = 'lines mismatch — expected ' . string(a:opts.expect_lines)
\ . ', actual ' . string(s:lines())
call s:record(0, a:name, l:detail)
return
endif
endif
if has_key(a:opts, 'expect_line') && has_key(a:opts, 'expect_at')
let l:got = getline(a:opts.expect_at)
if l:got !=# a:opts.expect_line
let l:detail = 'line ' . a:opts.expect_at . ' — expected ' . string(a:opts.expect_line)
\ . ', got ' . string(l:got)
call s:record(0, a:name, l:detail)
return
endif
endif
if has_key(a:opts, 'expect_cursor_line')
let l:lnum = line('.')
if l:lnum != a:opts.expect_cursor_line
let l:detail = 'cursor line — expected ' . a:opts.expect_cursor_line
\ . ', got ' . l:lnum
call s:record(0, a:name, l:detail)
return
endif
endif
call s:record(1, a:name, '')
catch
call s:record(0, a:name, v:exception)
endtry
endfunction
" ===== Smoke: ftplugin loaded + commands defined =====
if &filetype !=# 'vimwiki'
call s:record(0, 'ftplugin.attached', 'filetype is ' . &filetype . ' (expected vimwiki)')
else
call s:record(1, 'ftplugin.attached', '')
endif
if exists(':VimwikiTOC') == 2
call s:record(1, 'cmd.VimwikiTOC_defined', '')
else
call s:record(0, 'cmd.VimwikiTOC_defined', ':VimwikiTOC not registered')
endif
if exists(':NuwikiIndex') == 2
call s:record(1, 'cmd.NuwikiIndex_defined', '')
else
call s:record(0, 'cmd.NuwikiIndex_defined', ':NuwikiIndex not registered')
endif
" ===== Header nav (pure VimL) =====
call s:run('headers.]]_jumps_to_next', {
\ 'lines': ['= One =', 'body', '== Two ==', 'body', '= Three ='],
\ 'cursor': [1, 1],
\ 'keys': ']]',
\ 'expect_cursor_line': 3,
\ })
call s:run('headers.[[_jumps_to_prev', {
\ 'lines': ['= One =', 'body', '== Two ==', 'body', '= Three ='],
\ 'cursor': [5, 1],
\ 'keys': '[[',
\ 'expect_cursor_line': 3,
\ })
call s:run('headers.]u_jumps_to_parent', {
\ 'lines': ['= Parent =', '== Child ==', 'body'],
\ 'cursor': [3, 1],
\ 'keys': ']u',
\ 'expect_cursor_line': 1,
\ })
call s:run('headers.]=_jumps_to_next_sibling', {
\ 'lines': ['= A =', '== child ==', 'body', '= B ='],
\ 'cursor': [1, 1],
\ 'keys': ']=',
\ 'expect_cursor_line': 4,
\ })
" ===== Link nav (pure VimL) =====
call s:run('links.<Tab>_finds_next_wikilink', {
\ 'lines': ['intro [[A]] more [[B]] end'],
\ 'cursor': [1, 1],
\ 'keys': "\<Tab>",
\ 'expect_cursor_line': 1,
\ })
" ===== <CR> first press wraps bare word =====
call s:run('cr.wraps_bare_word_first_press', {
\ 'lines': ['MyPage rest'],
\ 'cursor': [1, 3],
\ 'keys': "\<CR>",
\ 'expect_line': '[[MyPage]] rest',
\ 'expect_at': 1,
\ })
" ===== Bullet continuation =====
call s:run('lists.o_continues_list_marker', {
\ 'lines': ['- first'],
\ 'cursor': [1, 1],
\ 'keys': "o\<Esc>",
\ 'expect_lines': ['- first', '- '],
\ })
call s:run('lists.o_continues_checkbox', {
\ 'lines': ['- [ ] first'],
\ 'cursor': [1, 1],
\ 'keys': "o\<Esc>",
\ 'expect_lines': ['- [ ] first', '- [ ] '],
\ })
call s:run('lists.O_continues_above', {
\ 'lines': ['- second'],
\ 'cursor': [1, 1],
\ 'keys': "O\<Esc>",
\ 'expect_lines': ['- ', '- second'],
\ })
" ===== Wrap up =====
call add(s:results, '')
call add(s:results, printf('SUMMARY: %d passed, %d failed', s:pass, s:fail))
call writefile(s:results, s:out)
execute (s:fail == 0 ? 'qall!' : 'cquit!')