parity(2): insert-mode list bindings (<C-D>/<C-T>/<C-L><C-?>)

Match upstream vimwiki's insert-mode list editing:
  <C-D>      list_change_level(-1)   dedent current item
  <C-T>      list_change_level(+1)   indent current item
  <C-L><C-J> list_cycle_symbol(+1)   cycle marker forward
  <C-L><C-K> list_cycle_symbol(-1)   cycle marker backward
  <C-L><C-M> list_toggle_or_add_chk  toggle if has checkbox, else add `[ ]`

Cycle order matches vimwiki's canonical run:
  - → * → # → 1. → 1) → a) → A) → i) → I) → (wrap)

Both Lua and VimL paths wired. Lua callbacks fire directly (no `<C-o>`
dance) since list_change_level / changeSymbol mutate the buffer via
the LSP applyEdit pipeline, which works cleanly in insert mode. VimL
keeps the `<C-o>:call …<CR>` idiom since that's the standard there.

Harness adds 5 cases covering the new insert-mode bindings plus one
direct LSP roundtrip for changeSymbol — surfaced a long-standing
stale-binary footgun in test-keymaps.sh (rebuilt only when bin was
missing; now rebuilds every run since incremental cargo is fast).
Harness also captures server log_messages and dumps them on failure
so future swallowed-error bugs (Err → log + Ok(None)) are visible.

Gates: 421 Rust / 25 Neovim / 12 Vim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:11:53 +00:00
parent ee0309b3c2
commit 46ae618b5f
6 changed files with 208 additions and 7 deletions
+67
View File
@@ -515,6 +515,73 @@ function M.list_change_lvl(direction)
M.list_change_level(delta, false)
end
--- Cycle the list marker forward (`direction = 1`) or backward
--- (`direction = -1`). Pure client-side: detects the current line's
--- marker via regex, picks the next symbol in vimwiki's canonical
--- order, and dispatches `list.changeSymbol`.
local _symbol_order = { '-', '*', '#', '1.', '1)', 'a)', 'A)', 'i)', 'I)' }
local function detect_current_symbol()
local line = vim.api.nvim_get_current_line()
-- Match optional indent, marker, then whitespace.
local indent, marker = line:match('^(%s*)([%-%*%#]) ')
if marker then return marker end
local n
indent, n, marker = line:match('^(%s*)(%d+)([%.%)]) ')
if marker then return '1' .. marker end
-- Roman lower
indent, marker = line:match('^(%s*)([ivxlcdm]+)%) ')
if marker then return 'i)' end
-- Roman upper
indent, marker = line:match('^(%s*)([IVXLCDM]+)%) ')
if marker then return 'I)' end
-- Alpha lower (single letter or short run)
indent, marker = line:match('^(%s*)([a-z]+)%) ')
if marker then return 'a)' end
indent, marker = line:match('^(%s*)([A-Z]+)%) ')
if marker then return 'A)' end
-- Suppress unused warning
local _ = indent
return nil
end
function M.list_cycle_symbol(direction)
local current = detect_current_symbol()
if not current then return end
local idx
for i, s in ipairs(_symbol_order) do
if s == current then idx = i break end
end
if not idx then return end
local next_idx = ((idx - 1 + (direction or 1)) % #_symbol_order) + 1
M.list_change_symbol(_symbol_order[next_idx], false)
end
--- `<C-L><C-M>` (insert mode) — toggle the checkbox on the current
--- item if any, OR insert `[ ]` after the marker if the line is a
--- plain list item without one. Pure client-side.
function M.list_toggle_or_add_checkbox()
local line = vim.api.nvim_get_current_line()
-- Already has a checkbox? Let the server's toggle handle it.
if line:match('%s*[%-%*%#]%s+%[[%sxXoO%.%-]%]') or
line:match('%s*%w+[%.%)]%s+%[[%sxXoO%.%-]%]') then
M.toggle_list_item()
return
end
-- Looks like a list item without a checkbox — insert `[ ] ` after
-- the marker.
local row = vim.api.nvim_win_get_cursor(0)[1]
local insert_at = nil
local function try(pattern)
local start_, finish = line:find(pattern)
if start_ then return finish end
end
insert_at = try('^%s*[%-%*%#] ') or try('^%s*%w+[%.%)] ')
if not insert_at then return end
local new_line = line:sub(1, insert_at) .. '[ ] ' .. line:sub(insert_at + 1)
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
end
-- §13.1 Cluster B — table rewriters.
function M.table_insert(cols, rows)