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>
User reported the editor-keymaps CI job hung for 30 minutes before
"Has been cancelled" — twice in a row. Two changes to keep that from
happening again:
1. Wrap both harness invocations with `timeout` (60s Neovim, 30s
Vim). A misbehaving LSP attach, a stuck `vim.defer_fn`, or
ex-mode hung on stdin can no longer hold the runner indefinitely;
`timeout` kills the process and the wrapper exits non-zero so CI
shows the real failure shape.
2. `</dev/null` on `vim -e -s` so ex-mode doesn't sit waiting for
input on CI's non-TTY stdin (the local zsh run inherits the
user's tty, masking this).
3. CI: the keymaps job now has `needs: test` and
`timeout-minutes: 10`. `needs: test` serialises behind the cargo
jobs so we don't fight for the single Gitea runner, and the
workflow-level timeout is a belt to the script-level `timeout`'s
braces — if for some reason both fail the same way, the runner
reclaims itself in 10 minutes instead of 30.
Local timing: `test-keymaps.sh` 10.4s, `test-keymaps-vim.sh` 0.1s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`scripts/test-keymaps.sh` boots `nvim --headless` against a scratch
wiki, sources `scripts/test-keymaps.lua`, and asserts the post-state
of each buffer-local mapping registered by `ftplugin/vimwiki.vim` +
`lua/nuwiki/keymaps.lua`.
The harness covers the bindings that actually have observable effects
on the buffer or cursor:
- **Lists** (LSP round-trip): `<C-Space>` / `<C-@>` / `<Nul>` toggle,
`gln` cycle, `glx` reject, plus a `[X]→[ ]` round-trip.
- **Tasks**: `gnt` jumps to the next unfinished `[ ]`.
- **Headings** (LSP round-trip): `=` adds a level, `-` removes one,
`=` on h6 clamps without growing.
- **Header nav** (pure Lua): `]]` next, `[[` prev, `]u` parent.
- **Link nav** (pure Lua): `<Tab>` to the next `[[…]]`.
- **`<CR>` two-step**: first press wraps a bare word as `[[word]]`
and stops (matches vimwiki's review-then-follow).
- **Bullet continuation**: `o` / `O` keep the marker, and `o` on a
`[ ]` line carries the checkbox forward.
Each case sets buffer lines, places the cursor (1-based), fires
`nvim_feedkeys` with `replace_termcodes`, waits long enough for the
LSP `executeCommand` round-trip (800 ms by default; 100 ms for the
pure-client maps), and asserts either the resulting lines, a specific
line, the cursor's new line, or the buffer's new name.
Output goes via `$NUWIKI_KEYMAP_RESULTS` (env-var IPC keeps it
robust to `--clean -u` argument parsing). Exit code mirrors the
harness: 0 on green, 1 on any FAIL. Local run yields:
SUMMARY: 19 passed, 0 failed
CI: new `keymaps` job in `.gitea/workflows/ci.yaml` installs Neovim
and runs the script, so a regression in `lua/nuwiki/keymaps.lua`,
`autoload/nuwiki/commands.vim`, or the LSP command surface is caught
on every push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>