From 5fdd7a842e5ae86ac59d35cf0a1fb316128a330d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Tue, 12 May 2026 12:41:46 +0000 Subject: [PATCH] test(keymaps): Vim path harness + fix 25 broken one-liner functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, `` wrap step). The LSP-roundtrip bindings (``, `=`, `-`, …) 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`), `` link-nav, `` 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) --- .gitea/workflows/ci.yaml | 9 +- autoload/nuwiki/commands.vim | 104 +++++++++++++++------ scripts/test-keymaps-vim.sh | 60 +++++++++++++ scripts/test-keymaps-vim.vim | 170 +++++++++++++++++++++++++++++++++++ 4 files changed, 311 insertions(+), 32 deletions(-) create mode 100755 scripts/test-keymaps-vim.sh create mode 100644 scripts/test-keymaps-vim.vim diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index b379510..02d3f94 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -71,10 +71,13 @@ jobs: key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }} restore-keys: | ${{ runner.os }}-cargo-test- - - name: install neovim + - name: install neovim + vim run: | sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends neovim + sudo apt-get install -y --no-install-recommends neovim vim nvim --version | head -1 - - name: run keymap harness + vim --version | head -1 + - name: run Neovim keymap harness run: ./scripts/test-keymaps.sh + - name: run Vim keymap harness + run: ./scripts/test-keymaps-vim.sh diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index daaedbf..acc9a82 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -282,13 +282,24 @@ function! s:diary_open(cmd_name) abort \ function('s:open_uri_from')) endfunction -function! nuwiki#commands#diary_today() abort | call s:diary_open('nuwiki.diary.openToday') | endfunction -function! nuwiki#commands#diary_yesterday() abort | call s:diary_open('nuwiki.diary.openYesterday') | endfunction -function! nuwiki#commands#diary_tomorrow() abort | call s:diary_open('nuwiki.diary.openTomorrow') | endfunction -function! nuwiki#commands#diary_index() abort | call s:diary_open('nuwiki.diary.openIndex') | endfunction -function! nuwiki#commands#diary_next() abort | call s:diary_open('nuwiki.diary.next') | endfunction -function! nuwiki#commands#diary_prev() abort | call s:diary_open('nuwiki.diary.prev') | endfunction - +function! nuwiki#commands#diary_today() abort + call s:diary_open('nuwiki.diary.openToday') +endfunction +function! nuwiki#commands#diary_yesterday() abort + call s:diary_open('nuwiki.diary.openYesterday') +endfunction +function! nuwiki#commands#diary_tomorrow() abort + call s:diary_open('nuwiki.diary.openTomorrow') +endfunction +function! nuwiki#commands#diary_index() abort + call s:diary_open('nuwiki.diary.openIndex') +endfunction +function! nuwiki#commands#diary_next() abort + call s:diary_open('nuwiki.diary.next') +endfunction +function! nuwiki#commands#diary_prev() abort + call s:diary_open('nuwiki.diary.prev') +endfunction function! nuwiki#commands#diary_generate_index() abort call s:exec('nuwiki.diary.generateIndex', [{ 'uri': s:buf_uri() }]) endfunction @@ -301,12 +312,21 @@ function! s:exec_pos(cmd) abort call s:exec(a:cmd, l:args) endfunction -function! nuwiki#commands#toggle_list_item() abort | call s:exec_pos('nuwiki.list.toggleCheckbox') | endfunction -function! nuwiki#commands#cycle_list_item() abort | call s:exec_pos('nuwiki.list.cycleCheckbox') | endfunction -function! nuwiki#commands#reject_list_item() abort | call s:exec_pos('nuwiki.list.rejectCheckbox') | endfunction -function! nuwiki#commands#heading_add() abort | call s:exec_pos('nuwiki.heading.addLevel') | endfunction -function! nuwiki#commands#heading_remove() abort | call s:exec_pos('nuwiki.heading.removeLevel') | endfunction - +function! nuwiki#commands#toggle_list_item() abort + call s:exec_pos('nuwiki.list.toggleCheckbox') +endfunction +function! nuwiki#commands#cycle_list_item() abort + call s:exec_pos('nuwiki.list.cycleCheckbox') +endfunction +function! nuwiki#commands#reject_list_item() abort + call s:exec_pos('nuwiki.list.rejectCheckbox') +endfunction +function! nuwiki#commands#heading_add() abort + call s:exec_pos('nuwiki.heading.addLevel') +endfunction +function! nuwiki#commands#heading_remove() abort + call s:exec_pos('nuwiki.heading.removeLevel') +endfunction function! nuwiki#commands#next_task() abort let l:p = s:cursor_position() let l:args = [{ 'uri': l:p['textDocument']['uri'], 'position': l:p['position'] }] @@ -326,9 +346,12 @@ endfunction " ===== Generation + workspace ===== -function! nuwiki#commands#toc_generate() abort | call s:exec('nuwiki.toc.generate', [{'uri': s:buf_uri()}]) | endfunction -function! nuwiki#commands#links_generate() abort | call s:exec('nuwiki.links.generate', [{'uri': s:buf_uri()}]) | endfunction - +function! nuwiki#commands#toc_generate() abort + call s:exec('nuwiki.toc.generate', [{'uri': s:buf_uri()}]) +endfunction +function! nuwiki#commands#links_generate() abort + call s:exec('nuwiki.links.generate', [{'uri': s:buf_uri()}]) +endfunction function! nuwiki#commands#check_links() abort call s:exec('nuwiki.workspace.checkLinks', [{ 'uri': s:buf_uri() }], \ {n -> s:results_to_qf(n, 'broken links')}) @@ -386,11 +409,18 @@ endfunction " ===== HTML export ===== -function! nuwiki#commands#export_current() abort | call s:exec('nuwiki.export.currentToHtml', [{'uri': s:buf_uri()}]) | endfunction -function! nuwiki#commands#export_all() abort | call s:exec('nuwiki.export.allToHtml', [{'uri': s:buf_uri()}]) | endfunction -function! nuwiki#commands#export_all_force() abort | call s:exec('nuwiki.export.allToHtmlForce', [{'uri': s:buf_uri()}]) | endfunction -function! nuwiki#commands#export_rss() abort | call s:exec('nuwiki.export.rss', [{'uri': s:buf_uri()}]) | endfunction - +function! nuwiki#commands#export_current() abort + call s:exec('nuwiki.export.currentToHtml', [{'uri': s:buf_uri()}]) +endfunction +function! nuwiki#commands#export_all() abort + call s:exec('nuwiki.export.allToHtml', [{'uri': s:buf_uri()}]) +endfunction +function! nuwiki#commands#export_all_force() abort + call s:exec('nuwiki.export.allToHtmlForce', [{'uri': s:buf_uri()}]) +endfunction +function! nuwiki#commands#export_rss() abort + call s:exec('nuwiki.export.rss', [{'uri': s:buf_uri()}]) +endfunction function! nuwiki#commands#export_browse() abort call s:exec('nuwiki.export.browse', [{ 'uri': s:buf_uri() }], \ {n -> s:open_browser(n)}) @@ -432,11 +462,27 @@ endfunction " ===== §13.1 deferred ===== -function! nuwiki#commands#list_change_lvl() abort | call s:notify_deferred(':VimwikiListChangeLvl') | endfunction -function! nuwiki#commands#list_remove_done() abort | call s:notify_deferred(':VimwikiRemoveDone') | endfunction -function! nuwiki#commands#table_insert() abort | call s:notify_deferred(':VimwikiTable') | endfunction -function! nuwiki#commands#table_move_left() abort | call s:notify_deferred(':VimwikiTableMoveColumnLeft') | endfunction -function! nuwiki#commands#table_move_right() abort | call s:notify_deferred(':VimwikiTableMoveColumnRight') | endfunction -function! nuwiki#commands#colorize(color) abort | call s:notify_deferred(':VimwikiColorize') | endfunction -function! nuwiki#commands#paste_link() abort | call s:notify_deferred(':VimwikiPasteLink') | endfunction -function! nuwiki#commands#paste_url() abort | call s:notify_deferred(':VimwikiPasteUrl') | endfunction +function! nuwiki#commands#list_change_lvl() abort + call s:notify_deferred(':VimwikiListChangeLvl') +endfunction +function! nuwiki#commands#list_remove_done() abort + call s:notify_deferred(':VimwikiRemoveDone') +endfunction +function! nuwiki#commands#table_insert() abort + call s:notify_deferred(':VimwikiTable') +endfunction +function! nuwiki#commands#table_move_left() abort + call s:notify_deferred(':VimwikiTableMoveColumnLeft') +endfunction +function! nuwiki#commands#table_move_right() abort + call s:notify_deferred(':VimwikiTableMoveColumnRight') +endfunction +function! nuwiki#commands#colorize(color) abort + call s:notify_deferred(':VimwikiColorize') +endfunction +function! nuwiki#commands#paste_link() abort + call s:notify_deferred(':VimwikiPasteLink') +endfunction +function! nuwiki#commands#paste_url() abort + call s:notify_deferred(':VimwikiPasteUrl') +endfunction \ No newline at end of file diff --git a/scripts/test-keymaps-vim.sh b/scripts/test-keymaps-vim.sh new file mode 100755 index 0000000..9231ecb --- /dev/null +++ b/scripts/test-keymaps-vim.sh @@ -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 (``, +# `=`, `-`, …) 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('')` 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" <"$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 diff --git a/scripts/test-keymaps-vim.vim b/scripts/test-keymaps-vim.vim new file mode 100644 index 0000000..01d37e4 --- /dev/null +++ b/scripts/test-keymaps-vim.vim @@ -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 ``). LSP-roundtrip bindings (``, `=`, `-`, …) 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._finds_next_wikilink', { + \ 'lines': ['intro [[A]] more [[B]] end'], + \ 'cursor': [1, 1], + \ 'keys': "\", + \ 'expect_cursor_line': 1, + \ }) + +" ===== first press wraps bare word ===== + +call s:run('cr.wraps_bare_word_first_press', { + \ 'lines': ['MyPage rest'], + \ 'cursor': [1, 3], + \ 'keys': "\", + \ 'expect_line': '[[MyPage]] rest', + \ 'expect_at': 1, + \ }) + +" ===== Bullet continuation ===== + +call s:run('lists.o_continues_list_marker', { + \ 'lines': ['- first'], + \ 'cursor': [1, 1], + \ 'keys': "o\", + \ 'expect_lines': ['- first', '- '], + \ }) +call s:run('lists.o_continues_checkbox', { + \ 'lines': ['- [ ] first'], + \ 'cursor': [1, 1], + \ 'keys': "o\", + \ 'expect_lines': ['- [ ] first', '- [ ] '], + \ }) +call s:run('lists.O_continues_above', { + \ 'lines': ['- second'], + \ 'cursor': [1, 1], + \ 'keys': "O\", + \ '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!')