Major clean up and improvements to vimwiki compatibility and documentation.
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
" scripts/syntax-diag.vim — dump highlighting state to /tmp/nuwiki-syndiag.log
|
||||
"
|
||||
" Usage: open a .wiki file with VARIED markup (heading, *bold*, _italic_,
|
||||
" `code`), then:
|
||||
" :source scripts/syntax-diag.vim
|
||||
" Then paste /tmp/nuwiki-syndiag.log back.
|
||||
|
||||
redir! > /tmp/nuwiki-syndiag.log
|
||||
|
||||
echo '=== state ==='
|
||||
echo 'filetype=' . &filetype
|
||||
echo 'syntax=' . &syntax
|
||||
echo 'b:current_syntax=' . get(b:, 'current_syntax', '<unset>')
|
||||
echo 'syntax_on=' . exists('g:syntax_on')
|
||||
echo 'colors_name=' . get(g:, 'colors_name', '<unset>')
|
||||
echo 'background=' . &background
|
||||
echo 'termguicolors=' . &termguicolors
|
||||
echo 't_Co=' . &t_Co
|
||||
echo 'has(nvim)=' . has('nvim')
|
||||
echo 'g:loaded_nuwiki=' . get(g:, 'loaded_nuwiki', '<unset>')
|
||||
echo 'g:loaded_vimwiki=' . get(g:, 'loaded_vimwiki', '<unset>')
|
||||
|
||||
echo ''
|
||||
echo '=== every syntax/vimwiki.vim in runtimepath (first wins) ==='
|
||||
for s:p in split(&runtimepath, ',')
|
||||
let s:f = s:p . '/syntax/vimwiki.vim'
|
||||
if filereadable(s:f)
|
||||
echo s:f
|
||||
endif
|
||||
endfor
|
||||
|
||||
echo ''
|
||||
echo '=== :hi for our groups (NO silent — capture into redir) ==='
|
||||
for s:g in ['nuwikiHeading', 'nuwikiBold', 'nuwikiItalic', 'nuwikiCode',
|
||||
\ 'nuwikiPlaceholder', 'nuwikiCommentLine', 'nuwikiWikilink',
|
||||
\ 'Title', 'PreProc', 'String', 'Comment', 'Underlined', 'Normal']
|
||||
execute 'highlight ' . s:g
|
||||
endfor
|
||||
|
||||
echo ''
|
||||
echo '=== synID walk over current buffer (first 30 lines) ==='
|
||||
for s:ln in range(1, min([line('$'), 30]))
|
||||
let s:l = getline(s:ln)
|
||||
let s:segs = []
|
||||
let s:last = ''
|
||||
let s:start = 1
|
||||
for s:c in range(1, len(s:l) + 1)
|
||||
let s:g = s:c <= len(s:l) ? synIDattr(synIDtrans(synID(s:ln, s:c, 1)), 'name') : ''
|
||||
if s:g != s:last
|
||||
if s:start <= s:c - 1
|
||||
let s:txt = strpart(s:l, s:start - 1, s:c - s:start)
|
||||
call add(s:segs, s:last . ':' . s:txt)
|
||||
endif
|
||||
let s:last = s:g
|
||||
let s:start = s:c
|
||||
endif
|
||||
endfor
|
||||
echo printf('L%d %s', s:ln, join(s:segs, ' | '))
|
||||
endfor
|
||||
|
||||
echo ''
|
||||
echo '=== :syntax list (full) ==='
|
||||
syntax list
|
||||
|
||||
echo ''
|
||||
echo '=== :scriptnames (filtered) ==='
|
||||
let s:sn = execute('scriptnames')
|
||||
for s:line in split(s:sn, '\n')
|
||||
if s:line =~? 'vimwiki\|nuwiki\|syntax\|colors'
|
||||
echo s:line
|
||||
endif
|
||||
endfor
|
||||
|
||||
redir END
|
||||
echo 'Wrote /tmp/nuwiki-syndiag.log'
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/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…"
|
||||
# `</dev/null` so Vim's ex-mode doesn't hang waiting on stdin;
|
||||
# `timeout` so a misbehaving script can't stall CI for 30 minutes.
|
||||
NUWIKI_KEYMAP_RESULTS="$RESULTS" \
|
||||
timeout 30 vim -e -s -u "$VIMRC" -c "edit $SEED" -c "source $REPO_ROOT/scripts/test-keymaps-vim.vim" \
|
||||
</dev/null >"$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
|
||||
@@ -1,353 +0,0 @@
|
||||
" 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'],
|
||||
\ })
|
||||
|
||||
" ===== Insert-mode smart_return (Cluster 5) =====
|
||||
|
||||
call s:run('cr.continues_list_marker', {
|
||||
\ 'lines': ['- first'],
|
||||
\ 'cursor': [1, 7],
|
||||
\ 'keys': "A\<CR>second\<Esc>",
|
||||
\ 'expect_lines': ['- first', '- second'],
|
||||
\ })
|
||||
call s:run('cr.continues_checkbox', {
|
||||
\ 'lines': ['- [ ] first'],
|
||||
\ 'cursor': [1, 11],
|
||||
\ 'keys': "A\<CR>second\<Esc>",
|
||||
\ 'expect_lines': ['- [ ] first', '- [ ] second'],
|
||||
\ })
|
||||
call s:run('cr.breaks_on_empty_list_line', {
|
||||
\ 'lines': ['- '],
|
||||
\ 'cursor': [1, 2],
|
||||
\ 'keys': "A\<CR>plain\<Esc>",
|
||||
\ 'expect_lines': ['', 'plain'],
|
||||
\ })
|
||||
call s:run('cr.adds_new_table_row', {
|
||||
\ 'lines': ['| a | b |'],
|
||||
\ 'cursor': [1, 4],
|
||||
\ 'keys': "A\<CR>x\<Esc>",
|
||||
\ 'expect_lines': ['| a | b |', '|x | |'],
|
||||
\ })
|
||||
|
||||
" ===== Insert-mode smart_tab / smart_shift_tab (Cluster 6) =====
|
||||
|
||||
call s:run('tab.next_cell_in_table', {
|
||||
\ 'lines': ['| a | b |'],
|
||||
\ 'cursor': [1, 3],
|
||||
\ 'keys': "A\<Tab>x\<Esc>",
|
||||
\ 'expect_lines': ['| a | b |', '|x | |'],
|
||||
\ })
|
||||
call s:run('tab.cycles_to_next_row_when_not_last', {
|
||||
\ 'lines': ['| a | b |', '|---|---|', '| c | d |'],
|
||||
\ 'cursor': [1, 7],
|
||||
\ 'keys': "i\<Tab>x\<Esc>",
|
||||
\ 'expect_lines': ['| a | b |', '|---|---|', '|x c | d |'],
|
||||
\ })
|
||||
call s:run('tab.inserts_row_only_when_on_last_row', {
|
||||
\ 'lines': ['| a | b |', '|---|---|', '| c | d |'],
|
||||
\ 'cursor': [3, 7],
|
||||
\ 'keys': "i\<Tab>x\<Esc>",
|
||||
\ 'expect_lines': ['| a | b |', '|---|---|', '| c | d |', '|x | |'],
|
||||
\ })
|
||||
call s:run('tab.from_first_cell_moves_to_second', {
|
||||
\ 'lines': ['| a | b |'],
|
||||
\ 'cursor': [1, 3],
|
||||
\ 'keys': "i\<Tab>x\<Esc>",
|
||||
\ 'expect_lines': ['| a |x b |'],
|
||||
\ })
|
||||
call s:run('shift_tab.prev_cell_in_table', {
|
||||
\ 'lines': ['| a | b |'],
|
||||
\ 'cursor': [1, 7],
|
||||
\ 'keys': "i\<S-Tab>x\<Esc>",
|
||||
\ 'expect_lines': ['|x a | b |'],
|
||||
\ })
|
||||
|
||||
" ===== Named link commands (Cluster 1) =====
|
||||
|
||||
if exists(':VimwikiNextLink') == 2
|
||||
call s:record(1, 'cmd.VimwikiNextLink_defined', '')
|
||||
else
|
||||
call s:record(0, 'cmd.VimwikiNextLink_defined', ':VimwikiNextLink not registered')
|
||||
endif
|
||||
if exists(':VimwikiPrevLink') == 2
|
||||
call s:record(1, 'cmd.VimwikiPrevLink_defined', '')
|
||||
else
|
||||
call s:record(0, 'cmd.VimwikiPrevLink_defined', ':VimwikiPrevLink not registered')
|
||||
endif
|
||||
if exists(':VimwikiBaddLink') == 2
|
||||
call s:record(1, 'cmd.VimwikiBaddLink_defined', '')
|
||||
else
|
||||
call s:record(0, 'cmd.VimwikiBaddLink_defined', ':VimwikiBaddLink not registered')
|
||||
endif
|
||||
|
||||
" ===== Text objects (Cluster 3) =====
|
||||
" Exercise the pure helpers directly — visual-mode mark inspection in
|
||||
" `vim -e -s` is unreliable for the same reasons the Lua harness uses
|
||||
" the helper-level tests.
|
||||
|
||||
call s:set_buf(['= h1 =', 'a', '== h2 ==', 'b', '== h3 ==', 'c'])
|
||||
call s:record(
|
||||
\ nuwiki#textobjects#_heading_block(1, 0) ==# [1, 2]
|
||||
\ ? 1 : 0,
|
||||
\ 'textobj.heading_block_section_only',
|
||||
\ 'got ' . string(nuwiki#textobjects#_heading_block(1, 0)))
|
||||
call s:record(
|
||||
\ nuwiki#textobjects#_heading_block(1, 1) ==# [1, 6]
|
||||
\ ? 1 : 0,
|
||||
\ 'textobj.heading_block_subtree',
|
||||
\ 'got ' . string(nuwiki#textobjects#_heading_block(1, 1)))
|
||||
|
||||
let s:cells = nuwiki#textobjects#_cell_ranges('| a | b | c |')
|
||||
call s:record(
|
||||
\ len(s:cells) == 3 ? 1 : 0,
|
||||
\ 'textobj.cell_ranges_three_cells',
|
||||
\ len(s:cells) == 3 ? '' : 'got ' . string(s:cells))
|
||||
|
||||
let s:cell_idx = nuwiki#textobjects#_current_cell_for_line('| a | b | c |', 8)
|
||||
call s:record(
|
||||
\ s:cell_idx == 2 ? 1 : 0,
|
||||
\ 'textobj.current_cell_picks_middle',
|
||||
\ s:cell_idx == 2 ? '' : 'got idx=' . s:cell_idx)
|
||||
|
||||
call setline(1, ['before', '| a | b |', '| c | d |', '', 'after'])
|
||||
let s:bnd = nuwiki#textobjects#_table_bounds(2)
|
||||
call s:record(
|
||||
\ s:bnd ==# [2, 3] ? 1 : 0,
|
||||
\ 'textobj.table_bounds_walks_contig_block',
|
||||
\ s:bnd ==# [2, 3] ? '' : 'got ' . string(s:bnd))
|
||||
|
||||
" Operator-pending: `dah` should delete the heading section.
|
||||
call s:set_buf(['= h1 =', 'body', '= h2 =', 'rest'])
|
||||
call cursor(1, 1)
|
||||
call feedkeys('dah', 'tx')
|
||||
let s:dah = getline(1, '$')
|
||||
call s:record(
|
||||
\ s:dah ==# ['= h2 =', 'rest'] || s:dah ==# ['', '= h2 =', 'rest']
|
||||
\ ? 1 : 0,
|
||||
\ 'textobj.dah_deletes_heading_section',
|
||||
\ 'got ' . string(s:dah))
|
||||
|
||||
" ===== Folding =====
|
||||
|
||||
call setline(1, ['= h1 =', 'a', '== h2 ==', 'b', 'c'])
|
||||
let &l:foldmethod = 'expr'
|
||||
let &l:foldexpr = 'nuwiki#folding#expr()'
|
||||
call s:record(
|
||||
\ foldlevel(1) > 0 ? 1 : 0,
|
||||
\ 'folding.heading_starts_fold',
|
||||
\ 'foldlevel(1)=' . foldlevel(1))
|
||||
call s:record(
|
||||
\ foldlevel(2) > 0 ? 1 : 0,
|
||||
\ 'folding.body_inherits_fold',
|
||||
\ 'foldlevel(2)=' . foldlevel(2))
|
||||
" ftplugin sets `foldlevel=99` so every heading-block fold starts open
|
||||
" rather than collapsed on file open.
|
||||
call s:record(
|
||||
\ &l:foldlevel == 99 ? 1 : 0,
|
||||
\ 'folding.starts_with_all_folds_open',
|
||||
\ '&l:foldlevel=' . &l:foldlevel)
|
||||
|
||||
" ===== Conceal =====
|
||||
|
||||
" The ftplugin reveals the cursor line in normal mode (concealcursor has no
|
||||
" `n`) so left/right motions move over visible characters instead of stalling
|
||||
" on hidden conceal regions like a wikilink's `[[`/`]]`.
|
||||
call s:record(
|
||||
\ &l:conceallevel == 2 ? 1 : 0,
|
||||
\ 'conceal.level_is_two',
|
||||
\ '&l:conceallevel=' . &l:conceallevel)
|
||||
call s:record(
|
||||
\ &l:concealcursor !~# 'n' ? 1 : 0,
|
||||
\ 'conceal.cursor_line_revealed_in_normal_mode',
|
||||
\ '&l:concealcursor=' . &l:concealcursor)
|
||||
|
||||
" ===== Wiki picker (config-driven, no LSP) =====
|
||||
|
||||
" The picker builds its list straight from config so it works from any buffer
|
||||
" before the LSP (which only starts on a wiki buffer) is running.
|
||||
let g:nuwiki_wikis = [
|
||||
\ {'name': 'Personal', 'root': '/tmp/nuwiki-a'},
|
||||
\ {'name': 'Work', 'root': '/tmp/nuwiki-b', 'file_extension': 'md'},
|
||||
\ ]
|
||||
let s:wl = nuwiki#commands#wiki_list()
|
||||
call s:record(
|
||||
\ len(s:wl) == 2 ? 1 : 0,
|
||||
\ 'wiki_select.lists_all_configured_wikis',
|
||||
\ 'count=' . len(s:wl))
|
||||
call s:record(
|
||||
\ get(get(s:wl, 0, {}), 'name', '') ==# 'Personal'
|
||||
\ && get(get(s:wl, 1, {}), 'name', '') ==# 'Work' ? 1 : 0,
|
||||
\ 'wiki_select.preserves_names_and_order',
|
||||
\ 'names=' . string(map(copy(s:wl), 'get(v:val, "name", "")')))
|
||||
call s:record(
|
||||
\ get(get(s:wl, 1, {}), 'ext', '') ==# '.md' ? 1 : 0,
|
||||
\ 'wiki_select.honours_per_wiki_extension',
|
||||
\ 'ext=' . get(get(s:wl, 1, {}), 'ext', ''))
|
||||
unlet g:nuwiki_wikis
|
||||
|
||||
" ===== 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!')
|
||||
@@ -1,529 +0,0 @@
|
||||
-- scripts/test-keymaps.lua — driven by scripts/test-keymaps.sh.
|
||||
--
|
||||
-- Headless Neovim harness for the buffer-local keymaps registered by
|
||||
-- `ftplugin/vimwiki.vim` + `lua/nuwiki/keymaps.lua`. Each case sets up
|
||||
-- the buffer, places the cursor, fires the key sequence via
|
||||
-- `nvim_feedkeys`, waits for any LSP round-trip, and asserts the
|
||||
-- expected post-state.
|
||||
--
|
||||
-- Pass/fail summary is written to the path passed as argv[1] so the
|
||||
-- shell wrapper can read + render it without trying to capture nvim's
|
||||
-- TUI output.
|
||||
|
||||
local OUT = vim.env.NUWIKI_KEYMAP_RESULTS or '/tmp/nuwiki-keymap-results.txt'
|
||||
local out_lines = {}
|
||||
local pass = 0
|
||||
local fail = 0
|
||||
|
||||
local function record(ok, name, detail)
|
||||
local suffix = (detail and detail ~= '') and (' — ' .. detail) or ''
|
||||
table.insert(out_lines, (ok and 'PASS' or 'FAIL') .. ' ' .. name .. suffix)
|
||||
if ok then pass = pass + 1 else fail = fail + 1 end
|
||||
end
|
||||
|
||||
local function set_buf(lines)
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
|
||||
end
|
||||
|
||||
local function get_lines()
|
||||
return vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
end
|
||||
|
||||
local function get_line(n)
|
||||
return vim.api.nvim_buf_get_lines(0, n - 1, n, false)[1] or ''
|
||||
end
|
||||
|
||||
local function send(keys)
|
||||
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), 'x', false)
|
||||
end
|
||||
|
||||
local function sleep(ms)
|
||||
vim.cmd('sleep ' .. ms .. 'm')
|
||||
end
|
||||
|
||||
--- Run one case: lines + cursor (1-based) + key sequence + expected
|
||||
--- post-state. `wait_ms` defaults to 800 (enough for an LSP
|
||||
--- executeCommand round-trip on a hot server).
|
||||
local function run(name, opts)
|
||||
local ok, err = pcall(function()
|
||||
set_buf(opts.lines)
|
||||
vim.api.nvim_win_set_cursor(0, opts.cursor or { 1, 0 })
|
||||
send(opts.keys)
|
||||
sleep(opts.wait_ms or 800)
|
||||
local actual_lines = get_lines()
|
||||
if opts.expect_lines then
|
||||
if not vim.deep_equal(actual_lines, opts.expect_lines) then
|
||||
error(string.format('lines mismatch\n expected: %s\n actual: %s',
|
||||
vim.inspect(opts.expect_lines), vim.inspect(actual_lines)))
|
||||
end
|
||||
end
|
||||
if opts.expect_line and opts.expect_at then
|
||||
local got = get_line(opts.expect_at)
|
||||
if got ~= opts.expect_line then
|
||||
error(string.format('line %d mismatch\n expected: %q\n actual: %q',
|
||||
opts.expect_at, opts.expect_line, got))
|
||||
end
|
||||
end
|
||||
if opts.expect_cursor_line then
|
||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
if lnum ~= opts.expect_cursor_line then
|
||||
error(string.format('cursor line mismatch — expected %d, got %d',
|
||||
opts.expect_cursor_line, lnum))
|
||||
end
|
||||
end
|
||||
if opts.expect_buf_path then
|
||||
local got = vim.api.nvim_buf_get_name(0)
|
||||
if not got:find(opts.expect_buf_path, 1, true) then
|
||||
error(string.format('buffer path mismatch\n expected substring: %q\n actual: %q',
|
||||
opts.expect_buf_path, got))
|
||||
end
|
||||
end
|
||||
end)
|
||||
record(ok, name, ok and '' or tostring(err))
|
||||
end
|
||||
|
||||
local function get_lsp_clients(opts)
|
||||
local fn = vim.lsp.get_clients or vim.lsp.get_active_clients
|
||||
if not fn then return {} end
|
||||
return fn(opts)
|
||||
end
|
||||
|
||||
local function wait_for_lsp(timeout_ms)
|
||||
local deadline = vim.loop.now() + (timeout_ms or 5000)
|
||||
while vim.loop.now() < deadline do
|
||||
local clients = get_lsp_clients({ name = 'nuwiki', bufnr = 0 })
|
||||
if #clients > 0 and clients[1].server_capabilities
|
||||
and clients[1].server_capabilities.executeCommandProvider then
|
||||
return clients[1]
|
||||
end
|
||||
sleep(100)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ===== Capture server log_message events =====
|
||||
local server_log = {}
|
||||
local orig_handler = vim.lsp.handlers['window/logMessage']
|
||||
vim.lsp.handlers['window/logMessage'] = function(err, result, ctx, cfg)
|
||||
if result and result.message then
|
||||
table.insert(server_log, '[' .. tostring(result.type) .. '] ' .. result.message)
|
||||
end
|
||||
if orig_handler then return orig_handler(err, result, ctx, cfg) end
|
||||
end
|
||||
|
||||
-- ===== Smoke: LSP attached =====
|
||||
|
||||
vim.defer_fn(function()
|
||||
local client = wait_for_lsp()
|
||||
if not client then
|
||||
record(false, 'lsp.attached', 'no nuwiki client after 5s')
|
||||
vim.fn.writefile(out_lines, OUT)
|
||||
vim.cmd('qall!')
|
||||
return
|
||||
end
|
||||
record(true, 'lsp.attached', 'client=' .. client.name)
|
||||
|
||||
-- ===== Lists =====
|
||||
|
||||
run('list.toggle_via_C-Space', {
|
||||
lines = { '= test =', '- [ ] task' },
|
||||
cursor = { 2, 0 },
|
||||
keys = '<C-Space>',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 2,
|
||||
})
|
||||
run('list.toggle_via_C-@', {
|
||||
lines = { '= test =', '- [ ] task' },
|
||||
cursor = { 2, 0 },
|
||||
keys = '<C-@>',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 2,
|
||||
})
|
||||
run('list.toggle_via_Nul', {
|
||||
lines = { '= test =', '- [ ] task' },
|
||||
cursor = { 2, 0 },
|
||||
keys = '<Nul>',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 2,
|
||||
})
|
||||
run('list.cycle_via_gln', {
|
||||
lines = { '- [ ] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gln',
|
||||
expect_line = '- [.] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.reject_via_glx', {
|
||||
lines = { '- [ ] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glx',
|
||||
expect_line = '- [-] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.toggle_round_trip', {
|
||||
lines = { '- [X] done' },
|
||||
cursor = { 1, 0 },
|
||||
keys = '<C-Space>',
|
||||
expect_line = '- [ ] done',
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Next task (gnt) =====
|
||||
|
||||
run('list.gnt_jumps_to_unfinished', {
|
||||
lines = { '- [X] done', '- [ ] todo', '- [ ] later' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gnt',
|
||||
expect_cursor_line = 2,
|
||||
wait_ms = 600,
|
||||
})
|
||||
|
||||
-- ===== Heading levels =====
|
||||
|
||||
run('heading.add_level_with_=', {
|
||||
lines = { '= H1 =' },
|
||||
cursor = { 1, 0 },
|
||||
keys = '=',
|
||||
expect_line = '== H1 ==',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('heading.remove_level_with_-', {
|
||||
lines = { '== H2 ==' },
|
||||
cursor = { 1, 0 },
|
||||
keys = '-',
|
||||
expect_line = '= H2 =',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('heading.add_level_clamps_at_6', {
|
||||
lines = { '====== H6 ======' },
|
||||
cursor = { 1, 0 },
|
||||
keys = '=',
|
||||
expect_line = '====== H6 ======', -- unchanged
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Header nav (pure Lua, no LSP) =====
|
||||
|
||||
run('headers.]]_jumps_to_next', {
|
||||
lines = { '= One =', 'body', '== Two ==', 'body', '= Three =' },
|
||||
cursor = { 1, 0 },
|
||||
keys = ']]',
|
||||
expect_cursor_line = 3,
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('headers.[[_jumps_to_prev', {
|
||||
lines = { '= One =', 'body', '== Two ==', 'body', '= Three =' },
|
||||
cursor = { 5, 0 },
|
||||
keys = '[[',
|
||||
expect_cursor_line = 3,
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('headers.]u_jumps_to_parent', {
|
||||
lines = { '= Parent =', '== Child ==', 'body' },
|
||||
cursor = { 3, 0 },
|
||||
keys = ']u',
|
||||
expect_cursor_line = 1,
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Link nav (pure Lua) =====
|
||||
|
||||
run('links.<Tab>_jumps_to_next_wikilink', {
|
||||
lines = { 'intro [[A]] more [[B]] end' },
|
||||
cursor = { 1, 0 },
|
||||
keys = '<Tab>',
|
||||
expect_cursor_line = 1,
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== <CR> two-step =====
|
||||
|
||||
run('cr.wraps_bare_word_first_press', {
|
||||
lines = { 'MyPage rest' },
|
||||
cursor = { 1, 3 },
|
||||
keys = '<CR>',
|
||||
expect_line = '[[MyPage]] rest',
|
||||
expect_at = 1,
|
||||
wait_ms = 200,
|
||||
})
|
||||
|
||||
-- ===== Link helpers (§13.1 Cluster C) =====
|
||||
|
||||
run('links.normalize_via_+', {
|
||||
lines = { 'MyPage rest' },
|
||||
cursor = { 1, 3 },
|
||||
keys = '+',
|
||||
expect_line = '[[MyPage]] rest',
|
||||
expect_at = 1,
|
||||
wait_ms = 200,
|
||||
})
|
||||
|
||||
-- ===== Bullet continuation (o/O) =====
|
||||
|
||||
run('lists.o_continues_list_marker', {
|
||||
lines = { '- first' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'o<Esc>',
|
||||
expect_lines = { '- first', '- ' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('lists.o_continues_checkbox', {
|
||||
lines = { '- [ ] first' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'o<Esc>',
|
||||
expect_lines = { '- [ ] first', '- [ ] ' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('lists.O_continues_above', {
|
||||
lines = { '- second' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'O<Esc>',
|
||||
expect_lines = { '- ', '- second' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Insert-mode list bindings (Cluster 2) =====
|
||||
|
||||
run('insert.C-L_C-M_adds_checkbox', {
|
||||
lines = { '- task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'A<C-L><C-M><Esc>',
|
||||
expect_line = '- [ ] task',
|
||||
expect_at = 1,
|
||||
wait_ms = 200,
|
||||
})
|
||||
run('insert.C-L_C-M_toggles_existing_checkbox', {
|
||||
lines = { '- [ ] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'A<C-L><C-M><Esc>',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
-- End-to-end regression for changeSymbol over LSP — direct call
|
||||
-- (no keymap) so we can spot a stale `bin/nuwiki-ls` or a dispatch
|
||||
-- error fast. Toggle has separate coverage above via `<C-Space>`.
|
||||
do
|
||||
local ok, err = pcall(function()
|
||||
set_buf({ '- [ ] item', '- [ ] two' })
|
||||
sleep(200)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
require('nuwiki.commands').list_change_symbol('*', false)
|
||||
sleep(1500)
|
||||
local got = get_line(1)
|
||||
if got ~= '* [ ] item' then
|
||||
error(string.format('change_symbol failed: got %q', got))
|
||||
end
|
||||
end)
|
||||
record(ok, 'direct.list_change_symbol_via_lsp', ok and '' or tostring(err))
|
||||
end
|
||||
run('insert.C-L_C-J_cycles_marker_forward', {
|
||||
lines = { '- item' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'A<C-L><C-J><Esc>',
|
||||
expect_line = '* item',
|
||||
expect_at = 1,
|
||||
wait_ms = 1500,
|
||||
})
|
||||
run('insert.C-L_C-K_cycles_marker_backward', {
|
||||
-- From `-` going backward wraps to the end of the order (`I)`).
|
||||
lines = { '- item' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'A<C-L><C-K><Esc>',
|
||||
expect_line = 'I) item',
|
||||
expect_at = 1,
|
||||
wait_ms = 1500,
|
||||
})
|
||||
|
||||
-- ===== Text objects (Cluster 3) =====
|
||||
-- Exercise the pure helpers directly (cell ranges, heading block,
|
||||
-- table bounds). The visual-mode integration is verified in a single
|
||||
-- end-to-end case via mark inspection, which exercises the full
|
||||
-- operator-pending path.
|
||||
|
||||
local function tobj_case(name, fn)
|
||||
local ok, err = pcall(fn)
|
||||
record(ok, name, ok and '' or tostring(err))
|
||||
end
|
||||
|
||||
tobj_case('textobj.heading_block_section_only', function()
|
||||
set_buf({ '= h1 =', 'a', '== h2 ==', 'b', '== h3 ==', 'c' })
|
||||
local s, e = require('nuwiki.textobjects')._heading_block(1, false)
|
||||
if not (s == 1 and e == 2) then
|
||||
error(string.format('section_only: got (%s,%s) want (1,2)', s, e))
|
||||
end
|
||||
end)
|
||||
tobj_case('textobj.heading_block_subtree', function()
|
||||
set_buf({ '= h1 =', 'a', '== h2 ==', 'b', '== h3 ==', 'c' })
|
||||
local s, e = require('nuwiki.textobjects')._heading_block(1, true)
|
||||
if not (s == 1 and e == 6) then
|
||||
error(string.format('subtree: got (%s,%s) want (1,6)', s, e))
|
||||
end
|
||||
end)
|
||||
tobj_case('textobj.cell_ranges_three_cells', function()
|
||||
local r = require('nuwiki.textobjects')._cell_ranges('| a | b | c |')
|
||||
if #r ~= 3 then error('want 3 cells, got ' .. #r) end
|
||||
-- Inside each cell, content includes the surrounding spaces.
|
||||
if not (r[1][1] == 2 and r[2][1] > r[1][2] and r[3][1] > r[2][2]) then
|
||||
error('cell ordering broken: ' .. vim.inspect(r))
|
||||
end
|
||||
end)
|
||||
tobj_case('textobj.current_cell_picks_middle', function()
|
||||
-- `| a | b | c |` cursor on the `b` (1-based col 8).
|
||||
local idx = require('nuwiki.textobjects')._current_cell_for_line(
|
||||
'| a | b | c |', 8)
|
||||
if idx ~= 2 then error('expected cell 2, got ' .. tostring(idx)) end
|
||||
end)
|
||||
tobj_case('textobj.table_bounds_walks_contig_block', function()
|
||||
set_buf({ 'before', '| a | b |', '| c | d |', '', 'after' })
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
local s, e = require('nuwiki.textobjects')._table_bounds(2)
|
||||
if not (s == 2 and e == 3) then
|
||||
error(string.format('want (2,3) got (%s,%s)', s, e))
|
||||
end
|
||||
end)
|
||||
|
||||
-- One end-to-end: operator-pending `dah` should delete the heading
|
||||
-- section. This avoids the visual-mode `'<`/`'>` mark dance and
|
||||
-- relies on Vim's actual operator pipeline to verify the keymap is
|
||||
-- correctly wired into `omap`.
|
||||
tobj_case('textobj.dah_deletes_heading_section', function()
|
||||
set_buf({ '= h1 =', 'body', '= h2 =', 'rest' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
send('dah')
|
||||
sleep(100)
|
||||
local got = get_lines()
|
||||
-- After deleting heading section (lines 1-2), buffer should be the
|
||||
-- remaining two lines. There may be a trailing empty line depending
|
||||
-- on linewise delete semantics; allow both shapes.
|
||||
if not (vim.deep_equal(got, { '= h2 =', 'rest' })
|
||||
or vim.deep_equal(got, { '', '= h2 =', 'rest' })) then
|
||||
error('dah result: ' .. vim.inspect(got))
|
||||
end
|
||||
end)
|
||||
|
||||
-- ===== Smart <CR> (Cluster 5) =====
|
||||
|
||||
run('cr.continues_list_marker', {
|
||||
lines = { '- first' },
|
||||
cursor = { 1, 7 }, -- end of line
|
||||
keys = 'A<CR>second<Esc>',
|
||||
expect_lines = { '- first', '- second' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('cr.continues_checkbox', {
|
||||
lines = { '- [ ] first' },
|
||||
cursor = { 1, 11 },
|
||||
keys = 'A<CR>second<Esc>',
|
||||
expect_lines = { '- [ ] first', '- [ ] second' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('cr.breaks_on_empty_list_line', {
|
||||
lines = { '- ' },
|
||||
cursor = { 1, 2 },
|
||||
keys = 'A<CR>plain<Esc>',
|
||||
expect_lines = { '', 'plain' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('cr.adds_new_table_row', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 4 }, -- inside `a`
|
||||
keys = 'A<CR>x<Esc>',
|
||||
-- New row inserted below with empty cells; cursor jumps to first.
|
||||
expect_lines = { '| a | b |', '|x | |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Insert-mode table navigation (Cluster 6) =====
|
||||
|
||||
run('tab.next_cell_in_table', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 2 }, -- inside cell `a`
|
||||
keys = 'A<Tab>x<Esc>',
|
||||
-- After A: cursor at end. <Tab>: no cell-start strictly after end
|
||||
-- so creates new row. Try cursor on inside-first-cell case below.
|
||||
-- Here cursor at end has no next cell so a new row is added.
|
||||
expect_lines = { '| a | b |', '|x | |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.from_first_cell_moves_to_second', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'lli<Tab>x<Esc>',
|
||||
-- `ll` from col 0 → col 2 (on `a`). `i` enters insert. `<Tab>`
|
||||
-- jumps to col after next `|`. `x` types in second cell.
|
||||
expect_lines = { '| a |x b |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.cycles_to_next_row_when_not_last', {
|
||||
lines = { '| a | b |', '|---|---|', '| c | d |' },
|
||||
cursor = { 1, 6 }, -- inside last cell of header row
|
||||
keys = 'i<Tab>x<Esc>',
|
||||
-- Past the final cell of row 1: skip separator, land in first cell of row 3.
|
||||
expect_lines = { '| a | b |', '|---|---|', '|x c | d |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.inserts_row_only_when_on_last_row', {
|
||||
lines = { '| a | b |', '|---|---|', '| c | d |' },
|
||||
cursor = { 3, 6 }, -- inside last cell of last data row
|
||||
keys = 'i<Tab>x<Esc>',
|
||||
expect_lines = { '| a | b |', '|---|---|', '| c | d |', '|x | |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('shift_tab.prev_cell_in_table', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 6 }, -- inside cell `b`
|
||||
keys = 'i<S-Tab>x<Esc>',
|
||||
-- Cursor lands just after the previous cell's `|`; typing `x`
|
||||
-- inserts at col 2 (before the leading space inside cell 1).
|
||||
expect_lines = { '|x a | b |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.outside_table_passes_through', {
|
||||
lines = { 'plain text' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'i<Tab>x<Esc>',
|
||||
-- Default Tab inserts a literal tab.
|
||||
expect_lines = { '\tx' .. 'plain text' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Diary nav =====
|
||||
-- Tested via the LSP open-* responses; can't fully validate without
|
||||
-- a populated diary, but at least confirm the maps fire without
|
||||
-- error.
|
||||
|
||||
-- ===== Wiki picker (config-driven, no LSP) =====
|
||||
-- The picker must build its list from config so it works from any buffer
|
||||
-- before the server attaches.
|
||||
tobj_case('wiki_select.config_list_reads_g_var', function()
|
||||
vim.g.nuwiki_wikis = {
|
||||
{ name = 'Personal', root = '/tmp/nuwiki-a' },
|
||||
{ name = 'Work', root = '/tmp/nuwiki-b', file_extension = 'md' },
|
||||
}
|
||||
local list = require('nuwiki.config').wiki_list()
|
||||
vim.g.nuwiki_wikis = nil
|
||||
if #list ~= 2 then error('want 2 wikis, got ' .. #list) end
|
||||
if list[1].name ~= 'Personal' or list[2].name ~= 'Work' then
|
||||
error('names/order broken: ' .. vim.inspect(list))
|
||||
end
|
||||
if list[2].ext ~= '.md' then
|
||||
error('per-wiki extension not honoured: ' .. tostring(list[2].ext))
|
||||
end
|
||||
end)
|
||||
|
||||
-- ===== Wrap up =====
|
||||
|
||||
table.insert(out_lines, '')
|
||||
-- Dump captured server log_messages only on failure — useful for
|
||||
-- diagnosing dispatch errors that the LSP swallows into log_message.
|
||||
if fail > 0 and #server_log > 0 then
|
||||
table.insert(out_lines, '--- server log_messages ---')
|
||||
for _, l in ipairs(server_log) do
|
||||
table.insert(out_lines, l)
|
||||
end
|
||||
table.insert(out_lines, '')
|
||||
end
|
||||
table.insert(out_lines, string.format('SUMMARY: %d passed, %d failed', pass, fail))
|
||||
vim.fn.writefile(out_lines, OUT)
|
||||
vim.cmd(fail == 0 and 'qall!' or 'cquit!')
|
||||
end, 1500)
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# scripts/test-keymaps.sh — drive scripts/test-keymaps.lua under
|
||||
# headless Neovim and report pass/fail per buffer-local keymap.
|
||||
#
|
||||
# The Lua harness needs a real LSP attachment (most keymaps dispatch
|
||||
# `workspace/executeCommand`), so we:
|
||||
# 1. ensure `bin/nuwiki-ls` is built (release),
|
||||
# 2. spin up a scratch wiki under a temp dir,
|
||||
# 3. invoke `nvim --headless` with a minimal init that loads the
|
||||
# plugin + calls `setup()`, then sources the harness Lua.
|
||||
#
|
||||
# Exit code mirrors the harness: 0 on green, 1 on any failure.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP="$(mktemp -d -t nuwiki-keymap-test-XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
log() { printf '\033[1;34m[keymap-test]\033[0m %s\n' "$*"; }
|
||||
|
||||
# Always rebuild the LSP binary so harness runs against current sources.
|
||||
# A previous version only built when the symlink was missing, which let
|
||||
# stale binaries pass tests against unrelated code — incremental cargo
|
||||
# build is fast, so just always run it.
|
||||
BIN="$REPO_ROOT/bin/nuwiki-ls"
|
||||
log 'building nuwiki-ls (release, incremental)…'
|
||||
cargo build --release -p nuwiki-ls --manifest-path "$REPO_ROOT/Cargo.toml" >/dev/null
|
||||
mkdir -p "$REPO_ROOT/bin"
|
||||
ln -sf "$REPO_ROOT/target/release/nuwiki-ls" "$BIN"
|
||||
|
||||
mkdir -p "$TMP/wiki"
|
||||
echo "= seed =" > "$TMP/wiki/index.wiki"
|
||||
|
||||
INIT="$TMP/init.lua"
|
||||
cat > "$INIT" <<EOF
|
||||
-- Keep the harness self-contained; no plugin manager required.
|
||||
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||
vim.g.nuwiki_binary_path = '$BIN'
|
||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki' })
|
||||
EOF
|
||||
|
||||
RESULTS="$TMP/results.txt"
|
||||
log "running harness…"
|
||||
|
||||
# `nvim --headless -u init.lua FILE +luafile …` — the harness's
|
||||
# `vim.defer_fn` lets the LSP attach before tests fire, then writes
|
||||
# the results file and exits. Output path is plumbed through env.
|
||||
# Wrapped in `timeout` so a hung LSP / harness fails fast instead of
|
||||
# stalling CI runners indefinitely.
|
||||
NUWIKI_KEYMAP_RESULTS="$RESULTS" \
|
||||
timeout 60 nvim --clean -u "$INIT" --headless "$TMP/wiki/index.wiki" \
|
||||
-c "luafile $REPO_ROOT/scripts/test-keymaps.lua" \
|
||||
>"$TMP/nvim.log" 2>&1 || true
|
||||
|
||||
if [[ ! -f "$RESULTS" ]]; then
|
||||
echo 'keymap test harness produced no output' >&2
|
||||
echo '--- nvim log ---' >&2
|
||||
cat "$TMP/nvim.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat "$RESULTS"
|
||||
|
||||
if grep -qE '^SUMMARY: [0-9]+ passed, 0 failed' "$RESULTS"; then
|
||||
exit 0
|
||||
fi
|
||||
exit 1
|
||||
Reference in New Issue
Block a user