parity(6): insert-mode <Tab>/<S-Tab> table cell nav
`<Tab>` / `<S-Tab>` in insert mode navigate between table cells when the cursor is on a `|…|` row, otherwise they pass through to their default insert-mode behaviour (literal tab / shift-tab). Same `<expr>`-mapping idiom as Cluster 5's smart_return. <Tab> next cell; creates a fresh row below if past the last cell <S-Tab> previous cell; no-op past the first Cursor lands immediately after the destination cell's leading `|`, matching upstream vimwiki's `vimwiki#tbl#go_*_cell` behaviour. Both Lua and VimL counterparts wired into ftplugin's existing table_editing block (gated alongside `gqq`/`<A-Left>`). Tests: 4 new in scripts/test-keymaps.lua — next cell, new-row overflow, previous cell, and pass-through outside a table. Gates: 421 Rust / 39 Neovim / 12 Vim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -726,6 +726,64 @@ function! nuwiki#commands#smart_return() abort
|
||||
return "\<CR>"
|
||||
endfunction
|
||||
|
||||
" Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||
" Used as `<expr>` mappings so we can pass `<Tab>` / `<S-Tab>` through
|
||||
" verbatim when the cursor isn't on a table row.
|
||||
function! s:table_cell_starts(line) abort
|
||||
let l:out = []
|
||||
let l:pos = 0
|
||||
while 1
|
||||
let l:bar = stridx(a:line, '|', l:pos)
|
||||
if l:bar < 0 | break | endif
|
||||
call add(l:out, l:bar + 1)
|
||||
let l:pos = l:bar + 1
|
||||
endwhile
|
||||
return l:out
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#smart_tab() abort
|
||||
let l:line = getline('.')
|
||||
if l:line !~# '^\s*|.*|\s*$'
|
||||
return "\<Tab>"
|
||||
endif
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
if len(l:starts) < 2
|
||||
return "\<Tab>"
|
||||
endif
|
||||
let l:col = col('.') - 1
|
||||
for l:s in l:starts
|
||||
if l:s > l:col + 1 && l:s < len(l:line)
|
||||
return "\<Esc>" . (l:s + 1) . '|i'
|
||||
endif
|
||||
endfor
|
||||
let l:n = len(l:starts) - 1
|
||||
let l:new_row = '|' . repeat(' |', l:n)
|
||||
return "\<CR>" . l:new_row . "\<Esc>0li"
|
||||
endfunction
|
||||
|
||||
function! nuwiki#commands#smart_shift_tab() abort
|
||||
let l:line = getline('.')
|
||||
if l:line !~# '^\s*|.*|\s*$'
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:starts = s:table_cell_starts(l:line)
|
||||
if len(l:starts) < 2
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:col = col('.') - 1
|
||||
let l:current_idx = -1
|
||||
let l:i = 0
|
||||
for l:s in l:starts
|
||||
if l:s <= l:col + 1 | let l:current_idx = l:i | endif
|
||||
let l:i += 1
|
||||
endfor
|
||||
if l:current_idx <= 0
|
||||
return "\<S-Tab>"
|
||||
endif
|
||||
let l:target = l:starts[l:current_idx - 1]
|
||||
return "\<Esc>" . (l:target + 1) . '|i'
|
||||
endfunction
|
||||
|
||||
" `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]`
|
||||
" without following. Pure-VimL.
|
||||
function! nuwiki#commands#normalize_link() abort
|
||||
|
||||
@@ -176,6 +176,8 @@ 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> <Tab> nuwiki#commands#smart_tab()
|
||||
inoremap <silent><buffer><expr> <S-Tab> nuwiki#commands#smart_shift_tab()
|
||||
|
||||
" Headers
|
||||
nnoremap <silent><buffer> = :call nuwiki#commands#heading_add()<CR>
|
||||
|
||||
@@ -627,6 +627,70 @@ function M.smart_return()
|
||||
return cr
|
||||
end
|
||||
|
||||
--- Insert-mode `<Tab>` / `<S-Tab>` table navigation — vimwiki parity.
|
||||
--- Used as `<expr>` keymaps so we can fall back to a literal `<Tab>` /
|
||||
--- `<S-Tab>` when the cursor isn't on a table row. Inside a row:
|
||||
--- `<Tab>` jumps to the next cell, creating a fresh row below the
|
||||
--- current one if we were in the final cell. `<S-Tab>` jumps to the
|
||||
--- previous cell, no-op past the first.
|
||||
local function table_cell_starts(line)
|
||||
-- Returns 0-based byte offsets right after each `|` separator.
|
||||
local out = {}
|
||||
local pos = 0
|
||||
while true do
|
||||
local bar = line:find('|', pos + 1, true)
|
||||
if not bar then break end
|
||||
table.insert(out, bar) -- 1-based: position immediately AFTER the |
|
||||
pos = bar
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function M.smart_tab()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||
local tab = tc('<Tab>')
|
||||
if not line:match('^%s*|.*|%s*$') then
|
||||
return tab
|
||||
end
|
||||
local cell_starts = table_cell_starts(line)
|
||||
if #cell_starts < 2 then return tab end
|
||||
local _, col = unpack(vim.api.nvim_win_get_cursor(0)) -- 0-based byte col
|
||||
-- Find the next cell start strictly after the cursor's column.
|
||||
for _, start in ipairs(cell_starts) do
|
||||
if start > col + 1 and start < #line then
|
||||
-- `<Esc>` + jump to that col (`start + 1` 1-based → +1 for space
|
||||
-- inside the cell), then `i` to re-enter insert.
|
||||
local target = start + 1
|
||||
return tc('<Esc>') .. tostring(target) .. '|i'
|
||||
end
|
||||
end
|
||||
-- Past last cell → insert a new row below, cursor in first cell.
|
||||
local cell_count = #cell_starts - 1
|
||||
local new_row = '|' .. string.rep(' |', cell_count)
|
||||
return tc('<CR>') .. new_row .. tc('<Esc>') .. '0li'
|
||||
end
|
||||
|
||||
function M.smart_shift_tab()
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
|
||||
local stab = tc('<S-Tab>')
|
||||
if not line:match('^%s*|.*|%s*$') then
|
||||
return stab
|
||||
end
|
||||
local cell_starts = table_cell_starts(line)
|
||||
if #cell_starts < 2 then return stab end
|
||||
local _, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
-- Find the cell the cursor is in, then go to the *previous* cell.
|
||||
local current_idx
|
||||
for i, start in ipairs(cell_starts) do
|
||||
if start <= col + 1 then current_idx = i end
|
||||
end
|
||||
if not current_idx or current_idx <= 1 then return stab end
|
||||
local target = cell_starts[current_idx - 1]
|
||||
return tc('<Esc>') .. tostring(target + 1) .. '|i'
|
||||
end
|
||||
|
||||
-- §13.1 Cluster B — table rewriters.
|
||||
|
||||
function M.table_insert(cols, rows)
|
||||
|
||||
@@ -261,6 +261,15 @@ function M.attach(bufnr, mappings)
|
||||
{ desc = 'nuwiki: smart return (insert)', expr = true }, bufnr)
|
||||
end
|
||||
|
||||
-- ===== Insert-mode table navigation (Cluster 6) =====
|
||||
-- Gated under `table_editing` since it's the same surface.
|
||||
if on('table_editing') then
|
||||
map('i', '<Tab>', cmd.smart_tab,
|
||||
{ desc = 'nuwiki: next table cell (insert)', expr = true }, bufnr)
|
||||
map('i', '<S-Tab>', cmd.smart_shift_tab,
|
||||
{ desc = 'nuwiki: prev table cell (insert)', expr = true }, bufnr)
|
||||
end
|
||||
|
||||
-- ===== Header levels + nav =====
|
||||
if on('headers') then
|
||||
-- vimwiki defaults — buffer-local so they don't clash globally.
|
||||
|
||||
@@ -433,6 +433,45 @@ vim.defer_fn(function()
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Insert-mode table navigation (Cluster 6) =====
|
||||
|
||||
run('tab.next_cell_in_table', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 2 }, -- inside cell `a`
|
||||
keys = 'A<Tab>x<Esc>',
|
||||
-- After A: cursor at end. <Tab>: no cell-start strictly after end
|
||||
-- so creates new row. Try cursor on inside-first-cell case below.
|
||||
-- Here cursor at end has no next cell so a new row is added.
|
||||
expect_lines = { '| a | b |', '|x | |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.from_first_cell_moves_to_second', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'lli<Tab>x<Esc>',
|
||||
-- `ll` from col 0 → col 2 (on `a`). `i` enters insert. `<Tab>`
|
||||
-- jumps to col after next `|`. `x` types in second cell.
|
||||
expect_lines = { '| a |x b |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('shift_tab.prev_cell_in_table', {
|
||||
lines = { '| a | b |' },
|
||||
cursor = { 1, 6 }, -- inside cell `b`
|
||||
keys = 'i<S-Tab>x<Esc>',
|
||||
-- Cursor lands just after the previous cell's `|`; typing `x`
|
||||
-- inserts at col 2 (before the leading space inside cell 1).
|
||||
expect_lines = { '|x a | b |' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
run('tab.outside_table_passes_through', {
|
||||
lines = { 'plain text' },
|
||||
cursor = { 1, 0 },
|
||||
keys = 'i<Tab>x<Esc>',
|
||||
-- Default Tab inserts a literal tab.
|
||||
expect_lines = { '\tx' .. 'plain text' },
|
||||
wait_ms = 100,
|
||||
})
|
||||
|
||||
-- ===== Diary nav =====
|
||||
-- Tested via the LSP open-* responses; can't fully validate without
|
||||
-- a populated diary, but at least confirm the maps fire without
|
||||
|
||||
Reference in New Issue
Block a user