Problem:
The Vim "stuff" concept breaks the ability to reason about the call
stack and thus the boundaries of a `CmdAtom`: a stuffed translation ("x"
=> "dl") defers to the main loop. This "continuation" must be modeled in
`CmdAtom`, by checking global flags at undefined times, during undefined
circumstances.
Solution:
- After a stuffed "translation", eagerly execute the stuff buffer
(`exec_stuffed()`).
- Delete the CmdAtom "continuation" junk.
Note:
- op_colon runs its cmdline "nested", but that's fine because operators
already nest interactive sessions there (op_change runs edit()), and
the cmdline is frameless so the operator's frame can own/capture it.
Problem:
An insert-session entered by a scheduled `feedkeys('i','n')` is
classified on entry (as "not typed"), and not reevaluated after that,
thus user input following it is not captured.
Repro: an `:imap` that does `feedkeys('<esc>','n')` and schedules
re-entering insert, emits one CmdAtom for the first press, then nothing
else.
Solution:
Sample `maptick` (ticked by `gotchars()` on typed input, including
mappings) at session start; if it advanced by session end, the session
is user input.
Problem:
A mapping that enters terminal-mode (`:FzfLua files` via `:startinsert`)
never reaches a composite-end: terminal-mode runs no "normal" CmdFrames.
The composite collects the entire terminal session (and more) into `lhs`.
E.g. if I have `<M-/>` mapped to open `:FzfLua files`, then interact
with fzf-lua UI, the emitted CmdAtom looks like:
<M-/>… => { type='mapping', lhs='<M-/><C-N><C-N><CR>', keys=nil }
Solution:
End the composite when terminal-mode is entered, which emits a more
meaningful and repeatable atom:
<M-/>… => { type='mapping', lhs='<M-/>', keys=nil }
Problem:
The "!" operator stuffs its cmdline continuation (`:.,.+1!`), so its
frame ends before capture (stuff pending) and the redo-prep disappears
with it.
!ipsort<CR> => { type='excmd', lhs=':.,.+1!sort<NL>', keys=':.,.+1!sort<NL>' }
Compare to builtin "." which works bc `do_bang()` completes the redo
(`!ip` + `sort<NL>`).
Solution:
Appoint the stuffed continuation frame as the "redo-prep" frame.
!ipsort<CR> => { type='operator', operator='!', lhs='!ipsort<NL>', keys='!ipsort<NL>' }
Notes:
- atom_cmd_end(): a frame ending with stuff pending re-points its
redo-prep to the next frame.
- atom_cmd_start(): a stuffed continuation frame (`KeyStuffed`) keeps
the redo-prep; flushed stuff discards it.
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}
Problem:
- `lhs` is not fully realized. E.g. for a "payload" mapping
`lhs` omits the `getchar()` payload during a mapping (vim-surround
`ds'` reports `lhs="ds"`). This means plugins like vim-repeat are
still needed...
- The "delta" fields of a CmdAtom are calculated too late.
- `<abuf>` and `changed` check whatever (wrong) buffer a command
(":bnext") might land in.
- CTRL-W_w between two windows on the same buffer reports type="motion".
Solution:
- `CmdOrigin` samples (buf/win/cursor/changedtick) at each "scope" entry
(CmdFrame, composite, Visual session, insert session).
- `dd<C-w>l` reports `changed=true` for the buffer it edited, regardless
of where the cursor ends up.
- New fields:
- `pos`: cursor position at command start.
- `moved`: indicates whether the cursor moved (in same buffer).
- `undoseq`: undo state at settlement.
- lhs now includes the payload: "ds)" reports lhs="ds)" instead of "ds".
- Easy for users to "replay" any atom.
- Rename: type "command" => "normal", "ex" => "excmd"; `arg` => `cmdarg`
- Drop `cascade` field (no reason to expose it)
Problem:
An unreplayable Visual operation does not emit a CmdAtom event. That's
maybe not super important, but it hints at a flaw in how `vatom`
"voiding" is plumbed: `vatom.state=kVatomVoid` replaces the "kind", so
that info is lost to later parts in the lifecycle.
Solution:
- Define `VatomState` as "flags", so `kVatomVoid` can "poison"
`vatom.state` without losing its kind flag.
- Unify `lhs`: always report the original user input in `CmdAtom.lhs`,
for all kinds of user actions: visual, "translated"/"stuffed" cmds,
and dot-repeat (".") itself.
- Unreplayable Visual atom emits CmdAtom with non-empty `lhs` and empty
`keys`, like a mapping/macro composite.
Problem:
- Dot-repeat of a Visual selection prepared by a `<Cmd>` mapping,
results in E1255 and leaves Visual mode active.
- Visual-mode capture is implemented as a second, parallel "capture
engine": it re-composes a CmdSpec from cmdarg_T per key and decides
replayability from its own table of "void" key classes.
Solution:
- Model nested `normal_execute()` as a `CmdFrame` stack,
instead of a single module-scoped `stage`.
- Capture `<Cmd>` ":norm …" commands as subatoms from its nested frames.
- Produce every command exactly once; atom_push_raw() routes the atom to
the Visual composite while a selection is open, like it already does
for mapping composites.
- `v/pat<CR>d` is now repeatable and cascades.
Problem:
`<cmd>` mappings do not emit `CmdAtom.text`.
`<cmd>` and Lua-callback mappings that edit the buffer apply only at the
primary cursor, not cascaded (multicursor).
Solution:
Capture the `<cmd>` command in getcmdkeycmd().
Add kKeyOpaque ("no capturable keys"); narrow kKeySynthetic ("not
a keystroke") to K_EVENT/K_IGNORE, so an opaque mapping's edit still
sets `map_edit` and cascades via LHS-replay.
Problem:
There is no unified notion of a "user action".
Vim processes input by one-char-at-a-time, and mostly throws away any
hints it might gather about the user's action, with one exception: it
stores the last _edit_ action (the "redo buffer", encoded as
unstructured `["x][v][count]body` bytes).
Plugins can only observe individual keys (vim.on_key) and high-level
effects (TextChanged, CursorMoved).
Solution:
- Users can subscribe to `CmdAtom` events to handle any user action.
- Event is deferred; handlers cannot cancel or interfere with user
actions.
- Capture `CmdSpec` from the normal/insert/visual subsystems.
- typeahead/readahead stay unstructured (`buffheader_T`): they are key
streams, not commands.
- the redo/record buffers become `StringBuilder`: fewer
allocations/copies.
- Repurpose the input/redo engine to accept `CmdSpec` objects.
"atom": one repeatable unit of user input, as a resolved (post-mapping)
keysequence plus structured fields. Only user actions, not `:normal`,
API calls, or non-"t" `feedkeys`.
BREAKING: dot-repeat of an Insert session, replays the entire session
including cursor-moves (:help ins-repeat).
BREAKING: dot-repeat of a Visual operation, replays the selection
instead of operating on a fixed-size region.