" 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'], ['', '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'], \ ['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'], \ ['', 'n'], ['', 'n'], \ ['ww', 'n'], ['ws', 'n'], ['wi', 'n'], \ ['ww', 'n'], ['wy', 'n'], \ ['wt', 'n'], ['wm', '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 " ===== Diary w{t,m} target (collision fix) ===== " Upstream: wt = today in a new tab, wm = " tomorrow. nuwiki used to bind both to tomorrow. call s:record( \ maparg('wt', 'n') =~# 'diary_today_tab' ? 1 : 0, \ 'diary.leader_t_opens_today_in_tab', \ 'rhs=' . maparg('wt', 'n')) call s:record( \ maparg('wm', 'n') =~# 'diary_tomorrow' ? 1 : 0, \ 'diary.leader_m_opens_tomorrow', \ 'rhs=' . maparg('wm', '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._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 " ===== Colorize (cword, visual selection, ranged command) ===== " The command is -nargs=1 -range: normal invocation wraps the cword; a visual " range (`:'<,'>VimwikiColorize` / the x-mode 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) ==# 'word 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\", 'tx') call nuwiki#commands#colorize('blue', 2) call s:record( \ getline(1) ==# 'one two 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\", '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 beta 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', 'word 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, '') + 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 y z', 'word 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), '` selection. call s:set_buf(['one two three']) call cursor(1, 5) call feedkeys("viw\", '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))) " ===== 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) " ===== :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(['x 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!')