Commit Graph

10 Commits

Author SHA1 Message Date
Justin M. Keyes
2514256d95 refactor(input): exec stuffed keys eagerly
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.
2026-08-29 01:06:26 +02:00
Justin M. Keyes
61df463c5e fix(cmdatom): insert-session entered by feedkeys() ignores typed input #41518
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.
2026-08-27 10:48:33 -04:00
Justin M. Keyes
69bf8c7792 fix(cmdatom): terminal-mode keys leak into mapping lhs #41455
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 }
2026-08-23 15:04:11 -04:00
Justin M. Keyes
bb82f9612c fix(cmdatom): "!" operator hardcodes its range #41451
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.
2026-08-23 13:58:45 -04:00
Justin M. Keyes
47cd769ed5 feat(cmdatom): mappings capture continuation
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}
2026-08-22 17:29:34 +02:00
Justin M. Keyes
6423657352 feat(cmdatom)!: eliminate the need for vim-repeat #41414
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)
2026-08-21 13:42:17 -04:00
Justin M. Keyes
31de0d69fc fix(cmdatom): lhs not always reported in CmdAtom #41386
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.
2026-08-20 05:18:36 -04:00
Justin M. Keyes
f6bf814378 fixfix(cmdatom): repeat Visual <Cmd>; CmdFrame + stage #41355
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.
2026-08-18 07:50:27 -04:00
Justin M. Keyes
581ce0b3da fix(cmdatom): <Cmd> mappings #41347
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.
2026-08-16 18:00:55 -04:00
Justin M. Keyes
64a301184e feat(input)!: CmdAtom event #41297
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.
2026-08-14 09:30:31 -04:00