diff --git a/autoload/nuwiki/commands.vim b/autoload/nuwiki/commands.vim index 0b735a6..07fb75d 100644 --- a/autoload/nuwiki/commands.vim +++ b/autoload/nuwiki/commands.vim @@ -726,6 +726,64 @@ function! nuwiki#commands#smart_return() abort return "\" endfunction +" Insert-mode `` / `` table navigation — vimwiki parity. +" Used as `` mappings so we can pass `` / `` 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 "\" + endif + let l:starts = s:table_cell_starts(l:line) + if len(l:starts) < 2 + return "\" + endif + let l:col = col('.') - 1 + for l:s in l:starts + if l:s > l:col + 1 && l:s < len(l:line) + return "\" . (l:s + 1) . '|i' + endif + endfor + let l:n = len(l:starts) - 1 + let l:new_row = '|' . repeat(' |', l:n) + return "\" . l:new_row . "\0li" +endfunction + +function! nuwiki#commands#smart_shift_tab() abort + let l:line = getline('.') + if l:line !~# '^\s*|.*|\s*$' + return "\" + endif + let l:starts = s:table_cell_starts(l:line) + if len(l:starts) < 2 + return "\" + 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 "\" + endif + let l:target = l:starts[l:current_idx - 1] + return "\" . (l:target + 1) . '|i' +endfunction + " `:VimwikiNormalizeLink` — wrap the word at cursor as `[[word]]` " without following. Pure-VimL. function! nuwiki#commands#normalize_link() abort diff --git a/ftplugin/vimwiki.vim b/ftplugin/vimwiki.vim index 74e66ce..12b70aa 100644 --- a/ftplugin/vimwiki.vim +++ b/ftplugin/vimwiki.vim @@ -176,6 +176,8 @@ if !has('nvim') inoremap :call nuwiki#commands#list_cycle_symbol(-1) inoremap :call nuwiki#commands#list_toggle_or_add_checkbox() inoremap nuwiki#commands#smart_return() + inoremap nuwiki#commands#smart_tab() + inoremap nuwiki#commands#smart_shift_tab() " Headers nnoremap = :call nuwiki#commands#heading_add() diff --git a/lua/nuwiki/commands.lua b/lua/nuwiki/commands.lua index 19db2a6..68c29ee 100644 --- a/lua/nuwiki/commands.lua +++ b/lua/nuwiki/commands.lua @@ -627,6 +627,70 @@ function M.smart_return() return cr end +--- Insert-mode `` / `` table navigation — vimwiki parity. +--- Used as `` keymaps so we can fall back to a literal `` / +--- `` when the cursor isn't on a table row. Inside a row: +--- `` jumps to the next cell, creating a fresh row below the +--- current one if we were in the final cell. `` 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('') + 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 + -- `` + 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('') .. 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('') .. new_row .. tc('') .. '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('') + 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('') .. tostring(target + 1) .. '|i' +end + -- §13.1 Cluster B — table rewriters. function M.table_insert(cols, rows) diff --git a/lua/nuwiki/keymaps.lua b/lua/nuwiki/keymaps.lua index 6fa070a..e6127fc 100644 --- a/lua/nuwiki/keymaps.lua +++ b/lua/nuwiki/keymaps.lua @@ -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', '', cmd.smart_tab, + { desc = 'nuwiki: next table cell (insert)', expr = true }, bufnr) + map('i', '', 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. diff --git a/scripts/test-keymaps.lua b/scripts/test-keymaps.lua index 18e927e..beee0c7 100644 --- a/scripts/test-keymaps.lua +++ b/scripts/test-keymaps.lua @@ -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 = 'Ax', + -- After A: cursor at end. : 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 = 'llix', + -- `ll` from col 0 → col 2 (on `a`). `i` enters insert. `` + -- 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 = 'ix', + -- 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 = 'ix', + -- 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