0dfe0910fc
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>
310 lines
9.7 KiB
VimL
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!')
|