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!')