Files
nuwiki/development/tests/test-keymaps-vim.vim
T

931 lines
34 KiB
VimL
Raw Permalink Normal View History

" 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 `<CR>`). LSP-roundtrip bindings (`<C-Space>`, `=`, `-`, …) 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', 'ShowVersion', 'Return', 'Var', 'Search',
\ ]
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 = [
\ ['<CR>', 'n'], ['<S-CR>', 'n'], ['<C-CR>', 'n'], ['<C-S-CR>', 'n'],
\ ['<M-CR>', 'n'], ['<D-CR>', 'n'],
\ ['<BS>', 'n'], ['<Tab>', 'n'], ['<S-Tab>', 'n'], ['+', 'nx'],
\ ['<CR>', 'x'],
\ ['<C-Space>', 'nx'], ['<C-@>', 'nx'], ['<Nul>', 'nx'],
\ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'],
\ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'],
\ ['gLH', 'n'], ['gLL', 'n'], ['gLR', 'n'],
\ ['glr', 'n'], ['gLr', 'n'], ['gl', 'n'], ['gL', 'n'],
\ ['gl-', 'n'], ['gL-', 'n'], ['gl*', 'n'], ['gL*', 'n'],
\ ['gl#', 'n'], ['gL#', 'n'], ['gl1', 'n'], ['gL1', 'n'],
\ ['gli', 'n'], ['gLi', 'n'], ['glI', 'n'], ['gLI', 'n'],
\ ['gla', 'n'], ['gLa', 'n'], ['glA', 'n'], ['gLA', 'n'],
\ ['o', 'n'], ['O', 'n'],
\ ['=', 'n'], ['-', 'n'], [']]', 'n'], ['[[', 'n'],
\ [']=', 'n'], ['[=', 'n'], [']u', 'n'], ['[u', 'n'],
\ ['gqq', 'n'], ['gq1', 'n'], ['gww', 'n'], ['gw1', 'n'],
\ ['<A-Left>', 'n'], ['<A-Right>', 'n'],
\ ['<Leader>ww', 'n'], ['<Leader>ws', 'n'], ['<Leader>wi', 'n'],
\ ['<Leader>w<Leader>w', 'n'], ['<Leader>w<Leader>y', 'n'],
\ ['<Leader>w<Leader>t', 'n'], ['<Leader>w<Leader>m', 'n'],
\ ['<Leader>w<Leader>i', 'n'],
\ ['<Leader>wn', 'n'], ['<Leader>wd', 'n'], ['<Leader>wr', 'n'],
\ ['<Leader>wh', 'n'], ['<Leader>whh', 'n'], ['<Leader>wha', 'n'],
\ ['<Leader>wc', 'nx'], ['<C-Down>', 'n'], ['<C-Up>', 'n'],
\ ['<2-LeftMouse>', 'n'], ['<S-2-LeftMouse>', 'n'], ['<C-2-LeftMouse>', 'n'],
\ ['<MiddleMouse>', 'n'], ['<RightMouse>', 'n'],
\ ['ah', 'ox'], ['ih', 'ox'], ['aH', 'ox'], ['iH', 'ox'],
\ ['al', 'ox'], ['il', 'ox'], ['a\', 'ox'], ['i\', 'ox'],
\ ['ac', 'ox'], ['ic', 'ox'],
\ ['<CR>', 'i'], ['<S-CR>', 'i'], ['<Tab>', 'i'], ['<S-Tab>', 'i'],
\ ['<C-D>', 'i'], ['<C-T>', 'i'],
\ ['<C-L><C-J>', 'i'], ['<C-L><C-K>', 'i'], ['<C-L><C-M>', '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
" ===== Diary <Leader>w<Leader>{t,m} target (collision fix) =====
" Upstream: <Leader>w<Leader>t = today in a new tab, <Leader>w<Leader>m =
" tomorrow. nuwiki used to bind both to tomorrow.
call s:record(
\ maparg('<Leader>w<Leader>t', 'n') =~# 'diary_today_tab' ? 1 : 0,
\ 'diary.leader_t_opens_today_in_tab',
\ 'rhs=' . maparg('<Leader>w<Leader>t', 'n'))
call s:record(
\ maparg('<Leader>w<Leader>m', 'n') =~# 'diary_tomorrow' ? 1 : 0,
\ 'diary.leader_m_opens_tomorrow',
\ 'rhs=' . maparg('<Leader>w<Leader>m', 'n'))
" ===== 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.<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 | |'],
\ })
" Regression: a `|` inside `[[u|t]]` must not split the cell — the link stays
" intact and the row keeps 2 columns (fresh row has 2 cells, not 3).
call s:run('cr.table_link_cell_pipe_not_a_separator', {
\ 'lines': ['| a | [[u|t]] |'],
\ 'cursor': [1, 4],
\ 'keys': "A\<CR>x\<Esc>",
\ 'expect_lines': ['| a | [[u|t]] |', '|x | |'],
\ })
" Insert <S-CR>: multiline list item — continue with NO marker, aligned under
" the text ("- " → 2 cols; "- [ ] " → 6 cols).
call s:run('cr.shift_cr_multiline', {
\ 'lines': ['- item'],
\ 'cursor': [1, 6],
\ 'keys': "A\<S-CR>x\<Esc>",
\ 'expect_lines': ['- item', ' x'],
\ })
call s:run('cr.shift_cr_multiline_checkbox', {
\ 'lines': ['- [ ] item'],
\ 'cursor': [1, 10],
\ 'keys': "A\<S-CR>x\<Esc>",
\ 'expect_lines': ['- [ ] item', ' 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
" ===== Colorize (cword, visual selection, ranged command) =====
" The command is -nargs=1 -range: normal invocation wraps the cword; a visual
" range (`:'<,'>VimwikiColorize` / the x-mode <Leader>wc map) wraps the
" selection. Run these while still on the vimwiki buffer (the buffer-local
" command must be defined).
call s:set_buf(['word here'])
call cursor(1, 1)
silent VimwikiColorize red
call s:record(
\ getline(1) ==# '<span style="color:red">word</span> here' ? 1 : 0,
\ 'colorize.command_wraps_cword',
\ 'got ' . string(getline(1)))
" Visual selection → wraps the selection (visual flag passed explicitly).
call s:set_buf(['one two three'])
call cursor(1, 5)
call feedkeys("viw\<Esc>", 'tx')
call nuwiki#commands#colorize('blue', 2)
call s:record(
\ getline(1) ==# 'one <span style="color:blue">two</span> three' ? 1 : 0,
\ 'colorize.visual_wraps_selection',
\ 'got ' . string(getline(1)))
" Ranged command must not raise E481 and wraps the selection.
call s:set_buf(['alpha beta gamma'])
call cursor(1, 8)
call feedkeys("viw\<Esc>", 'tx')
let s:cz_err = ''
try
silent '<,'>VimwikiColorize green
catch
let s:cz_err = v:exception
endtry
call s:record(
\ s:cz_err ==# '' && getline(1) ==# 'alpha <span style="color:green">beta</span> gamma' ? 1 : 0,
\ 'colorize.ranged_command_no_error',
\ 'err=' . s:cz_err . ' got ' . string(getline(1)))
" Colour spans are concealed down to their text. Put the span on a non-cursor
" line so concealcursor (=c) doesn't reveal it, then refresh + inspect.
call s:set_buf(['plain line', '<span style="color:red">word</span> tail'])
call cursor(1, 1)
call nuwiki#colors#refresh()
let s:cz_l = getline(2)
let s:cz_open = synconcealed(2, 1)[0]
let s:cz_word = synconcealed(2, stridx(s:cz_l, 'word') + 1)
let s:cz_close = synconcealed(2, stridx(s:cz_l, '</span>') + 1)[0]
call s:record(
\ s:cz_open == 1 && s:cz_close == 1 && s:cz_word[0] == 0
\ && synIDattr(synID(2, stridx(s:cz_l, 'word') + 1, 1), 'name') ==# 'nuwikiColorSpan_red' ? 1 : 0,
\ 'colorize.conceal_hides_tags_shows_text',
\ 'open=' . s:cz_open . ' close=' . s:cz_close . ' word=' . s:cz_word[0]
\ . ' grp=' . synIDattr(synID(2, stridx(s:cz_l, 'word') + 1, 1), 'name'))
" Running the command conceals the new span immediately (colorize refreshes;
" no manual step). Move the cursor off the line so concealcursor reveals nothing.
call s:set_buf(['target word', 'other line'])
call cursor(1, 1)
silent VimwikiColorize teal
call cursor(2, 1)
let s:cz_l2 = getline(1)
call s:record(
\ synconcealed(1, 1)[0] == 1
\ && synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name') ==# 'nuwikiColorSpan_teal' ? 1 : 0,
\ 'colorize.command_conceals_immediately',
\ 'concealed=' . synconcealed(1, 1)[0]
\ . ' grp=' . synIDattr(synID(1, stridx(s:cz_l2, 'target') + 1, 1), 'name'))
" A span whose colour Vim can't allocate (rgb()/prose) must be skipped — not
" abort refresh, not conceal — while a real span afterwards still conceals.
call s:set_buf(['plain', 'x <span style="color:rgb(1,2,3)">y</span> z', '<span style="color:red">word</span> z'])
call cursor(1, 1)
let s:cz_e = ''
try
call nuwiki#colors#refresh()
catch
let s:cz_e = v:exception
endtry
call s:record(
\ s:cz_e ==# ''
\ && synconcealed(3, 1)[0] == 1
\ && synconcealed(2, stridx(getline(2), '<span') + 1)[0] == 0 ? 1 : 0,
\ 'colorize.skips_unallocatable_colour',
\ 'err=' . s:cz_e . ' real=' . synconcealed(3, 1)[0]
\ . ' rgb=' . synconcealed(2, stridx(getline(2), '<span') + 1)[0])
" ===== NormalizeLink visual wraps the selection (pure-VimL) =====
" `:VimwikiNormalizeLink 1` / the x-mode `+` wrap the `'<`/`'>` selection.
call s:set_buf(['one two three'])
call cursor(1, 5)
call feedkeys("viw\<Esc>", 'tx')
call nuwiki#commands#normalize_link(1)
call s:record(
\ getline(1) ==# 'one [[two]] three' ? 1 : 0,
\ 'normalize.visual_wraps_selection',
\ 'got ' . string(getline(1)))
" ===== Visual <CR> mapping wraps the selection (== visual `+`) =====
call s:set_buf(['alpha beta gamma'])
call cursor(1, 7)
call feedkeys("viw\<CR>", 'tx')
call s:record(
\ getline(1) ==# 'alpha [[beta]] gamma' ? 1 : 0,
\ 'map.visual_cr_wraps_selection',
\ 'got ' . string(getline(1)))
" ===== Follow-link resolves + opens in the current window (regression) =====
" follow_link_or_create() must resolve the definition itself and `:edit` the
" target in the CURRENT window — including a page that does not exist yet
" (create-on-follow). We open the URI ourselves rather than delegating to
" :LspDefinition / coc's jumpDefinition because those fetch the target text
" (vim-lsp readfile()s it, throwing E484 for a missing page and aborting the
" jump). This harness loads only nuwiki, so :LspDefinition is absent and the
" coc branch of s:open_definition() runs once CocAction* are stubbed.
" The stubbed getDefinition points at a NON-EXISTENT file, so a green result
" proves both current-window placement and create-on-follow.
let g:_nuwiki_target = tempname() . '-newpage.wiki'
function! CocActionAsync(...) abort
endfunction
function! CocAction(action, ...) abort
if a:action ==# 'definitions'
return [{ 'uri': 'file://' . g:_nuwiki_target,
\ 'range': { 'start': { 'line': 0, 'character': 0 },
\ 'end': { 'line': 0, 'character': 0 } } }]
endif
return []
endfunction
" `:edit` must be able to abandon the (modified) scratch buffer.
let s:had_hidden = &hidden
set hidden
let s:seed = expand('%:p')
call s:set_buf(['[[NewPage]] rest'])
call cursor(1, 4)
call nuwiki#commands#follow_link_or_create()
let s:now = expand('%:p')
call s:record(
\ (s:now =~# 'newpage\.wiki$' && s:now !=# s:seed) ? 1 : 0,
\ 'cr.follow_opens_missing_page_in_current_window',
\ 'buf=' . s:now)
let &hidden = s:had_hidden
delfunction CocAction
delfunction CocActionAsync
unlet g:_nuwiki_target
" NOTE: the vim-lsp branch of s:open_definition() can't be driven headlessly —
" it gates on `exists('*lsp#send_request')`, which returns 0 for an inline stub
" (Vim only resolves real autoload files). It shares the same s:open_location()
" open logic the coc case above exercises, and mirrors the shipped
" follow_link_drop() request mechanism; its end-to-end coverage is the manual
" development/start-vim.sh check.
" ===== :VimwikiTable -nargs=* (cols+rows) =====
" Was -nargs=1, which raised E488 on `:VimwikiTable 4 3`. The table insert
" itself is an LSP roundtrip (no server in this headless harness), so we only
" assert the arg parse succeeds — i.e. no E488 trailing-characters error.
call s:set_buf([''])
call cursor(1, 1)
let s:tbl_err = ''
try
silent VimwikiTable 4 3
catch /E488/
let s:tbl_err = v:exception
catch
" A non-E488 error (e.g. no LSP attached) is expected and fine here.
endtry
call s:record(
\ s:tbl_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiTable_accepts_cols_rows',
\ 'err=' . s:tbl_err)
" ===== :VimwikiListChangeLvl -range -nargs=+ =====
" Was -nargs=? (range-less): `:1,3VimwikiListChangeLvl increase 0` raised E481
" (no range allowed) and/or E488 (trailing chars). The level change is an LSP
" roundtrip (no server here), so assert only the attribute parse: no E481/E488.
call s:set_buf(['- a', '- b', '- c'])
let s:lcl_err = ''
try
silent 1,3VimwikiListChangeLvl increase 0
catch /E481\|E488/
let s:lcl_err = v:exception
catch
" non-attribute error (e.g. no LSP) is expected and fine here.
endtry
call s:record(
\ s:lcl_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiListChangeLvl_accepts_range_and_args',
\ 'err=' . s:lcl_err)
" ===== :VimwikiSearch / VWS -nargs=* + :VimwikiTableAlignQ/W -nargs=? =====
" Upstream attributes: Search/VWS take -nargs=* (was -nargs=1, so a multi-word
" `:VimwikiSearch foo bar` raised E488); TableAlignQ/W take -nargs=? (were bare,
" so `:VimwikiTableAlignQ 2` raised E488). lvimgrep/align run for real here, so
" swallow any non-attribute error and assert only that E488 is gone.
let s:attr_err = ''
try
silent! VimwikiSearch foo bar
silent! VWS foo bar
silent! VimwikiTableAlignQ 2
silent! VimwikiTableAlignW 2
silent! NuwikiTableAlign 2
catch /E488/
let s:attr_err = v:exception
catch
" runtime errors (no match, empty table, …) are expected and fine here.
endtry
call s:record(
\ s:attr_err ==# '' ? 1 : 0,
\ 'cmd.search_and_tablealign_nargs',
\ 'err=' . s:attr_err)
" A no-match search must be caught (friendly message), not raise a raw E480.
let s:nm_err = ''
try
VimwikiSearch zzznomatchxyzqqq
catch /E480/
let s:nm_err = v:exception
catch
" other errors (no wiki files, etc.) are fine — only E480 is the regression.
endtry
call s:record(s:nm_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiSearch_no_match_graceful', 'err=' . s:nm_err)
" ===== auto_header inserts `= Stem =` on a new wiki page; skips the index =====
let s:ah_root = fnamemodify(expand(nuwiki#commands#wiki_list()[0].root), ':p')
if s:ah_root[len(s:ah_root)-1:] !=# '/' | let s:ah_root .= '/' | endif
new
execute 'file ' . fnameescape('/tmp/nuwiki-ah-plain/MyNewPage.wiki')
call nuwiki#commands#auto_header()
call s:record(getline(1) ==# '= MyNewPage =' ? 1 : 0,
\ 'cmd.auto_header_inserts', 'line1=' . getline(1))
bwipeout!
new
execute 'file ' . fnameescape(s:ah_root . 'index.wiki')
call nuwiki#commands#auto_header()
call s:record(getline(1) ==# '' ? 1 : 0,
\ 'cmd.auto_header_skips_index', 'line1=' . getline(1))
bwipeout!
unlet s:ah_root
" ===== :VimwikiGoto -nargs=* =====
" Was -nargs=1, so a page name with a space raised E488 (and the empty-arg
" prompt fallback was unreachable). Now -nargs=*: a multi-word arg must parse.
" The goto is an LSP roundtrip (no server here); swallow non-attribute errors.
let s:goto_err = ''
try
silent! VimwikiGoto My Page
catch /E471\|E488/
let s:goto_err = v:exception
catch
" no-LSP / not-found errors are expected and fine here.
endtry
call s:record(
\ s:goto_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiGoto_accepts_multiword',
\ 'err=' . s:goto_err)
" ===== :VimwikiVar get/set =====
" `:VimwikiVar key val` sets g:nuwiki_<key>; bare/get forms just echo.
silent VimwikiVar testkey hello world
call s:record(get(g:, 'nuwiki_testkey', '') ==# 'hello world' ? 1 : 0,
\ 'cmd.VimwikiVar_sets_global',
\ 'g:nuwiki_testkey=' . get(g:, 'nuwiki_testkey', '(unset)'))
" ===== global entry points work outside a wiki buffer =====
" Index / TabIndex / Var / ShowVersion are defined globally (upstream defines
" the Index family in plugin/). Open a throwaway non-wiki buffer and confirm.
new
setlocal buftype=nofile
let s:glob_ok = exists(':VimwikiIndex') == 2 && exists(':VimwikiTabIndex') == 2
\ && exists(':VimwikiVar') == 2 && exists(':VimwikiShowVersion') == 2
call s:record(s:glob_ok ? 1 : 0, 'cmd.global_entry_points',
\ 'Index/TabIndex/Var/ShowVersion visible outside a wiki buffer')
bwipeout!
" ===== autowriteall / table_auto_fmt (Vim path) =====
" autowriteall (default on) sets Vim's global &autowriteall on the wiki buffer.
call s:record(&autowriteall ? 1 : 0, 'config.autowriteall_applied',
\ '&autowriteall=' . &autowriteall)
" table_auto_fmt (default on) registers the NuwikiTableAutoFmt InsertLeave group.
call s:record(exists('#NuwikiTableAutoFmt#InsertLeave') ? 1 : 0,
\ 'config.table_auto_fmt_autocmd',
\ exists('#NuwikiTableAutoFmt#InsertLeave') ? '' : 'no InsertLeave autocmd')
" ===== :VimwikiRemoveDone -bang -range =====
" Upstream is `-range`; nuwiki had `-bang` only, so `:'<,'>VimwikiRemoveDone`
" raised E481. Now `-bang -range`: a ranged invocation must parse (no E481),
" and the `!` whole-buffer form must still work. The removal is an LSP
" roundtrip (no server here), so assert only the attribute parse.
call s:set_buf(['- [X] a', '- [ ] b', '- [X] c'])
let s:rd_err = ''
try
silent 1,2VimwikiRemoveDone
silent VimwikiRemoveDone!
catch /E481/
let s:rd_err = v:exception
catch
" non-E481 (e.g. no LSP attached) is expected and fine here.
endtry
call s:record(
\ s:rd_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiRemoveDone_accepts_range_and_bang',
\ 'err=' . s:rd_err)
" ===== :VimwikiToggleRejectedListItem -range =====
" Was bare (range-less): `:1,3VimwikiToggleRejectedListItem` raised E481. The
" reject is an LSP roundtrip (no server here), so assert only the attribute
" parse: no E481.
call s:set_buf(['- [ ] a', '- [ ] b', '- [ ] c'])
let s:trl_err = ''
try
silent 1,3VimwikiToggleRejectedListItem
catch /E481/
let s:trl_err = v:exception
catch
" non-E481 (e.g. no LSP attached) is expected and fine here.
endtry
call s:record(
\ s:trl_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiToggleRejectedListItem_accepts_range',
\ 'err=' . s:trl_err)
" ===== :VimwikiSplitLink -nargs=* =====
" Upstream takes optional reuse/move-cursor flags; nuwiki's defs took none, so
" `:VimwikiSplitLink 0 1` raised E488. Now -nargs=*: assert the arg parse
" succeeds. The split opens a window (the follow itself is an LSP no-op here);
" collapse back to one window afterward.
call s:set_buf(['[[Target]]'])
let s:sl_err = ''
try
silent VimwikiSplitLink 0 1
catch /E488/
let s:sl_err = v:exception
catch
" non-E488 (e.g. no LSP attached) is expected and fine here.
endtry
silent! only
call s:record(
\ s:sl_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiSplitLink_accepts_args',
\ 'err=' . s:sl_err)
" ===== Command-line completion (nuwiki#complete#*) =====
" Pure client-side completers backing the -complete= specs on VimwikiGoto
" (pages), VimwikiColorize (colours), and the tag commands. Driven directly
" (no LSP) against a throwaway wiki dir.
let s:cw = tempname()
call mkdir(s:cw, 'p')
call writefile(['= alpha =', 'tagged :foo:bar:'], s:cw . '/alpha.wiki')
call writefile(['= beta ='], s:cw . '/beta.wiki')
let s:save_root = get(g:, 'nuwiki_wiki_root', v:null)
let g:nuwiki_wiki_root = s:cw
let s:pg = nuwiki#complete#pages('a', '', 0)
call s:record(
\ (index(s:pg, 'alpha') >= 0 && index(s:pg, 'beta') < 0) ? 1 : 0,
\ 'complete.pages_globs_and_filters',
\ 'got ' . string(s:pg))
let s:tg = nuwiki#complete#tags('', '', 0)
call s:record(
\ (index(s:tg, 'foo') >= 0 && index(s:tg, 'bar') >= 0) ? 1 : 0,
\ 'complete.tags_scans_wiki_files',
\ 'got ' . string(s:tg))
if s:save_root is v:null
unlet g:nuwiki_wiki_root
else
let g:nuwiki_wiki_root = s:save_root
endif
call delete(s:cw, 'rf')
" Colours come from `color:NAME` spans already in the buffer.
call s:set_buf(['<span style="color:tomato">x</span> plain'])
let s:cl = nuwiki#complete#colors('t', '', 0)
call s:record(
\ index(s:cl, 'tomato') >= 0 ? 1 : 0,
\ 'complete.colors_from_buffer_spans',
\ 'got ' . string(s:cl))
" ===== Diary-note family -count=0 (wiki selector) =====
" Was bare: `:2VimwikiMakeDiaryNote` raised E481. The diary open is an LSP
" roundtrip (no server here), so assert only the attribute parse: no E481.
call s:set_buf(['= page ='])
let s:dc_err = ''
try
silent 2VimwikiMakeDiaryNote
catch /E481/
let s:dc_err = v:exception
catch
" non-E481 (e.g. no LSP attached) is expected and fine here.
endtry
call s:record(
\ s:dc_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiMakeDiaryNote_accepts_count',
\ 'err=' . s:dc_err)
let s:di_err = ''
try
silent 2VimwikiDiaryIndex
catch /E481/
let s:di_err = v:exception
catch
endtry
call s:record(
\ s:di_err ==# '' ? 1 : 0,
\ 'cmd.VimwikiDiaryIndex_accepts_count',
\ 'err=' . s:di_err)
" ===== 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!')