parity(5): smart <CR> in insert mode (VimwikiReturn)
Bind `<CR>` in insert mode as an `<expr>` map to a `smart_return`
helper. Behaviour matches upstream vimwiki's `:VimwikiReturn`:
- On a list line with content → continue the list with the same
marker on a new line (preserving leading checkbox `[ ]` if any).
- On an empty list line (marker only) → clear the marker and break
out of the list with a plain newline.
- Inside a `|…|` table row → insert a fresh empty row below with
the same column count, cursor lands inside the first cell.
- Otherwise → plain `<CR>`.
Implementation note: the helper is `<expr>`-bound, which means
textlock is active and the buffer can't be mutated from inside the
callback. The function returns key sequences only — including
`<Esc>0DA<CR>` for the empty-list break and `<Esc>0li` for the
table-row cursor jump — so every effect flows through Vim's normal
keystroke pipeline and stays undo-coherent.
Both Lua and VimL counterparts shipped.
Tests: 4 new in scripts/test-keymaps.lua covering list continuation,
checkbox preservation, empty-marker break-out, and new table row.
Gates: 421 Rust / 35 Neovim / 12 Vim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -684,6 +684,48 @@ function! nuwiki#commands#paste_url() abort
|
|||||||
call s:exec('nuwiki.link.pasteUrl', l:args)
|
call s:exec('nuwiki.link.pasteUrl', l:args)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
" Smart <CR> for insert mode — vimwiki `:VimwikiReturn` parity.
|
||||||
|
" Used as `inoremap <expr> <CR> nuwiki#commands#smart_return()`. The
|
||||||
|
" returned string is fed into Vim's input stream after the function
|
||||||
|
" finishes, keeping the undo block coherent.
|
||||||
|
function! nuwiki#commands#smart_return() abort
|
||||||
|
let l:line = getline('.')
|
||||||
|
if l:line =~# '^\s*|.*|\s*$'
|
||||||
|
let l:n = max([len(split(l:line, '|', 1)) - 2, 1])
|
||||||
|
let l:new_row = '|' . repeat(' |', l:n)
|
||||||
|
let l:row = line('.')
|
||||||
|
call append(l:row, l:new_row)
|
||||||
|
call cursor(l:row + 1, 3)
|
||||||
|
return ''
|
||||||
|
endif
|
||||||
|
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)
|
||||||
|
let l:indent = l:m[1]
|
||||||
|
let l:marker = l:m[2]
|
||||||
|
let l:rest = l:m[3]
|
||||||
|
let l:cb = matchlist(l:rest, '^\(\[[ xXoO.\-]\]\)\s*\(.*\)$')
|
||||||
|
let l:has_cb = !empty(l:cb)
|
||||||
|
let l:body = l:has_cb ? l:cb[2] : l:rest
|
||||||
|
if l:body =~# '^\s*$'
|
||||||
|
call setline('.', '')
|
||||||
|
call cursor(line('.'), 1)
|
||||||
|
return "\<CR>"
|
||||||
|
endif
|
||||||
|
let l:cont = l:indent . l:marker . ' '
|
||||||
|
if l:has_cb
|
||||||
|
let l:cont .= '[ ] '
|
||||||
|
endif
|
||||||
|
return "\<CR>" . l:cont
|
||||||
|
endif
|
||||||
|
return "\<CR>"
|
||||||
|
endfunction
|
||||||
|
|
||||||
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
||||||
" without following. Pure-VimL.
|
" without following. Pure-VimL.
|
||||||
function! nuwiki#commands#normalize_link() abort
|
function! nuwiki#commands#normalize_link() abort
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ if !has('nvim')
|
|||||||
inoremap <silent><buffer> <C-L><C-J> <C-o>:call nuwiki#commands#list_cycle_symbol(1)<CR>
|
inoremap <silent><buffer> <C-L><C-J> <C-o>:call nuwiki#commands#list_cycle_symbol(1)<CR>
|
||||||
inoremap <silent><buffer> <C-L><C-K> <C-o>:call nuwiki#commands#list_cycle_symbol(-1)<CR>
|
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> <C-L><C-M> <C-o>:call nuwiki#commands#list_toggle_or_add_checkbox()<CR>
|
||||||
|
inoremap <silent><buffer><expr> <CR> nuwiki#commands#smart_return()
|
||||||
|
|
||||||
" Headers
|
" Headers
|
||||||
nnoremap <silent><buffer> = :call nuwiki#commands#heading_add()<CR>
|
nnoremap <silent><buffer> = :call nuwiki#commands#heading_add()<CR>
|
||||||
|
|||||||
@@ -582,6 +582,51 @@ function M.list_toggle_or_add_checkbox()
|
|||||||
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
vim.api.nvim_buf_set_lines(0, row - 1, row, false, { new_line })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
|
||||||
|
--- Used as an `<expr>` keymap so the function is side-effect-free on
|
||||||
|
--- the buffer (textlock is active). The returned string is fed into
|
||||||
|
--- Vim's input stream — we encode everything (including the table-row
|
||||||
|
--- insertion) as keystrokes so the user's typed keys after `<CR>` land
|
||||||
|
--- at the right cursor position.
|
||||||
|
function M.smart_return()
|
||||||
|
local line = vim.api.nvim_get_current_line()
|
||||||
|
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||||
|
local cr = tc('<CR>')
|
||||||
|
local esc = tc('<Esc>')
|
||||||
|
|
||||||
|
-- Table row → newline + empty row + cursor inside first cell.
|
||||||
|
if line:match('^%s*|.*|%s*$') then
|
||||||
|
local cell_count = 0
|
||||||
|
for _ in line:gmatch('|') do cell_count = cell_count + 1 end
|
||||||
|
cell_count = math.max(cell_count - 1, 1)
|
||||||
|
local new_row = '|' .. string.rep(' |', cell_count)
|
||||||
|
-- After `<CR>` + new_row, cursor sits at end of new row. Bounce to
|
||||||
|
-- normal, jump to col 0, step right past the leading `|`, re-enter
|
||||||
|
-- insert: `<Esc>0li` puts the cursor immediately after the `|`.
|
||||||
|
return cr .. new_row .. esc .. '0li'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- List item with marker?
|
||||||
|
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 marker then
|
||||||
|
local cb = after:match('^%[[%sxXoO%.%-]%]%s*(.*)$')
|
||||||
|
local body = cb or after
|
||||||
|
if body:match('^%s*$') then
|
||||||
|
-- Empty item → drop to normal, clear the line, re-enter insert,
|
||||||
|
-- then a fresh newline. Indent is dropped (matches vimwiki's
|
||||||
|
-- "break out of the list" behaviour on an empty bullet).
|
||||||
|
return esc .. '0DA' .. cr
|
||||||
|
end
|
||||||
|
return cr .. indent .. marker .. ' ' .. (cb and '[ ] ' or '')
|
||||||
|
end
|
||||||
|
return cr
|
||||||
|
end
|
||||||
|
|
||||||
-- §13.1 Cluster B — table rewriters.
|
-- §13.1 Cluster B — table rewriters.
|
||||||
|
|
||||||
function M.table_insert(cols, rows)
|
function M.table_insert(cols, rows)
|
||||||
|
|||||||
@@ -254,6 +254,11 @@ function M.attach(bufnr, mappings)
|
|||||||
{ desc = 'nuwiki: cycle list symbol backward (insert)' }, bufnr)
|
{ desc = 'nuwiki: cycle list symbol backward (insert)' }, bufnr)
|
||||||
map('i', '<C-L><C-M>', cmd.list_toggle_or_add_checkbox,
|
map('i', '<C-L><C-M>', cmd.list_toggle_or_add_checkbox,
|
||||||
{ desc = 'nuwiki: toggle/add checkbox (insert)' }, bufnr)
|
{ desc = 'nuwiki: toggle/add checkbox (insert)' }, bufnr)
|
||||||
|
|
||||||
|
-- Smart <CR>: continue list / table rows. `<expr>` so the function
|
||||||
|
-- result is the keystrokes fed — keeps undo coherent.
|
||||||
|
map('i', '<CR>', cmd.smart_return,
|
||||||
|
{ desc = 'nuwiki: smart return (insert)', expr = true }, bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ===== Header levels + nav =====
|
-- ===== Header levels + nav =====
|
||||||
|
|||||||
@@ -401,6 +401,38 @@ vim.defer_fn(function()
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
-- ===== Smart <CR> (Cluster 5) =====
|
||||||
|
|
||||||
|
run('cr.continues_list_marker', {
|
||||||
|
lines = { '- first' },
|
||||||
|
cursor = { 1, 7 }, -- end of line
|
||||||
|
keys = 'A<CR>second<Esc>',
|
||||||
|
expect_lines = { '- first', '- second' },
|
||||||
|
wait_ms = 100,
|
||||||
|
})
|
||||||
|
run('cr.continues_checkbox', {
|
||||||
|
lines = { '- [ ] first' },
|
||||||
|
cursor = { 1, 11 },
|
||||||
|
keys = 'A<CR>second<Esc>',
|
||||||
|
expect_lines = { '- [ ] first', '- [ ] second' },
|
||||||
|
wait_ms = 100,
|
||||||
|
})
|
||||||
|
run('cr.breaks_on_empty_list_line', {
|
||||||
|
lines = { '- ' },
|
||||||
|
cursor = { 1, 2 },
|
||||||
|
keys = 'A<CR>plain<Esc>',
|
||||||
|
expect_lines = { '', 'plain' },
|
||||||
|
wait_ms = 100,
|
||||||
|
})
|
||||||
|
run('cr.adds_new_table_row', {
|
||||||
|
lines = { '| a | b |' },
|
||||||
|
cursor = { 1, 4 }, -- inside `a`
|
||||||
|
keys = 'A<CR>x<Esc>',
|
||||||
|
-- New row inserted below with empty cells; cursor jumps to first.
|
||||||
|
expect_lines = { '| a | b |', '|x | |' },
|
||||||
|
wait_ms = 100,
|
||||||
|
})
|
||||||
|
|
||||||
-- ===== Diary nav =====
|
-- ===== Diary nav =====
|
||||||
-- Tested via the LSP open-* responses; can't fully validate without
|
-- Tested via the LSP open-* responses; can't fully validate without
|
||||||
-- a populated diary, but at least confirm the maps fire without
|
-- a populated diary, but at least confirm the maps fire without
|
||||||
|
|||||||
Reference in New Issue
Block a user