mirror of
https://github.com/neovim/neovim.git
synced 2026-09-08 15:07:49 +00:00
fix(cmdatom): operator with Lua textobject is not type=operator
Problem:
An operator completed by a Lua `:omap` textobject emits
`CmdAtom.type="mapping"` (lhs-only, no keys) instead of `type="operator"`.
Analysis:
`atom_redo_set()` declined K_LUA, though the prepped redo
("op" + K_LUA + id + CR) is exactly what "." replays. A no-edit "g@"
emits nothing at all.
Solution:
- `atom_redo_set`: don't decline K_LUA; the redo route now captures the
operator atom.
- `atom_capture_cmd`: don't early-return if the frame has prepped redo.
- op_function(): save/restore redobuff when invoking 'operatorfunc',
like `call_user_func()` does for Vimscript. (Else the Lua callback
may clobber the prepped "g@" redo / dot-repeat.)
fix #41482
TODO:
- async Lua (timer/vim.schedule) can still clobber the pending dot-repeat...
This commit is contained in:
@@ -361,6 +361,7 @@ describe('CmdAtom', function()
|
||||
it('stuffed translations execute within their command (exec_stuffed)', function()
|
||||
-- A register prefix crosses into the "." replay; an explicit register must not touch the
|
||||
-- small-delete register.
|
||||
atoms_start()
|
||||
fn.setline(1, { 'aaa bbb', 'ccc ddd' })
|
||||
feed('gg0dw')
|
||||
eq('aaa ', fn.getreg('-'))
|
||||
@@ -368,16 +369,30 @@ describe('CmdAtom', function()
|
||||
eq('ccc ', fn.getreg('z'))
|
||||
eq('aaa ', fn.getreg('-'))
|
||||
eq({ 'bbb', 'ddd' }, get_lines())
|
||||
-- One atom: the stuffed replay collects into the "."-labeled composite.
|
||||
eq(
|
||||
{ type = 'operator', lhs = k('"z.'), keys = '"zdw', reg = 'z' },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys', 'reg')
|
||||
)
|
||||
-- i_CTRL-O inside a stuffed translation's insert session ("S" == "cc"): the session resumes
|
||||
-- after ONE normal command.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'xxxx', 'yyyy' })
|
||||
feed('ggSabc<C-o>0def<Esc>')
|
||||
eq({ 'defabc', 'yyyy' }, get_lines())
|
||||
-- i_CTRL-O resolution is lossy: keys=nil, replay the "S"-labeled `lhs` instead.
|
||||
eq(
|
||||
{ type = 'mapping', lhs = k('Sabc<C-o>0def<Esc>') },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys')
|
||||
)
|
||||
-- r<Tab> under 'expandtab'/'smarttab' ("{count}R<Tab><Esc>": edit() does the tab).
|
||||
command('set expandtab shiftwidth=4 tabstop=4')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'abcdefgh', 'ABCDEFGH' })
|
||||
feed('gg03r<Tab>')
|
||||
eq(' defgh', fn.getline(1))
|
||||
eq(
|
||||
{ type = 'insert', lhs = k('3r<Tab>'), keys = k('3R<Tab><Esc>') },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys')
|
||||
)
|
||||
feed('j0.')
|
||||
eq(' DEFGH', fn.getline(2))
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'abcdefgh' })
|
||||
@@ -393,6 +408,7 @@ describe('CmdAtom', function()
|
||||
command('1s/blue/red/')
|
||||
feed('2G&3G2&')
|
||||
eq({ 'red a', 'red b', 'red c', 'red d' }, get_lines())
|
||||
eq({ type = 'excmd', lhs = '2&', keys = ':.,.+1s\n' }, pick(atom_last(), 'type', 'lhs', 'keys'))
|
||||
end)
|
||||
|
||||
it('mapping that enters :terminal mode', function()
|
||||
@@ -1110,6 +1126,103 @@ describe('CmdAtom', function()
|
||||
{ type = 'motion', keys = 'f(', lhs = 'f(' },
|
||||
{ type = 'excmd', keys = ':call DelSurround()\n)', lhs = 'ds)' },
|
||||
}, atoms_tail(2, 'type', 'keys', 'lhs'))
|
||||
|
||||
-- Same, mid-op ("t(" + "ds" in one batch): the next mapping is not the pending op's operand.
|
||||
command('nnoremap ,D d')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'a x(one)' })
|
||||
feed('gg0')
|
||||
n.poke_eventloop()
|
||||
feed(',Dt(ds)')
|
||||
eq({ 'one' }, get_lines())
|
||||
eq({
|
||||
{ type = 'operator', keys = 'dt(', lhs = ',Dt(' },
|
||||
{ type = 'excmd', keys = ':call DelSurround()\n)', lhs = 'ds)' },
|
||||
}, atoms_tail(2, 'type', 'keys', 'lhs'))
|
||||
end)
|
||||
|
||||
it('operator completed by a Lua textobject (:omap) #41482', function()
|
||||
-- Lua :omap textobject (starts Visual mode, |omap-info|) captures the operator: `keys` is the
|
||||
-- redobuff (K_LUA + luaref), replayable like "." repeating it. `cmd` is omitted (a Lua
|
||||
-- callback has no notation).
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('o', 'gt', function()
|
||||
vim.cmd('normal! viw')
|
||||
end)
|
||||
]])
|
||||
fn.setline(1, { 'aaa xxx', 'bbb yyy', 'ccc zzz' })
|
||||
atoms_start()
|
||||
feed('gg0wdgt')
|
||||
eq('aaa ', fn.getline(1))
|
||||
local ev = atom_last()
|
||||
eq(
|
||||
{ type = 'operator', operator = 'd', lhs = 'dgt', changed = true },
|
||||
pick(ev, 'type', 'operator', 'lhs', 'cmd', 'changed')
|
||||
)
|
||||
t.matches('^d\128', ev.keys) -- "d" + K_LUA…: the redobuf.
|
||||
feed('j0w')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq('bbb ', fn.getline(2))
|
||||
-- "." repeats it, captured as one "."-labeled operator atom.
|
||||
feed('j0w.')
|
||||
eq('ccc ', fn.getline(3))
|
||||
eq(
|
||||
{ type = 'operator', operator = 'd', lhs = '.' },
|
||||
pick(atom_last(), 'type', 'operator', 'lhs')
|
||||
)
|
||||
|
||||
-- 'operatorfunc' + Lua textobject: "ghgt". Non-edit, still emits its operator atom.
|
||||
n.exec_lua([[
|
||||
_G.oplog = {}
|
||||
vim.keymap.set('n', 'gh', function()
|
||||
vim.o.operatorfunc = function(mtype)
|
||||
table.insert(_G.oplog, mtype)
|
||||
end
|
||||
return 'g@'
|
||||
end, { expr = true })
|
||||
]])
|
||||
feed('gg0ghgt')
|
||||
eq({ 'char' }, n.exec_lua('return _G.oplog'))
|
||||
ev = atom_last()
|
||||
eq(
|
||||
{ type = 'operator', operator = 'g@', lhs = 'ghgt', changed = false },
|
||||
pick(ev, 'type', 'operator', 'lhs', 'cmd', 'changed')
|
||||
)
|
||||
t.matches('^g@\128', ev.keys) -- "g@" + K_LUA….
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq({ 'char', 'char' }, n.exec_lua('return _G.oplog'))
|
||||
|
||||
-- Lua 'operatorfunc' editing via ":norm!" must not clobber the captured "g@" redo/atom.
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('n', 'gr', function()
|
||||
vim.o.operatorfunc = function()
|
||||
vim.cmd([=[normal! `[v`]rX]=])
|
||||
end
|
||||
return 'g@'
|
||||
end, { expr = true })
|
||||
]])
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'mmm nnn', 'ooo ppp' })
|
||||
feed('gg0grgt')
|
||||
eq('XXX nnn', fn.getline(1))
|
||||
ev = atom_last()
|
||||
eq(
|
||||
{ type = 'operator', operator = 'g@', lhs = 'grgt', changed = true },
|
||||
pick(ev, 'type', 'operator', 'lhs', 'changed')
|
||||
)
|
||||
t.matches('^g@\128', ev.keys) -- "g@" + K_LUA….
|
||||
feed('j0')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq('XXX ppp', fn.getline(2))
|
||||
|
||||
-- Aborted operator (cpo+=E empty region) cancels its redo: no atom captured.
|
||||
command('set cpo+=E')
|
||||
n.exec_lua([[vim.keymap.set('o', 'ge', function() end)]])
|
||||
feed('dge')
|
||||
eq('XXX ppp', fn.getline(2))
|
||||
eq(
|
||||
{ type = 'mapping', lhs = 'dge', changed = false },
|
||||
pick(atom_last(), 'type', 'lhs', 'changed')
|
||||
)
|
||||
command('set cpo-=E')
|
||||
end)
|
||||
|
||||
it('|restore-undo-cursor|: `pos` + `undoseq` restore across every undo form', function()
|
||||
|
||||
Reference in New Issue
Block a user