mirror of
https://github.com/neovim/neovim.git
synced 2026-09-03 04:43:48 +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:
@@ -575,7 +575,7 @@ void redo_free_all(void)
|
||||
void prep_redo(bool as_atom, bool arg_meta, CmdSpec spec)
|
||||
{
|
||||
if (as_atom) {
|
||||
atom_redo_set(spec);
|
||||
atom_redo_prepped();
|
||||
}
|
||||
redo_new(spec);
|
||||
if (block_redo) {
|
||||
@@ -593,7 +593,7 @@ void prep_redo_visual(const char *keys, size_t len, CmdSpec spec)
|
||||
CmdSpec stored = spec;
|
||||
stored.regname = 0;
|
||||
stored.count = 0;
|
||||
atom_redo_set(stored);
|
||||
atom_redo_prepped();
|
||||
redo_new(stored);
|
||||
if (block_redo) {
|
||||
return;
|
||||
@@ -611,6 +611,7 @@ void redo_cancel(void)
|
||||
return;
|
||||
}
|
||||
|
||||
atom_redo_cancel();
|
||||
kv_destroy(redobuff.cur.body);
|
||||
redobuff.cur = redobuff.old;
|
||||
redobuff.old = (CmdSpec){ 0 };
|
||||
|
||||
@@ -115,7 +115,7 @@ static struct {
|
||||
|
||||
/// Per-command capture scratch.
|
||||
static struct {
|
||||
uint64_t redo_frame; ///< The CmdFrame that prepped redo (prep_redo*()). 0: none.
|
||||
uint64_t redo_frame; ///< Frame whose redobuf (potentially) defines the atom. 0: none.
|
||||
char *cmdline; ///< The ":" payload captured at cmdline accept. NULL: none.
|
||||
///< Note: search payloads ("/pat<CR>") travel on `cmdarg.searchbuf`.
|
||||
bool ins_cascaded; ///< Did the command's insert-session already cascade?
|
||||
@@ -294,7 +294,7 @@ static String atoms_concat_keys(CmdAtomVec atoms)
|
||||
/// Renders a (cmd, arg, op) char for CmdAtom: key-notation for special keys, else UTF-8. NUL => "".
|
||||
static char *atom_key_name(int c)
|
||||
{
|
||||
if (c == NUL) {
|
||||
if (c == NUL || c == K_LUA) {
|
||||
return xstrdup("");
|
||||
}
|
||||
if (IS_SPECIAL(c) || c < ' ') {
|
||||
@@ -840,19 +840,20 @@ void atom_op_global_set(void)
|
||||
curcmd.op_global = true;
|
||||
}
|
||||
|
||||
/// Sets `curcmd.redo_frame`: at frame end, the redobuf defines `CmdAtom.keys`.
|
||||
/// Not for nested frames (":norm"), nor Lua operators.
|
||||
void atom_redo_set(CmdSpec spec)
|
||||
/// Declares that the current frame prepped redo. Not for nested frames (":norm").
|
||||
void atom_redo_prepped(void)
|
||||
{
|
||||
if (spec.cmd == K_LUA) {
|
||||
atom_redo_reset();
|
||||
return;
|
||||
}
|
||||
if (atom_is_user_cmd()) {
|
||||
curcmd.redo_frame = cur_frame != NULL ? cur_frame->id : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Redo-prep was canceled (aborted operation).
|
||||
void atom_redo_cancel(void)
|
||||
{
|
||||
curcmd.redo_frame = 0;
|
||||
}
|
||||
|
||||
/// Starts accumulating a composite for a macro's commands, labeled "@x".
|
||||
void atom_macro_start(int regname)
|
||||
{
|
||||
@@ -889,6 +890,10 @@ void atom_map_start(const char *lhs, size_t len, bool peeked)
|
||||
return;
|
||||
}
|
||||
if (atom_composite_active()) {
|
||||
if (get_real_state() == MODE_OP_PENDING) {
|
||||
// The pending operator's own composite ("gr" <expr> mapping => "g@") absorbs its operand.
|
||||
return;
|
||||
}
|
||||
// Mapping resolved from another's trailing prefix ("nmap x j," + "nnoremap ,w w"): end the
|
||||
// pending composite, so each `lhs` owns only the keys it produced.
|
||||
typed.map_start = kv_size(typed.keys);
|
||||
@@ -1270,7 +1275,9 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
&& (!Visual.active
|
||||
|| (equalpos(old->visual.start, Visual.start)
|
||||
&& old->visual.mode == Visual.mode));
|
||||
if (opaque && unchanged) {
|
||||
if (opaque && unchanged
|
||||
// Operator atom? (non-edit Lua/<Cmd> 'operatorfunc'). #41482
|
||||
&& curcmd.redo_frame != old->id) {
|
||||
return;
|
||||
}
|
||||
bool ins_cascaded = user && curcmd.ins_cascaded;
|
||||
@@ -1355,8 +1362,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
// Route: decide the atom type and push it.
|
||||
//
|
||||
if (curcmd.redo_frame == old->id && !mouse_cmd) {
|
||||
// Not for mouse commands (middle-click paste): pasting at every cursor would use
|
||||
// viewport-dependent positions.
|
||||
// Redoable edit ("dw", "p", "rX", "g@…"): this frame's redobuf defines the atom.
|
||||
CmdAtom atom = atom_from_redo(kAOperator);
|
||||
// The payload ('operatorfunc' getchar()) is not in the captured redo, append it.
|
||||
atom_payload_append(&atom, old);
|
||||
@@ -1371,7 +1377,7 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
}
|
||||
} else if (ca->searchbuf != NULL && (ca->cmdchar == '/' || ca->cmdchar == '?')
|
||||
&& !(vis && unchanged)) {
|
||||
// Payload typed in the cmdline ("/pat<CR>"). Emit-only. Not if pattern was not found.
|
||||
// Payload typed in search cmdline ("/pat<CR>"). Emit-only. Not if pattern was not found.
|
||||
CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf);
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, &atom);
|
||||
@@ -1417,8 +1423,8 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes a cmd at normal_execute() exit: captures its atom, pushes its staged one, ends the
|
||||
/// composite. Pops the frame.
|
||||
/// Completes a normal_execute(): captures its atom, pushes its staged one, ends the composite.
|
||||
/// Pops the frame.
|
||||
void atom_cmd_end(cmdarg_T *ca, CmdFrame *old)
|
||||
{
|
||||
atom_capture_cmd(ca, old);
|
||||
|
||||
@@ -4557,6 +4557,7 @@ static void nv_replace(cmdarg_T *cap)
|
||||
// Other characters are done below to avoid problems with things like
|
||||
// CTRL-V 048 (for edit() this would be R CTRL-V 0 ESC).
|
||||
if (had_ctrl_v != Ctrl_V && cap->nchar == '\t' && (curbuf->b_p_et || p_sta)) {
|
||||
atom_stuff_start(cap);
|
||||
stuffnumReadbuff(cap->count1);
|
||||
stuffcharReadbuff('R');
|
||||
stuffcharReadbuff('\t');
|
||||
|
||||
@@ -3185,10 +3185,16 @@ static void op_function(const oparg_T *oap)
|
||||
const bool save_finish_op = finish_op;
|
||||
finish_op = false;
|
||||
|
||||
// Preserve prepped "g@" redo from the callback's own commands.
|
||||
// Like Vimscript call_user_func() does.
|
||||
RedoState save_redo;
|
||||
save_redobuff(&save_redo);
|
||||
|
||||
typval_T rettv;
|
||||
if (callback_call(&p_opfunc, 1, argv, &rettv)) {
|
||||
tv_clear(&rettv);
|
||||
}
|
||||
restore_redobuff(&save_redo);
|
||||
|
||||
virtual_op = save_virtual_op;
|
||||
finish_op = save_finish_op;
|
||||
|
||||
@@ -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