fix(tables/lua): switch smart_tab/return to <Cmd>, drop vim.schedule
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:
+50
-60
@@ -725,49 +725,53 @@ local function align_table_at(row)
|
||||
vim.api.nvim_buf_set_lines(0, s - 1, e, false, new_lines)
|
||||
end
|
||||
|
||||
local function ensure_insert_mode()
|
||||
if vim.api.nvim_get_mode().mode ~= 'i' then
|
||||
vim.cmd('startinsert')
|
||||
end
|
||||
-- Public helpers dispatched via `<Cmd>` from the `<expr>` mappings.
|
||||
-- The mappings return a `<Cmd>lua require('nuwiki.commands')._foo(…)<CR>`
|
||||
-- keystroke so these run synchronously inside the same keypress —
|
||||
-- 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
|
||||
|
||||
local function schedule_insert_table_row_below(row)
|
||||
-- After `align_table_at`, the row count of the table containing
|
||||
-- `row` is fixed and `getline(row)` reflects the aligned content.
|
||||
vim.schedule(function()
|
||||
align_table_at(row)
|
||||
local new_line = vim.fn.getline(row)
|
||||
local starts = table_cell_starts(new_line)
|
||||
if #starts < 2 then return end
|
||||
local indent = new_line:match('^(%s*)') or ''
|
||||
local cell_count = #starts - 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)
|
||||
function M._table_insert_row_below(row)
|
||||
align_table_at(row)
|
||||
local new_line = vim.fn.getline(row)
|
||||
local starts = table_cell_starts(new_line)
|
||||
if #starts < 2 then return end
|
||||
local indent = new_line:match('^(%s*)') or ''
|
||||
local cell_count = #starts - 1
|
||||
local fresh = indent .. '|' .. string.rep(' |', cell_count)
|
||||
vim.api.nvim_buf_set_lines(0, row, row, false, { fresh })
|
||||
vim.api.nvim_win_set_cursor(0, { row + 1, #indent + 1 })
|
||||
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). Non-table branches encode their
|
||||
--- behaviour as returned keystrokes; the table-row branch schedules a
|
||||
--- post-textlock callback that aligns the table, inserts a fresh row,
|
||||
--- and places the cursor inside the new row's first cell.
|
||||
--- the buffer (textlock is active). The table-row branch hands off to
|
||||
--- `_table_insert_row_below` via `<Cmd>` so the new row is in place
|
||||
--- before the user's next keystroke is processed.
|
||||
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 → 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
|
||||
local cur = vim.api.nvim_win_get_cursor(0)
|
||||
schedule_insert_table_row_below(cur[1])
|
||||
return ''
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
return string.format(
|
||||
'<Cmd>lua require("nuwiki.commands")._table_insert_row_below(%d)<CR>',
|
||||
row
|
||||
)
|
||||
end
|
||||
|
||||
-- List item with marker?
|
||||
@@ -814,26 +818,17 @@ function M.smart_tab()
|
||||
if start <= col + 1 then current_idx = i end
|
||||
end
|
||||
local target_idx = current_idx + 1
|
||||
|
||||
vim.schedule(function()
|
||||
align_table_at(row)
|
||||
local new_line = vim.fn.getline(row)
|
||||
local starts = table_cell_starts(new_line)
|
||||
if target_idx < #starts then
|
||||
-- Land at the first space of the target cell (byte position
|
||||
-- equal to the 1-based pipe position).
|
||||
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
|
||||
ensure_insert_mode()
|
||||
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 ''
|
||||
if target_idx >= #cell_starts then
|
||||
return string.format(
|
||||
'<Cmd>lua require("nuwiki.commands")._table_insert_row_below(%d)<CR>',
|
||||
row
|
||||
)
|
||||
end
|
||||
return string.format(
|
||||
'<Cmd>lua require("nuwiki.commands")._table_jump_to_cell(%d, %d)<CR>',
|
||||
row,
|
||||
target_idx
|
||||
)
|
||||
end
|
||||
|
||||
function M.smart_shift_tab()
|
||||
@@ -853,16 +848,11 @@ function M.smart_shift_tab()
|
||||
end
|
||||
if not current_idx or current_idx <= 1 then return stab end
|
||||
local target_idx = current_idx - 1
|
||||
|
||||
vim.schedule(function()
|
||||
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
|
||||
vim.api.nvim_win_set_cursor(0, { row, starts[target_idx] })
|
||||
ensure_insert_mode()
|
||||
end)
|
||||
return ''
|
||||
return string.format(
|
||||
'<Cmd>lua require("nuwiki.commands")._table_jump_to_cell(%d, %d)<CR>',
|
||||
row,
|
||||
target_idx
|
||||
)
|
||||
end
|
||||
|
||||
-- §13.1 Cluster B — table rewriters.
|
||||
|
||||
Reference in New Issue
Block a user