" development/tests/test-keymaps-vim.vim — driven by development/tests/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 development/tests/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 " ===== Documented command surface ===== " Every command must be reachable in BOTH the canonical :Nuwiki* form and " the vimwiki-compatible :Vimwiki* form. The suffixes below are the command " list minus the prefix. :NuwikiInstall is the one exception — it's a nuwiki-only " maintenance command with no :Vimwiki* alias, so it's checked separately. let s:both_forms = [ \ 'Index', 'TabIndex', 'UISelect', 'Goto', 'FollowLink', 'Backlinks', \ 'SplitLink', 'VSplitLink', 'TabnewLink', 'TabDropLink', 'GoBackLink', \ 'NextLink', 'PrevLink', 'BaddLink', 'RenameFile', 'DeleteFile', \ 'MakeDiaryNote', 'TabMakeDiaryNote', 'MakeYesterdayDiaryNote', \ 'MakeTomorrowDiaryNote', \ 'DiaryIndex', 'DiaryGenerateLinks', 'DiaryNextDay', 'DiaryPrevDay', \ 'TOC', 'GenerateLinks', 'CheckLinks', 'SearchTags', 'GenerateTagLinks', \ 'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss', \ 'NormalizeLink', 'RenumberList', 'RenumberAllLists', \ 'ListToggle', 'IncrementListItem', 'DecrementListItem', \ 'CatUrl', \ ] for s:suffix in s:both_forms for s:prefix in ['Nuwiki', 'Vimwiki'] let s:cmd = s:prefix . s:suffix let s:ok = exists(':' . s:cmd) == 2 call s:record(s:ok ? 1 : 0, 'surface.' . s:cmd, \ s:ok ? '' : ':' . s:cmd . ' not registered') endfor endfor let s:ok = exists(':NuwikiInstall') == 2 call s:record(s:ok ? 1 : 0, 'surface.NuwikiInstall', \ s:ok ? '' : ':NuwikiInstall not registered') " Asymmetric / compat-only spellings: the canonical :Nuwiki* name differs " from the upstream :Vimwiki* name (or has no canonical counterpart because " nuwiki already exposes it under a clearer name). for s:cmd in [ \ 'NuwikiTableAlign', 'VimwikiTableAlignQ', 'VimwikiTableAlignW', \ 'NuwikiChangeSymbol', 'VimwikiChangeSymbolTo', 'VimwikiListChangeSymbolI', \ 'NuwikiChangeSymbolInList', 'VimwikiChangeSymbolInListTo', \ 'VimwikiDeleteLink', 'VimwikiRenameLink', 'VimwikiGenerateTags', \ 'NuwikiRemoveCheckbox', 'VimwikiRemoveSingleCB', \ 'NuwikiRemoveCheckboxInList', 'VimwikiRemoveCBInList', \ ] let s:ok = exists(':' . s:cmd) == 2 call s:record(s:ok ? 1 : 0, 'surface.' . s:cmd, \ s:ok ? '' : ':' . s:cmd . ' not registered') endfor " ===== Pure-VimL command invocation (cursor movement) ===== " :NuwikiNextLink / :NuwikiPrevLink wrap search('\[\[', …) so they run " entirely in VimL and move the cursor without touching the LSP. call s:set_buf(['intro [[A]] more [[B]] end']) call cursor(1, 1) silent NuwikiNextLink call s:record( \ col('.') == 7 ? 1 : 0, \ 'invoke.NuwikiNextLink_jumps_to_first_link', \ 'col=' . col('.')) silent NuwikiNextLink call s:record( \ col('.') == 18 ? 1 : 0, \ 'invoke.NuwikiNextLink_jumps_to_second_link', \ 'col=' . col('.')) silent NuwikiPrevLink call s:record( \ col('.') == 7 ? 1 : 0, \ 'invoke.NuwikiPrevLink_jumps_back', \ 'col=' . col('.')) " The :Vimwiki* alias must drive the identical motion. call cursor(1, 1) silent VimwikiNextLink call s:record( \ col('.') == 7 ? 1 : 0, \ 'invoke.VimwikiNextLink_jumps_to_first_link', \ 'col=' . col('.')) " ===== Documented mapping surface ===== " Every documented mapping must resolve to a buffer-local mapping in the " mode(s) the doc lists it under. The mouse group is opt-in — the shell " wrapper sets g:nuwiki_mouse_mappings before the ftplugin loads. function! s:has_map(lhs, mode) abort let l:d = maparg(a:lhs, a:mode, 0, 1) return !empty(l:d) && get(l:d, 'buffer', 0) endfunction let s:mapping_surface = [ \ ['', 'n'], ['', 'n'], ['', 'n'], ['', 'n'], \ ['', 'n'], ['', 'n'], ['', 'n'], ['+', 'nx'], \ ['', 'nx'], ['', 'nx'], ['', 'nx'], \ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'], \ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'], \ ['glr', 'n'], ['gLr', 'n'], ['gl', 'n'], ['gL', 'n'], \ ['o', 'n'], ['O', 'n'], \ ['=', 'n'], ['-', 'n'], [']]', 'n'], ['[[', 'n'], \ [']=', 'n'], ['[=', 'n'], [']u', 'n'], ['[u', 'n'], \ ['gqq', 'n'], ['gq1', 'n'], ['gww', 'n'], ['gw1', 'n'], \ ['', 'n'], ['', 'n'], \ ['ww', 'n'], ['ws', 'n'], ['wi', 'n'], \ ['ww', 'n'], ['wy', 'n'], \ ['wt', 'n'], ['wi', 'n'], \ ['wn', 'n'], ['wd', 'n'], ['wr', 'n'], \ ['wh', 'n'], ['whh', 'n'], ['wha', 'n'], \ ['wc', 'nx'], ['', 'n'], ['', 'n'], \ ['<2-LeftMouse>', 'n'], ['', 'n'], ['', 'n'], \ ['', 'n'], ['', 'n'], \ ['ah', 'ox'], ['ih', 'ox'], ['aH', 'ox'], ['iH', 'ox'], \ ['al', 'ox'], ['il', 'ox'], ['a\', 'ox'], ['i\', 'ox'], \ ['ac', 'ox'], ['ic', 'ox'], \ ['', 'i'], ['', 'i'], ['', 'i'], \ ['', 'i'], ['', 'i'], \ ['', 'i'], ['', 'i'], ['', 'i'], \ ] for s:entry in s:mapping_surface for s:m in split(s:entry[1], '\zs') let s:ok = s:has_map(s:entry[0], s:m) call s:record(s:ok ? 1 : 0, 'map[' . s:m . '].' . s:entry[0], \ s:ok ? '' : s:entry[0] . ' (' . s:m . ') not mapped buffer-local') endfor endfor " ===== 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, \ }) call s:run('headers.[=_jumps_to_prev_sibling', { \ 'lines': ['= A =', '== child ==', 'body', '= B ='], \ 'cursor': [4, 1], \ 'keys': '[=', \ 'expect_cursor_line': 1, \ }) " ===== 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'], \ }) " ===== Insert-mode smart_return (Cluster 5) ===== call s:run('cr.continues_list_marker', { \ 'lines': ['- first'], \ 'cursor': [1, 7], \ 'keys': "A\second\", \ 'expect_lines': ['- first', '- second'], \ }) call s:run('cr.continues_checkbox', { \ 'lines': ['- [ ] first'], \ 'cursor': [1, 11], \ 'keys': "A\second\", \ 'expect_lines': ['- [ ] first', '- [ ] second'], \ }) call s:run('cr.breaks_on_empty_list_line', { \ 'lines': ['- '], \ 'cursor': [1, 2], \ 'keys': "A\plain\", \ 'expect_lines': ['', 'plain'], \ }) call s:run('cr.adds_new_table_row', { \ 'lines': ['| a | b |'], \ 'cursor': [1, 4], \ 'keys': "A\x\", \ '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\x\", \ '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\x\", \ '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\x\", \ '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\x\", \ 'expect_lines': ['| a |x b |'], \ }) call s:run('shift_tab.prev_cell_in_table', { \ 'lines': ['| a | b |'], \ 'cursor': [1, 7], \ 'keys': "i\x\", \ '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!')