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:
2026-05-12 21:37:53 +00:00
parent e347f605ff
commit 2562a046db
5 changed files with 172 additions and 0 deletions
+64
View File
@@ -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)