Fix glp backward cycle and gl<Space> current-list scope
CI / cargo fmt --check (push) Successful in 16s
CI / cargo clippy (push) Successful in 21s
CI / cargo test (push) Successful in 37s
CI / cargo fmt --check (pull_request) Successful in 34s
CI / cargo clippy (pull_request) Successful in 36s
CI / cargo test (pull_request) Successful in 38s
CI / editor keymaps (push) Successful in 1m23s
CI / editor keymaps (pull_request) Successful in 1m31s
CI / cargo fmt --check (push) Successful in 16s
CI / cargo clippy (push) Successful in 21s
CI / cargo test (push) Successful in 37s
CI / cargo fmt --check (pull_request) Successful in 34s
CI / cargo clippy (pull_request) Successful in 36s
CI / cargo test (pull_request) Successful in 38s
CI / editor keymaps (push) Successful in 1m23s
CI / editor keymaps (pull_request) Successful in 1m31s
Two documented checkbox-list mappings did not match their spec: - `glp` dispatched the same forward-only cycleCheckbox as `gln`, so it could never "cycle the checkbox state backward". Add `ops::cycle_state_back` and thread a `reverse` flag through the dispatcher and both clients. - `gl<Space>` and `gL<Space>` both swept the whole buffer, making them identical. `gl<Space>` now passes the cursor position and the server scopes deletion to the contiguous list block under the cursor (current list, cascading into sublists); `gL<Space>` stays whole-buffer. Adds Rust unit tests, plus behavioral and full-registration coverage for the documented mapping surface in the Neovim and Vim keymap harnesses, and a command-coverage suite mirroring the doc's command groups. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,9 @@ VIMRC="$TMP/vimrc"
|
||||
cat > "$VIMRC" <<EOF
|
||||
set nocompatible
|
||||
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||
" Enable the opt-in mouse mappings before the ftplugin loads so the
|
||||
" harness can assert the full documented mapping surface (doc §6 mouse).
|
||||
let g:nuwiki_mouse_mappings = 1
|
||||
filetype plugin indent on
|
||||
syntax enable
|
||||
" ftdetect/nuwiki.vim already maps *.wiki → vimwiki via \`set filetype\`.
|
||||
|
||||
@@ -95,6 +95,107 @@ else
|
||||
call s:record(0, 'cmd.NuwikiIndex_defined', ':NuwikiIndex not registered')
|
||||
endif
|
||||
|
||||
" ===== Documented command surface (doc/nuwiki.txt §5) =====
|
||||
" Every command must be reachable in BOTH the canonical :Nuwiki* form and
|
||||
" the vimwiki-compatible :Vimwiki* form. The suffixes below are the §5 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',
|
||||
\ 'NextLink', 'PrevLink', 'BaddLink', 'RenameFile', 'DeleteFile',
|
||||
\ 'MakeDiaryNote', 'MakeYesterdayDiaryNote', 'MakeTomorrowDiaryNote',
|
||||
\ 'DiaryIndex', 'DiaryGenerateLinks', 'DiaryNextDay', 'DiaryPrevDay',
|
||||
\ 'TOC', 'GenerateLinks', 'CheckLinks', 'SearchTags', 'GenerateTagLinks',
|
||||
\ 'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||
\ ]
|
||||
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')
|
||||
|
||||
" ===== 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 (doc §6/§7/§8) =====
|
||||
" 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'],
|
||||
\ ['<BS>', 'n'], ['<Tab>', 'n'], ['<S-Tab>', 'n'], ['+', 'nx'],
|
||||
\ ['<C-Space>', 'nx'], ['<C-@>', 'nx'], ['<Nul>', 'nx'],
|
||||
\ ['gnt', 'n'], ['gln', 'nx'], ['glp', 'nx'], ['glx', 'nx'],
|
||||
\ ['glh', 'n'], ['gll', 'n'], ['gLh', 'n'], ['gLl', 'n'],
|
||||
\ ['glr', 'n'], ['gLr', 'n'], ['gl<Space>', 'n'], ['gL<Space>', '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>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'], ['<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
|
||||
|
||||
" ===== Header nav (pure VimL) =====
|
||||
|
||||
call s:run('headers.]]_jumps_to_next', {
|
||||
@@ -121,6 +222,12 @@ call s:run('headers.]=_jumps_to_next_sibling', {
|
||||
\ '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) =====
|
||||
|
||||
|
||||
@@ -123,6 +123,105 @@ vim.defer_fn(function()
|
||||
end
|
||||
record(true, 'lsp.attached', 'client=' .. client.name)
|
||||
|
||||
-- ===== Documented command surface (doc/nuwiki.txt §5) =====
|
||||
-- Every command must be reachable in BOTH the canonical :Nuwiki* form
|
||||
-- and the vimwiki-compatible :Vimwiki* form. The suffixes below are the
|
||||
-- §5 list minus the prefix. :NuwikiInstall is the one exception — a
|
||||
-- nuwiki-only maintenance command with no :Vimwiki* alias.
|
||||
local both_forms = {
|
||||
'Index', 'TabIndex', 'UISelect', 'Goto', 'FollowLink', 'Backlinks',
|
||||
'NextLink', 'PrevLink', 'BaddLink', 'RenameFile', 'DeleteFile',
|
||||
'MakeDiaryNote', 'MakeYesterdayDiaryNote', 'MakeTomorrowDiaryNote',
|
||||
'DiaryIndex', 'DiaryGenerateLinks', 'DiaryNextDay', 'DiaryPrevDay',
|
||||
'TOC', 'GenerateLinks', 'CheckLinks', 'SearchTags', 'GenerateTagLinks',
|
||||
'RebuildTags', '2HTML', '2HTMLBrowse', 'All2HTML', 'Rss',
|
||||
}
|
||||
for _, suffix in ipairs(both_forms) do
|
||||
for _, prefix in ipairs({ 'Nuwiki', 'Vimwiki' }) do
|
||||
local cmd = prefix .. suffix
|
||||
local exists = vim.fn.exists(':' .. cmd) == 2
|
||||
record(exists, 'surface.' .. cmd, exists and '' or (':' .. cmd .. ' not registered'))
|
||||
end
|
||||
end
|
||||
do
|
||||
local exists = vim.fn.exists(':NuwikiInstall') == 2
|
||||
record(exists, 'surface.NuwikiInstall',
|
||||
exists and '' or ':NuwikiInstall not registered')
|
||||
end
|
||||
|
||||
-- ===== Pure-Lua command invocation (cursor movement) =====
|
||||
-- :NuwikiNextLink / :NuwikiPrevLink wrap the link-search motion and run
|
||||
-- without the LSP, so we can assert exact cursor movement here.
|
||||
do
|
||||
local ok, err = pcall(function()
|
||||
set_buf({ 'intro [[A]] more [[B]] end' })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.cmd('NuwikiNextLink')
|
||||
local c1 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c1 ~= 6 then error('NextLink #1 col=' .. c1 .. ' want 6') end
|
||||
vim.cmd('NuwikiNextLink')
|
||||
local c2 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c2 ~= 17 then error('NextLink #2 col=' .. c2 .. ' want 17') end
|
||||
vim.cmd('VimwikiPrevLink')
|
||||
local c3 = vim.api.nvim_win_get_cursor(0)[2]
|
||||
if c3 ~= 6 then error('PrevLink col=' .. c3 .. ' want 6') end
|
||||
end)
|
||||
record(ok, 'invoke.next_prev_link_cursor_movement', ok and '' or tostring(err))
|
||||
end
|
||||
|
||||
-- ===== Documented mapping surface (doc §6/§7/§8) =====
|
||||
-- Every documented mapping must resolve to a buffer-local mapping in
|
||||
-- the mode(s) the doc lists it under. `modes` is a string of mode
|
||||
-- letters: n/x/o/i. The mouse group is opt-in — the harness enables it
|
||||
-- via setup({ mappings = { mouse = true } }).
|
||||
local function has_map(lhs, mode)
|
||||
local d = vim.fn.maparg(lhs, mode, false, true)
|
||||
return next(d) ~= nil and d.buffer == 1
|
||||
end
|
||||
local mapping_surface = {
|
||||
-- §6 Links
|
||||
{ '<CR>', 'n' }, { '<S-CR>', 'n' }, { '<C-CR>', 'n' }, { '<C-S-CR>', 'n' },
|
||||
{ '<BS>', 'n' }, { '<Tab>', 'n' }, { '<S-Tab>', 'n' }, { '+', 'nx' },
|
||||
-- §6 Lists
|
||||
{ '<C-Space>', 'nx' }, { '<C-@>', 'nx' }, { '<Nul>', 'nx' },
|
||||
{ 'gnt', 'n' }, { 'gln', 'nx' }, { 'glp', 'nx' }, { 'glx', 'nx' },
|
||||
{ 'glh', 'n' }, { 'gll', 'n' }, { 'gLh', 'n' }, { 'gLl', 'n' },
|
||||
{ 'glr', 'n' }, { 'gLr', 'n' }, { 'gl<Space>', 'n' }, { 'gL<Space>', 'n' },
|
||||
{ 'o', 'n' }, { 'O', 'n' },
|
||||
-- §6 Headers
|
||||
{ '=', 'n' }, { '-', 'n' }, { ']]', 'n' }, { '[[', 'n' },
|
||||
{ ']=', 'n' }, { '[=', 'n' }, { ']u', 'n' }, { '[u', 'n' },
|
||||
-- §6 Tables
|
||||
{ 'gqq', 'n' }, { 'gq1', 'n' }, { 'gww', 'n' }, { 'gw1', 'n' },
|
||||
{ '<A-Left>', 'n' }, { '<A-Right>', 'n' },
|
||||
-- §6 Wiki / diary / export
|
||||
{ '<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>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' },
|
||||
-- §6 Mouse (opt-in)
|
||||
{ '<2-LeftMouse>', 'n' }, { '<S-2-LeftMouse>', 'n' }, { '<C-2-LeftMouse>', 'n' },
|
||||
{ '<MiddleMouse>', 'n' }, { '<RightMouse>', 'n' },
|
||||
-- §7 Text objects (operator-pending + visual)
|
||||
{ 'ah', 'ox' }, { 'ih', 'ox' }, { 'aH', 'ox' }, { 'iH', 'ox' },
|
||||
{ 'al', 'ox' }, { 'il', 'ox' }, { 'a\\', 'ox' }, { 'i\\', 'ox' },
|
||||
{ 'ac', 'ox' }, { 'ic', 'ox' },
|
||||
-- §8 Insert-mode
|
||||
{ '<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 _, entry in ipairs(mapping_surface) do
|
||||
local lhs, modes = entry[1], entry[2]
|
||||
for m in modes:gmatch('.') do
|
||||
local ok = has_map(lhs, m)
|
||||
record(ok, 'map[' .. m .. '].' .. lhs,
|
||||
ok and '' or (lhs .. ' (' .. m .. ') not mapped buffer-local'))
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== Lists =====
|
||||
|
||||
run('list.toggle_via_C-Space', {
|
||||
@@ -167,6 +266,84 @@ vim.defer_fn(function()
|
||||
expect_line = '- [ ] done',
|
||||
expect_at = 1,
|
||||
})
|
||||
-- `glp` must cycle BACKWARD (regression for the glp==gln bug). From
|
||||
-- `[.]` the backward step lands on `[ ]`, whereas `gln` would advance
|
||||
-- to `[o]`.
|
||||
run('list.cycle_back_via_glp', {
|
||||
lines = { '- [.] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glp',
|
||||
expect_line = '- [ ] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.cycle_back_via_glp_wraps', {
|
||||
-- `[ ]` backward wraps to the top of the progression (`[X]`).
|
||||
lines = { '- [ ] task' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glp',
|
||||
expect_line = '- [X] task',
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Change level (glh/gll) =====
|
||||
|
||||
run('list.indent_via_gll', {
|
||||
lines = { '- item' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gll',
|
||||
expect_line = ' - item',
|
||||
expect_at = 1,
|
||||
})
|
||||
run('list.dedent_via_glh', {
|
||||
lines = { ' - item' },
|
||||
cursor = { 1, 2 },
|
||||
keys = 'glh',
|
||||
expect_line = '- item',
|
||||
expect_at = 1,
|
||||
})
|
||||
|
||||
-- ===== Renumber (glr / gLr) =====
|
||||
|
||||
run('list.renumber_via_glr', {
|
||||
lines = { '5. one', '9. two', '2. three' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'glr',
|
||||
expect_lines = { '1. one', '2. two', '3. three' },
|
||||
})
|
||||
|
||||
-- ===== Remove done (gl<Space> current list / gL<Space> whole doc) =====
|
||||
|
||||
-- `gl<Space>` removes done items from the cursor's list only; the
|
||||
-- second list's done item survives.
|
||||
run('list.remove_done_current_list_via_gl_space', {
|
||||
lines = {
|
||||
'- [X] done a', '- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gl<Space>',
|
||||
expect_lines = {
|
||||
'- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
})
|
||||
-- `gL<Space>` sweeps the whole buffer regardless of cursor position.
|
||||
run('list.remove_done_whole_buffer_via_gL_space', {
|
||||
lines = {
|
||||
'- [X] done a', '- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b', '- [X] done b',
|
||||
},
|
||||
cursor = { 1, 0 },
|
||||
keys = 'gL<Space>',
|
||||
expect_lines = {
|
||||
'- [ ] todo a', '',
|
||||
'paragraph', '',
|
||||
'- [ ] todo b',
|
||||
},
|
||||
})
|
||||
|
||||
-- ===== Next task (gnt) =====
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ cat > "$INIT" <<EOF
|
||||
-- Keep the harness self-contained; no plugin manager required.
|
||||
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||
vim.g.nuwiki_binary_path = '$BIN'
|
||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki' })
|
||||
-- Enable the opt-in mouse mappings so the harness can assert the full
|
||||
-- documented mapping surface (doc §6 mouse group).
|
||||
require('nuwiki').setup({ wiki_root = '$TMP/wiki', mappings = { mouse = true } })
|
||||
EOF
|
||||
|
||||
RESULTS="$TMP/results.txt"
|
||||
|
||||
Reference in New Issue
Block a user