fix(tables/lua): switch smart_tab/return to <Cmd>, drop vim.schedule
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m5s
CI / cargo test (push) Successful in 1m19s
CI / editor keymaps (push) Successful in 1m57s

The keymap harness caught a regression in 5b6f789: returning '' from
the <expr> mapping and deferring cursor placement to vim.schedule
breaks any keypress immediately following Tab/<CR>. nvim_feedkeys with
the 'x' flag drains all queued keys synchronously, so the user's next
key lands at the pre-jump cursor position — and the scheduled
callback's startinsert leaks insert mode into the next test.

Mirror what the VimL side already does: have <expr> return
"<Cmd>lua require('nuwiki.commands')._helper(args)<CR>" so the
align + cursor-jump runs synchronously inside the same keypress. No
schedule, no mode juggling.

  cr.adds_new_table_row              ✓
  tab.next_cell_in_table             ✓
  tab.from_first_cell_moves_to_second ✓
  shift_tab.prev_cell_in_table       ✓

39/39 in scripts/test-keymaps.sh (was 35/4-fail).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 01:39:05 +00:00
parent f4e086f981
commit 695702555f
+50 -60
View File
@@ -725,49 +725,53 @@ local function align_table_at(row)
vim.api.nvim_buf_set_lines(0, s - 1, e, false, new_lines) vim.api.nvim_buf_set_lines(0, s - 1, e, false, new_lines)
end end
local function ensure_insert_mode() -- Public helpers dispatched via `<Cmd>` from the `<expr>` mappings.
if vim.api.nvim_get_mode().mode ~= 'i' then -- The mappings return a `<Cmd>lua require('nuwiki.commands')._foo(…)<CR>`
vim.cmd('startinsert') -- keystroke so these run synchronously inside the same keypress —
end -- crucial when the user types something *immediately* after Tab/<CR>,
-- which would otherwise land at the pre-jump cursor (the bug a prior
-- `vim.schedule`-based version shipped with).
function M._table_jump_to_cell(row, target_idx)
align_table_at(row)
local new_line = vim.fn.getline(row)
local starts = table_cell_starts(new_line)
if target_idx < 1 or target_idx >= #starts then return end
-- starts[i] is the 1-based byte position of the i-th `|`. Placing the
-- cursor at 0-based byte `starts[i]` lands on the space immediately
-- after that pipe — the first character inside the cell it opens.
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
end end
local function schedule_insert_table_row_below(row) function M._table_insert_row_below(row)
-- After `align_table_at`, the row count of the table containing align_table_at(row)
-- `row` is fixed and `getline(row)` reflects the aligned content. local new_line = vim.fn.getline(row)
vim.schedule(function() local starts = table_cell_starts(new_line)
align_table_at(row) if #starts < 2 then return end
local new_line = vim.fn.getline(row) local indent = new_line:match('^(%s*)') or ''
local starts = table_cell_starts(new_line) local cell_count = #starts - 1
if #starts < 2 then return end local fresh = indent .. '|' .. string.rep(' |', cell_count)
local indent = new_line:match('^(%s*)') or '' vim.api.nvim_buf_set_lines(0, row, row, false, { fresh })
local cell_count = #starts - 1 vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
local new_row = indent .. '|' .. string.rep(' |', cell_count)
vim.api.nvim_buf_set_lines(0, row, row, false, { new_row })
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
ensure_insert_mode()
end)
end end
--- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`. --- Smart `<CR>` for insert mode — vimwiki parity for `:VimwikiReturn`.
--- Used as an `<expr>` keymap so the function is side-effect-free on --- Used as an `<expr>` keymap so the function is side-effect-free on
--- the buffer (textlock is active). Non-table branches encode their --- the buffer (textlock is active). The table-row branch hands off to
--- behaviour as returned keystrokes; the table-row branch schedules a --- `_table_insert_row_below` via `<Cmd>` so the new row is in place
--- post-textlock callback that aligns the table, inserts a fresh row, --- before the user's next keystroke is processed.
--- and places the cursor inside the new row's first cell.
function M.smart_return() function M.smart_return()
local line = vim.api.nvim_get_current_line() local line = vim.api.nvim_get_current_line()
local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end local function tc(s) return vim.api.nvim_replace_termcodes(s, true, false, true) end
local cr = tc('<CR>') local cr = tc('<CR>')
local esc = tc('<Esc>') local esc = tc('<Esc>')
-- Table row → align existing rows, then add a fresh empty row below.
-- Doing this via `<CR>` + typed row keystrokes would split the
-- current line at the cursor and drag the trailing `|` onto the new
-- line — so we punt to `vim.schedule` instead.
if is_table_row(line) then if is_table_row(line) then
local cur = vim.api.nvim_win_get_cursor(0) local row = vim.api.nvim_win_get_cursor(0)[1]
schedule_insert_table_row_below(cur[1]) return string.format(
return '' '<Cmd>lua require("nuwiki.commands")._table_insert_row_below(%d)<CR>',
row
)
end end
-- List item with marker? -- List item with marker?
@@ -814,26 +818,17 @@ function M.smart_tab()
if start <= col + 1 then current_idx = i end if start <= col + 1 then current_idx = i end
end end
local target_idx = current_idx + 1 local target_idx = current_idx + 1
if target_idx >= #cell_starts then
vim.schedule(function() return string.format(
align_table_at(row) '<Cmd>lua require("nuwiki.commands")._table_insert_row_below(%d)<CR>',
local new_line = vim.fn.getline(row) row
local starts = table_cell_starts(new_line) )
if target_idx < #starts then end
-- Land at the first space of the target cell (byte position return string.format(
-- equal to the 1-based pipe position). '<Cmd>lua require("nuwiki.commands")._table_jump_to_cell(%d, %d)<CR>',
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] }) row,
ensure_insert_mode() target_idx
else )
local indent = new_line:match('^(%s*)') or ''
local cell_count = math.max(#starts - 1, 1)
local new_row = indent .. '|' .. string.rep(' |', cell_count)
vim.api.nvim_buf_set_lines(0, row, row, false, { new_row })
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
ensure_insert_mode()
end
end)
return ''
end end
function M.smart_shift_tab() function M.smart_shift_tab()
@@ -853,16 +848,11 @@ function M.smart_shift_tab()
end end
if not current_idx or current_idx <= 1 then return stab end if not current_idx or current_idx <= 1 then return stab end
local target_idx = current_idx - 1 local target_idx = current_idx - 1
return string.format(
vim.schedule(function() '<Cmd>lua require("nuwiki.commands")._table_jump_to_cell(%d, %d)<CR>',
align_table_at(row) row,
local new_line = vim.fn.getline(row) target_idx
local starts = table_cell_starts(new_line) )
if target_idx < 1 or target_idx >= #starts then return end
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
ensure_insert_mode()
end)
return ''
end end
-- §13.1 Cluster B — table rewriters. -- §13.1 Cluster B — table rewriters.