mirror of
https://github.com/neovim/neovim.git
synced 2026-09-02 20:34:06 +00:00
Problem:
A mapping that ends mid-operation (`nnoremap ,D d`) emits a content-free
"mapping" atom plus a `pending` field, and the "continuation" motion
arrives as a sibling atom. Consumers must stitch the two together (which
has broken cases, e.g. Insert-opening mappings (",i") lose their session
entirely).
- ",i": the session atom is dropped bc the mapping RHS is consumed
before the session starts (typebuf_maplen()==0), so
atom_is_user_input()=false.
- ":normal"-in-opfunc: the opfunc internal "v..y" session (a) became
kVatomTyped just because the deferred composite was open, masking the
real operator capture via atom_captures, and (b) its nested frames
re-derived the outer redo.
- "Motion" based on `moved=true`, has false negatives.
- `CmdAtom.remap` is unnecessary, and clutters the docs/usage.
Solution:
- Introduce `frame_id` to identify CmdFrames.
- Classify `type=motion` better, via `NV_MOTION` flag on the `nv_cmds` table.
- Drop `CmdAtom.pending`, `CmdAtom.remap`.
- Defer atom_composite_end() at the clock edge while an operator is
pending, Visual is active, or `restart_edit` is set: the composite
keeps collecting, so the continuation is captured in the mapping atom.
- ",i": Now an open composite counts as user input.
- ":normal"-in-opfunc: Now handled correctly.
- `remap` is now decided by `composite.payload || 0 subatoms`.
atom_payload_mark() records the read the resolution never captures.
- `toplevel` is now decided by `CmdFrame.parent == NULL`.
before/after:
INPUT BEFORE AFTER
---------------------------------------------------------------------
,D w {mapping lhs=,D pending=operator} {operator lhs=,Dw keys=dw}
+ {operator keys=dw}
ysiw" {mapping lhs=ys pending=operator} {operator lhs=ysiw" keys=g@iw"}
+ {operator lhs=g@iw"}
,v d {mapping pending=visual} {visual lhs=,vd keys=viwd}
+ {visual lhs=viwd}
,i XY<Esc> {normal keys=i lhs=,iXY<Esc>} {insert keys=1iXY<Esc> text=XY}
103 lines
2.8 KiB
Lua
103 lines
2.8 KiB
Lua
-- Helpers shared by the atom-capture specs (mcursor_spec.lua, cmdatom_spec.lua).
|
|
|
|
local n = require('test.functional.testnvim')()
|
|
|
|
local m = {}
|
|
|
|
function m.get_lines()
|
|
return n.buf_lines(0)
|
|
end
|
|
|
|
--- vim.keycode(): |key-notation| => the raw bytes of the CmdAtom event's keys/lhs.
|
|
function m.k(s)
|
|
return n.api.nvim_replace_termcodes(s, true, true, true)
|
|
end
|
|
|
|
--- Starts collecting CmdAtom event-data.
|
|
function m.atoms_start()
|
|
n.exec_lua([[
|
|
_G.atoms = {}
|
|
vim.api.nvim_create_autocmd('CmdAtom', {
|
|
callback = function(ev)
|
|
table.insert(_G.atoms, ev.data)
|
|
end,
|
|
})
|
|
]])
|
|
end
|
|
|
|
--- Gets the collected CmdAtom event-data.
|
|
function m.atoms()
|
|
n.poke_eventloop() -- CmdAtom is deferred, so drain the event loop first.
|
|
return n.exec_lua('return _G.atoms')
|
|
end
|
|
|
|
--- Gets the last collected CmdAtom event.
|
|
function m.atom_last()
|
|
local evs = m.atoms()
|
|
return evs[#evs]
|
|
end
|
|
|
|
--- Projects only the named fields of `ev` (a nil field stays absent, so `eq` still asserts
|
|
--- omission when the expected table lacks it).
|
|
function m.pick(ev, ...)
|
|
local r = {}
|
|
for _, f in ipairs({ ... }) do
|
|
r[f] = ev[f]
|
|
end
|
|
return r
|
|
end
|
|
|
|
--- Gets the last `count` collected CmdAtom events: bare `keys` strings by default, or
|
|
--- projections of the named `fields`.
|
|
function m.atoms_tail(count, ...)
|
|
local evs = m.atoms()
|
|
local fields = select('#', ...) > 0 and { ... } or nil
|
|
local tail = {}
|
|
for i = #evs - count + 1, #evs do
|
|
table.insert(tail, fields and m.pick(evs[i], unpack(fields)) or evs[i].keys)
|
|
end
|
|
return tail
|
|
end
|
|
|
|
--- Minimal vim-surround "ys": an <expr> mapping sets 'operatorfunc' and returns "g@"; the opfunc
|
|
--- reads the wrap char with getchar() and wraps the motion region (yank, modify register, paste
|
|
--- back).
|
|
m.minisurround_vim = [[
|
|
function! MiniSurroundSetup() abort
|
|
set operatorfunc=MiniSurround
|
|
return 'g@'
|
|
endfunction
|
|
function! MiniSurround(type) abort
|
|
let char = nr2char(getchar())
|
|
let save = getreg('"')
|
|
silent exe "norm! v`[o`]y"
|
|
call setreg('"', char .. getreg('"') .. char, 'v')
|
|
silent exe "norm! gvp`["
|
|
call setreg('"', save)
|
|
endfunction
|
|
nnoremap <expr> ys MiniSurroundSetup()
|
|
]]
|
|
|
|
--- Minimal vim-sneak: :omap whose ":call" reads a 2-char getchar() and moves the cursor.
|
|
m.minisneak_vim = [[
|
|
function! MiniSneak() abort
|
|
let c1 = nr2char(getchar())
|
|
let c2 = nr2char(getchar())
|
|
call search('\V' . c1 . c2, 'W')
|
|
endfunction
|
|
onoremap <silent> z :<C-U>call MiniSneak()<CR>
|
|
]]
|
|
|
|
--- Minimal vim-surround "ds": a ":call" mapping whose edit runs through :normal inside a
|
|
--- function, with a getchar() payload naming the surround to delete.
|
|
m.delsurround_vim = [[
|
|
function! DelSurround() abort
|
|
call getchar()
|
|
" cursor is on the "("; delete it and its matching ")".
|
|
normal! mz%x`zx
|
|
endfunction
|
|
nnoremap <silent> ds :<C-U>call DelSurround()<CR>
|
|
]]
|
|
|
|
return m
|