Files
nuwiki/scripts/test-keymaps-vim.vim
T
gffranco 0dfe0910fc fix(tables): cycle Tab into the next row instead of inserting one
Pressing <Tab> past the last cell unconditionally appended a new row
below, even when there was already a next row to jump into. Tabbing
through a header → separator → body table from the header row
inserted a blank row between header and separator instead of landing
in the first body cell.

After the final cell, walk forward from the current row to the end
of the table, skip any separator row, and jump to the first cell of
the next data row. Only insert a fresh row when the cursor is on the
table's last row (or only separator rows follow). Applied to both
the Vim (autoload/nuwiki/commands.vim) and Neovim (lua/nuwiki/
commands.lua) sides so the two harnesses stay in sync; covered by
new keymap tests in scripts/test-keymaps-vim.vim and test-keymaps.lua.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 22:55:40 -03:00

310 lines
9.7 KiB
VimL

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