feat(parity): close the entire P3 Mappings section

Both remaining mapping gaps (client-side, both clients):

- Visual <CR>: upstream binds it to NormalizeLinkVisualCR (== visual `+`).
  Added `x <CR>` -> normalize_link(true) (wrap the selection as a wikilink)
  in both clients.

- Insert <S-CR>: upstream's multiline-list-item continuation (VimwikiReturn
  2 2 -> kbd_cr with no new marker). New smart_shift_return (both clients,
  <expr> insert mapping): on a list item returns <CR> + spaces aligning under
  the item text (marker width + 1, +4 for a `[ ] ` checkbox), no marker; off a
  list item, a plain <CR>. Mirrors smart_return's auto-indent handling.

Tests: map.visual_cr_wraps_selection + map.insert_shift_cr_multiline
(test-keymaps.lua, the latter exercising the handler directly since <S-CR>
doesn't round-trip headless feedkeys), and map.visual_cr_wraps_selection +
cr.shift_cr_multiline{,_checkbox} (test-keymaps-vim.vim). Docs (README +
doc/nuwiki.txt) and the gap doc updated. Neovim 305, Vim 299/18/21 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 11:04:56 +00:00
parent 305324c6e9
commit 724712121d
9 changed files with 146 additions and 7 deletions
+2 -2
View File
@@ -323,8 +323,8 @@ The `<Leader>w` prefix in the `wiki_prefix`, `links`, and `html_export` rows is
|-----|---------|----------|
| `enabled` | `true` | master switch for the whole keymap layer |
| `wiki_prefix` | `true` | `<Leader>ww`, `<Leader>wt`, `<Leader>ws`, `<Leader>wi`, `<Leader>w<Leader>w`, `<Leader>w<Leader>y`, `<Leader>w<Leader>t`, `<Leader>w<Leader>m`, `<Leader>w<Leader>i` |
| `links` | `true` | `<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`, `<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+` (normal + visual), `<Leader>wn`, `<Leader>wd`, `<Leader>wr`, `<Leader>wc` (normal + visual) |
| `lists` | `true` | `<C-Space>` (also `<C-@>`/`<Nul>`), `gnt`, `gln`, `glp`, `glx`, `glh`, `gll`, `gLh`, `gLl`, `gLH`, `gLL`, `glr`, `gLr`, `gLR`, `gl-`, `gl*`, `gl#`, `gl1`, `gli`, `glI`, `gla`, `glA`, `gL-`, `gL*`, `gL#`, `gL1`, `gLi`, `gLI`, `gLa`, `gLA`, `gl`, `gL`, `o`, `O`; insert: `<C-D>`, `<C-T>`, `<C-L><C-J>`, `<C-L><C-K>`, `<C-L><C-M>`, `<CR>` |
| `links` | `true` | `<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`, `<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+` (normal + visual), `<CR>` (visual: normalize selection), `<Leader>wn`, `<Leader>wd`, `<Leader>wr`, `<Leader>wc` (normal + visual) |
| `lists` | `true` | `<C-Space>` (also `<C-@>`/`<Nul>`), `gnt`, `gln`, `glp`, `glx`, `glh`, `gll`, `gLh`, `gLl`, `gLH`, `gLL`, `glr`, `gLr`, `gLR`, `gl-`, `gl*`, `gl#`, `gl1`, `gli`, `glI`, `gla`, `glA`, `gL-`, `gL*`, `gL#`, `gL1`, `gLi`, `gLI`, `gLa`, `gLA`, `gl`, `gL`, `o`, `O`; insert: `<C-D>`, `<C-T>`, `<C-L><C-J>`, `<C-L><C-K>`, `<C-L><C-M>`, `<CR>`, `<S-CR>` (multiline item) |
| `headers` | `true` | `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u` |
| `table_editing` | `true` | `gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`, `<A-Right>`; insert: `<Tab>`, `<S-Tab>` |
| `diary` | `true` | `<C-Down>`, `<C-Up>` |
+23
View File
@@ -1310,6 +1310,29 @@ function! nuwiki#commands#smart_return() abort
return "\<CR>"
endfunction
" Insert-mode `<S-CR>`: continue the current list item on a new line WITHOUT a
" new marker, indented to align under the item's text — vimwiki's "multiline
" list item" (`VimwikiReturn 2 2`). Off a list item it's a plain `<CR>`.
function! nuwiki#commands#smart_shift_return() abort
let l:line = getline('.')
let l:m = matchlist(l:line, '^\(\s*\)\([-*#]\)\s\(.*\)$')
if empty(l:m)
let l:m2 = matchlist(l:line, '^\(\s*\)\(\d\+\)\([.)]\)\s\(.*\)$')
if !empty(l:m2)
let l:m = [l:line, l:m2[1], l:m2[2] . l:m2[3], l:m2[4]]
endif
endif
if empty(l:m)
return "\<CR>"
endif
let l:indent = l:m[1]
let l:marker = l:m[2]
let l:has_cb = l:m[3] =~# '^\[[ xXoO.\-]\]'
let l:pad = len(l:marker) + 1 + (l:has_cb ? 4 : 0)
let l:auto = &autoindent || &smartindent || &cindent || !empty(&indentexpr)
return "\<CR>" . (l:auto ? '' : l:indent) . repeat(' ', l:pad)
endfunction
" :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
" continuation (upstream's mapping-driven `VimwikiReturn`). Enters insert at
" end of line and triggers the buffer-local `<CR>` mapping (smart_return),
+25 -1
View File
@@ -186,6 +186,7 @@ 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'],
@@ -212,7 +213,7 @@ let s:mapping_surface = [
\ ['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'],
\ ['<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'],
\ ]
@@ -335,6 +336,20 @@ call s:run('cr.adds_new_table_row', {
\ 'keys': "A\<CR>x\<Esc>",
\ 'expect_lines': ['| a | b |', '|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) =====
@@ -589,6 +604,15 @@ call s:record(
\ '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
+37 -1
View File
@@ -208,6 +208,7 @@ vim.defer_fn(function()
{ '<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' }, -- visual normalize-link (== visual `+`)
-- Lists
{ '<C-Space>', 'nx' }, { '<C-@>', 'nx' }, { '<Nul>', 'nx' },
{ 'gnt', 'n' }, { 'gln', 'nx' }, { 'glp', 'nx' }, { 'glx', 'nx' },
@@ -242,7 +243,7 @@ vim.defer_fn(function()
{ 'al', 'ox' }, { 'il', 'ox' }, { 'a\\', 'ox' }, { 'i\\', 'ox' },
{ 'ac', 'ox' }, { 'ic', 'ox' },
-- Insert-mode
{ '<CR>', 'i' }, { '<Tab>', 'i' }, { '<S-Tab>', 'i' },
{ '<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' },
}
@@ -1009,6 +1010,41 @@ vim.defer_fn(function()
end
end)
-- Visual <CR> mapping wraps the selection (upstream parity with visual `+`).
-- Select via `normal!` (sets the '<,'> marks reliably), then `gv` reselects
-- and the fed <CR> fires the x-mode mapping.
tobj_case('map.visual_cr_wraps_selection', function()
set_buf({ 'alpha beta gamma' })
vim.cmd('normal! 0wviw\27') -- select 'beta', leave visual → marks set
send('gv<CR>')
local got = vim.api.nvim_get_current_line()
if got ~= 'alpha [[beta]] gamma' then
error('visual <CR> did not wrap selection: ' .. got)
end
end)
-- Insert <S-CR> continues a list item with no marker, aligned under the text.
-- Test the `<expr>` handler directly — `<S-CR>` doesn't round-trip through
-- headless feedkeys, but the handler is what the mapping invokes.
tobj_case('map.insert_shift_cr_multiline', function()
local cr = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
set_buf({ '- item' }) -- "- " is 2 cols → continuation aligns at column 2
local got = require('nuwiki.commands').smart_shift_return()
if got ~= cr .. ' ' then
error('S-CR (bullet) wrong: ' .. vim.inspect(got))
end
set_buf({ '- [ ] item' }) -- "- [ ] " is 6 cols
got = require('nuwiki.commands').smart_shift_return()
if got ~= cr .. ' ' then
error('S-CR (checkbox) wrong: ' .. vim.inspect(got))
end
set_buf({ 'plain text' }) -- not a list → plain <CR>
got = require('nuwiki.commands').smart_shift_return()
if got ~= cr then
error('S-CR (non-list) should be plain CR: ' .. vim.inspect(got))
end
end)
-- ===== Configurable map_prefix (vimwiki g:vimwiki_map_prefix) =====
-- Overriding map_prefix must relocate the whole <prefix>* family. We attach
-- to a throwaway scratch buffer with a custom prefix and assert the new
+19 -2
View File
@@ -569,8 +569,20 @@ fix site.
- [ ] `list_ignore_newline` / `text_ignore_newline` — export newline handling.
### Mappings
- [ ] Visual `<CR>` normalize-link (visual `+` covers the same intent).
- [ ] Insert `<S-CR>` multiline list item.
- [x] **Visual `<CR>` normalize-link** _(fixed 2026-06-03)_ — upstream binds
visual `<CR>` to `<Plug>VimwikiNormalizeLinkVisualCR``:VimwikiNormalizeLink 1`,
identical to visual `+`. nuwiki had only visual `+`. _Fix:_ added `x <CR>`
the same wrap-selection handler (`normalize_link(true)`) in both clients
(`lua/nuwiki/keymaps.lua`, `ftplugin/vimwiki.vim`). Covered by
`map.visual_cr_wraps_selection` in both harnesses.
- [x] **Insert `<S-CR>` multiline list item** _(fixed 2026-06-03)_ — upstream's
insert `<S-CR>` (`VimwikiReturn 2 2``kbd_cr` with no new marker) continues
the current list item on a new line indented to align under the item's text.
_Fix:_ new `smart_shift_return` (both clients, `<expr>` insert mapping): on a
list item returns `<CR>` + (indent +) `marker-width + 1 (+4 for a checkbox)`
spaces — no marker; off a list item, a plain `<CR>`. Mirrors `smart_return`'s
auto-indent handling. Covered by `map.insert_shift_cr_multiline`
(`test-keymaps.lua`) + `cr.shift_cr_multiline{,_checkbox}` (`test-keymaps-vim.vim`).
- [x] **`gLH` / `gLL` / `gLR`** _(fixed 2026-06-03)_ — upstream binds these as
case-variant **aliases** of `gLh` / `gLl` / `gLr` (dedent / indent whole item,
renumber all lists; upstream `ftplugin/vimwiki.vim:553,555,561`). nuwiki had
@@ -713,3 +725,8 @@ audits don't re-flag them.
`use_calendar` opt-out (`g:nuwiki_no_calendar` / setup `use_calendar = false`).
So `use_calendar` is no longer an intentional divergence — removed from that
list above. See `development/calendar-vim-integration.md`.
- Cleared the **entire P3 Mappings section** (2026-06-03): visual `<CR>`
normalize-link (same handler as visual `+`), and insert `<S-CR>` → multiline
list item (`smart_shift_return`, continuation with no new marker aligned under
the text). Both client-side, both clients, tested in both harnesses (Neovim
305, Vim 299/18/21). Only P3 Config items now remain open.
+6 -1
View File
@@ -275,6 +275,7 @@ Vim-specific globals ~
`<Leader>w<Leader>m`, `<Leader>w<Leader>i`
`links` `<CR>`, `<S-CR>`, `<C-CR>`, `<C-S-CR>`, `<D-CR>`,
`<M-CR>`, `<BS>`, `<Tab>`, `<S-Tab>`, `+` (n/x),
`<CR>` (x: normalize selection),
`<Leader>wn`, `<Leader>wd`, `<Leader>wr`,
`<Leader>wc` (n/x)
`lists` `<C-Space>` (also `<C-@>`/`<Nul>`), `gnt`, `gln`,
@@ -283,7 +284,7 @@ Vim-specific globals ~
`gla`, `glA`, `gL-`, `gL*`, `gL#`, `gL1`, `gLi`,
`gLI`, `gLa`, `gLA`, `gl`, `gL`, `o`, `O`; insert
`<C-D>`, `<C-T>`, `<C-L><C-J>`, `<C-L><C-K>`,
`<C-L><C-M>`, `<CR>`
`<C-L><C-M>`, `<CR>`, `<S-CR>`
`headers` `=`, `-`, `]]`, `[[`, `]=`, `[=`, `]u`, `[u`
`table_editing` `gqq`, `gq1`, `gww`, `gw1`, `<A-Left>`,
`<A-Right>`; insert `<Tab>`, `<S-Tab>`
@@ -747,6 +748,10 @@ on context:
same column count; cursor lands inside the first cell.
* Otherwise — plain `<CR>`.
Insert `<S-CR>` continues a list item as a *multiline* item: a new line
with no marker, indented to align under the item's text (or a plain
`<CR>` when not on a list item). Mirrors vimwiki's `<S-CR>`.
Table cell navigation ~
<Tab> Jump to the next cell. Past the last cell, insert a fresh
+3
View File
@@ -260,6 +260,8 @@ if !has('nvim')
nnoremap <silent><buffer> <S-Tab> ?\[\[<CR>:nohlsearch<CR>
nnoremap <silent><buffer> + :call nuwiki#commands#normalize_link()<CR>
xnoremap <silent><buffer> + :<C-u>call nuwiki#commands#normalize_link(1)<CR>
" Upstream binds visual <CR> to the same normalize-link as visual `+`.
xnoremap <silent><buffer> <CR> :<C-u>call nuwiki#commands#normalize_link(1)<CR>
exe 'nnoremap <silent><buffer> ' . s:prefix . 'n :call nuwiki#commands#wiki_goto_page('''')<CR>'
exe 'nnoremap <silent><buffer> ' . s:prefix . 'd :call nuwiki#commands#delete_file()<CR>'
exe 'nnoremap <silent><buffer> ' . s:prefix . 'r :call nuwiki#commands#rename_file()<CR>'
@@ -332,6 +334,7 @@ if !has('nvim')
inoremap <silent><buffer> <C-L><C-K> <C-o>:call nuwiki#commands#list_cycle_symbol(-1)<CR>
inoremap <silent><buffer> <C-L><C-M> <C-o>:call nuwiki#commands#list_toggle_or_add_checkbox()<CR>
inoremap <silent><buffer><expr> <CR> nuwiki#commands#smart_return()
inoremap <silent><buffer><expr> <S-CR> nuwiki#commands#smart_shift_return()
endif
" Headers
+25
View File
@@ -1000,6 +1000,31 @@ function M.smart_return()
return cr
end
-- Insert-mode `<S-CR>`: continue the current list item on a new line WITHOUT a
-- new marker, indented to align under the item's text — vimwiki's "multiline
-- list item" (`VimwikiReturn 2 2`). Off a list item it's a plain `<CR>`.
function M.smart_shift_return()
local line = vim.api.nvim_get_current_line()
local cr = vim.api.nvim_replace_termcodes('<CR>', true, false, true)
local indent, marker, after = line:match('^(%s*)([%-%*%#])%s(.*)$')
if not marker then
local n
indent, n, marker, after = line:match('^(%s*)(%d+)([%.%)])%s(.*)$')
if marker then marker = n .. marker end
end
if not marker then
return cr
end
-- Align under the text: marker width + 1 space (+ 4 for a `[ ] ` checkbox).
local has_cb = after:match('^%[[%sxXoO%.%-]%]') ~= nil
local pad = #marker + 1 + (has_cb and 4 or 0)
-- Mirror smart_return's auto-indent handling: when an auto-indent mechanism
-- is active, Vim already re-inserts the line's indent after `<CR>`.
local auto = vim.bo.autoindent or vim.bo.smartindent or vim.bo.cindent
or (vim.bo.indentexpr ~= nil and vim.bo.indentexpr ~= '')
return cr .. (auto and '' or indent) .. string.rep(' ', pad)
end
-- :VimwikiReturn / :NuwikiReturn — the command form of the smart `<CR>`
-- continuation (upstream's mapping-driven VimwikiReturn). Enters insert at end
-- of line and triggers the buffer-local `<CR>` mapping (smart_return). Args
+6
View File
@@ -181,6 +181,9 @@ function M.attach(bufnr, mappings)
map('n', '+', cmd.normalize_link, { desc = 'nuwiki: wrap word as wikilink' }, bufnr)
map('x', '+', function() cmd.normalize_link(true) end,
{ desc = 'nuwiki: wrap selection as wikilink' }, bufnr)
-- Upstream binds visual <CR> to the same normalize-link as visual `+`.
map('x', '<CR>', function() cmd.normalize_link(true) end,
{ desc = 'nuwiki: wrap selection as wikilink' }, bufnr)
map('n', p .. 'n', cmd.wiki_goto_page, { desc = 'nuwiki: goto page' }, bufnr)
map('n', p .. 'd', cmd.delete_file, { desc = 'nuwiki: delete page' }, bufnr)
map('n', p .. 'r', cmd.rename_file, { desc = 'nuwiki: rename page' }, bufnr)
@@ -277,6 +280,9 @@ function M.attach(bufnr, mappings)
-- result is the keystrokes fed — keeps undo coherent.
map('i', '<CR>', cmd.smart_return,
{ desc = 'nuwiki: smart return (insert)', expr = true }, bufnr)
-- Smart <S-CR>: multiline list item (continuation with no new marker).
map('i', '<S-CR>', cmd.smart_shift_return,
{ desc = 'nuwiki: multiline list item (insert)', expr = true }, bufnr)
end
-- ===== Insert-mode table navigation (Cluster 6) =====