aboutsummaryrefslogtreecommitdiff
path: root/test/functional/editor/cmdatom_spec.lua
Commit message (Collapse)AuthorAge
* test: simplify functional test expectationsLewis Russell2026-10-05
| | | | | | | | | | | | Problem: Repeated default fields and field-by-field assertions obscure the values that each functional test is meant to check. Solution: Add eq_partial() to check selected record fields while preserving list length and order. Use local helpers to fill fixed expected defaults where tests need full equality. Keep explicit absence checks and test-specific values at the call sites. AI-assisted
* fix(input): aborted dot-repeat causes ModeChanged during emsg() #42181Justin M. Keyes2026-09-30
| | | | | | | | | | Problem: When a flush discards dot-repeat keys mid-selection, flush_buffers() ends Visual-mode immediately, triggering `ModeChanged` autocmds from `emsg()` (before the error is shown) or `vgetorpeek()`. Solution: End Visual-mode from `normal_finish_command()` by checking a flag, similar to `Visual.restart_select`.
* fix(multicursor): zq edge cases; cascade perf #42165Justin M. Keyes2026-09-30
| | | | | | | | | | | - `{Visual}Q` / `{Visual}zqj`: for linewise motion ("j"), every line gets a cursor. - `zq`: handle 3-key motions (`g'a`), don't scroll the view, preserve the search direction, update existing cursors `gv` target. - Disallow `zq` in macros. - Abort `zq` on non-motion ("zqx"). - Classify `g'x` classify like `'x`. - Classify `'(` `'{` etc. as `type=motion` (they are cursor-relative). - Perf: skip save/restore of marks when cascading motions ('follow').
* fix(input): end Visual mode when aborting redo #42151Qiming zhao2026-09-30
| | | | | | | | | | | Problem: Skipping buffer flushing in Visual mode breaks map-error and leaves failed redo motions that raise errors stuck in Visual mode. Solution: Keep flushing on errors and exit Visual mode when pending redo is discarded. Cover failed motions, unset marks, macros, and mappings. AI-assisted
* feat(multicursor): zq operator; cursor-local marks #42148Justin M. Keyes2026-09-29
| | | | | | | | | | | | | | | | | | Problem: - `[count]Q` and `{Visual}Q` are implemented as ad hoc utils, because `Q` is not an "operator". - `Q` always "toggles", it is not "idempotent". - Various *cursor-relative* marks (`'.`, `'[`) are not saved/restored per-cursor, so they don't work during cascade. - Insert cascade clobbers the primary's `'^` and changelist. Solution: - Introduce the `zq{motion}` operator, which places cursors at every reachable `{motion}`. - `{Visual}Q` is now an alias to `{Visual}zqj`. - Introduce `1Q` and `2Q` for idempotent cursor placement. - Drop `[count]Q`, it's redundant with `zqgn`. - Classify cursor-relative marks as `type=motion` instead of `type=jump`. Save/restore per-cursor during cascade.
* fix(multicursor): "gv" + Insert-entering cmd #42110Justin M. Keyes2026-09-27
| | | | | | | | | | | | | | | | | | | | | Problem: - Multicursor does not cascade a `gv`-entered insert-session (`gvc`). - CmdAtom of a `gv`-entered insert-session (e.g. `gvcX<Esc>`) has `keys="1vcX<Esc>"` (redo's fallback), not the typed keys. And is not emitted at all if multicursor is inactive. Analysis: Redo can't contain "gv", so it does the "1v" fallback, and the insert-cascade replays redo. Solution: Replay "gv" + op at each cursor: store the typed selection in `InsSession.vsel`; it supplants redo's "1v" in the emitted atom and in the insert-cascade replay. A cursor without a Visual area skips a "gv"-based replay (else its keys would run as Normal-mode: "gvrx" => "rx"). Redo behavior is unchanged ("1v" fallback): "gv" is absolute, "." acts at the cursor.
* feat(multicursor): jump vs motion #42052Justin M. Keyes2026-09-23
| | | | | | | | | | | | | | | | | | | | Problem: Incoherent classification of motions vs jumps. Solution: Define "motion" as cursor-relative, and "jump" as "absolute". - Classify G/gg/H/L/M/go/N% as `CmdAtom.type=jump`. - Classify `*`/`#` as motions. Behavior changes: - `*` in follow-mode now moves each cursor to the next occurrence of its own word. - Note: the "last search pattern" is not stored per-cursor, so `n` still searches for the primary's search pattern. - G/gg/H/M/L in follow-mode do not cascade (would converge/dedupe all cursors). - If you want to force cursors to converge for some reason, use `9999j`. - Also affects mappings that mix an absolute move with an edit. E.g. mapping `nnoremap X ggdd` only replays `dd` per-cursor now. This is how marks already behaved.
* fix(multicursor): cascade mapping-edit during Visual selection #42027Justin M. Keyes2026-09-21
| | | | | | | | | | | | | | | | Problem: - A mapping that edits the buffer during a Visual selection applies only at the primary cursor. The edit "voids" the pending Visual-session, because `mc_vsel_refresh` only previews the selection per-cursor and must not edit. - A cursor overlapping the primary applies an edit twice, if the command moves the primary before cascade ("yiwp"). Solution: Cascade a Visual session's edits span-by-span, like an insert-session. - Each span is prefixed with "gv" so per-cursor reselect works. - Void only if no subatom was captured. - Dedupe cursors against the primary's anchor at cmd start, not its position after.
* fix(cmdatom): capture <Cmd>, not its subatoms #42020Justin M. Keyes2026-09-21
| | | | | | | | | | | | | | | | | | | Problem: A command's `:norm` children are captured as Visual subatoms: `v<Cmd>normal! e<CR>d` is "decomposed" to `ved`. That is a cosmetic, nice-to-have feature, which greatly complicates the cmdatom logic: it must detect "lossy" cases (API moves, "gv" on marks the command set, a session ended and restarted). Solution: Drop the nice-to-have. - A command is captured as itself, never its `:norm` children. - While a child frame runs, a pending Visual session is kept open until the enclosing frame resolves. - `:`/`<Cmd>` no longer decline to be a Visual subatom. - Stuffed keys ("." replay, "x" => "dl") and K_EVENT children (RPC, timer), are still captured as their translation. - `mc_add()` discards any pending Visual session, so `{Visual}Q` does not cascade recursively to the cursors it just created.
* fix(multicursor): replay of Lua mapping-id ending in "0" #42011Justin M. Keyes2026-09-20
| | | | | | | | | | | | | | | | Problem: - A captured Lua mapping (`K_LUA`+id) whose id ends in "0" replays the wrong mapping: `atom_from_cmdline()` escapes the id like a typed cmdline (trailing "0" => CTRL-V "048"), but the id is read raw. E.g. treesitter `an` (id 60) replays `gc` (id 6), so `van` does not preview/cascade at mcursors. - Same for `<Cmd>…` ending in "0" or "^" (`<Cmd>normal! 0<CR>`). - `c` + Lua/":" textobj (`canX`) does not insert-cascade. Solution: - Only CTRL-V-escape ":", "/" cmdline; append `<Cmd>` and Lua mapping-ids "raw" (`sb_add_spec()`). - `kVInsMotion`: an insert entered by Ex/Lua motion re-executes the motion, unless it read input.
* fix(multicursor): selection by API/Lua from mapping #41999Justin M. Keyes2026-09-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: A Visual selection produced by a Lua mapping is captured as the keys it fed (`:norm!`), so non-primary cursors get the wrong selection. Examples: - API cursor-moves around a fed "o" are lost. - `setpos("'<")` + "gv" reselects the primary's marks (treesitter `an`). - A mapping that feeds an absolute selection (`:norm! <Esc>`, then `norm! 3GV6G`). Solution: Classify fed keys as `lossy` if their produced selection differs from the selection at mapping-end, or fed-key-start: the mapping moved it by other means (API). Same idea as `composite.lossy` LHS-replay. Now the captured Visual session contains the mapping's own cmd (`<Cmd>…`, `:…`, or a Lua mapping-id) as one subatom, instead of the keys it fed. E.g. `v…<K_LUA id>` instead of `v…V…G`. Also affects dot-repeat ("."). A `:norm!` that ends a typed selection keeps the Visual session (`vatom`) open (instead of the usual `atom_visual_reset()` plus emit/stage), so the enclosing user frame (the mapping's `<Cmd>`/Lua, or a typed `:norm!`) can complete or replace it. - A child frame re-entering Visual does not restart it, so a later `norm! V` continues that session instead of a new `kVatomFed` one. - At end of the user (toplevel) frame, the fed subatoms are dropped in favor of the mapping's own atom: as a subatom if Visual is active, else as the session's closing suffix.
* fix(cmdatom): feedkeys() without "t" emits CmdAtom #41996Justin M. Keyes2026-09-19
| | | | | | | | | | | | | Problem: `nvim_feedkeys('gj', 'm')` (remap, but not "t"=typed) emits a CmdAtom for the mapping's insert session. Same for `feedkeys('ihello<Esc>', '')`. Keys fed without "t" land in `tb_maplen` ("mapped prefix"), just like a mapping's expansion, and `atom_is_user_input()` treats that as user input. Solution: Narrow the `atom_is_user_input` condition. A mapping resolved from typed keys always has a composite by the time its commands run, so `KeyTyped || (user cmd && composite active)` is sufficient.
* fix(multicursor): visual "gv", change-marks '[,'] #41988Justin M. Keyes2026-09-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: - `gv` reselects only primary's region, it doesn't have per-cursor behavior. '< '> are buffer-global and the sandbox restores the primary's marks. The Visual atom containing `gv` was voided anyway, so it never cascaded. - Same for change-marks (`'[ '] '.`): they are stale after edits from other cursors, not managed per-cursor. - `McSandbox` saves "editor state", which should be owned by the `Context` module. Solution: Lift more state from `McSandbox` into `Context`. `kCtxCursor` (position, curswant) and `kCtxMarks` (Visual area, change-marks), are owned by `Context` now, as an "in-memory" special-case (TODO: support it for the "serialized" case?). - Classify a `gv` selection as replayable. Manage its state per-cursor. Dot-repeat "." after `gv` behaves like Vim (the "1v" fallback). - Save/restore change-marks per-cursor. The primary's change-marks are shifted by edits from other cursors during the cascade. `McSandbox` still contains (for now) the view and replay isolation (typeahead/mode, redobuff, search patterns, the active selection). Bonus: - `visualmode()` is per cursor. - A cursor's `curswant` follows its column when other cursors' edits shift it, instead of the column captured after its own replay.
* fix(cmdatom)!: diff-mode "do"/"dp" not captured properly #41982Justin M. Keyes2026-09-18
| | | | | | | | | | Problem: - "do"/"dp" (:diffget/:diffput) clear the "d" operator before `do_pending_operator()`, so the atom captured only "o"/"p". - Dot-repeat "." does not repeat do/dp (as in Vim). Solution: - Capture the atom (and prep its redo) before the operator is cleared. - Support dot-repeat for do/dp (diverges from Vim).
* refactor(undo): redundant "forget" logic #41980Justin M. Keyes2026-09-18
| | | | | | | | | | | | | | | Problem: `u_undo_and_forget()` and `u_forget_unchanged()` do similar undo-tree surgery: unlink a header, promote the alt branch it displaced. Solution: Extract common logic to `u_forget_header()`. Bonus: - The `:undo!` copy now fixes `b_u_oldhead`, so after forgetting a root header, its alt branch is still listed by `undotree()`. - Freeing can no longer unlink the promoted branch: `uh_alt_prev` is cleared first. The old code relied on the forgotten header being the first sibling.
* fix(cmdatom): mapping tail after a mode-change is not captured #41897Justin M. Keyes2026-09-13
| | | | | | | | | | | | | | | | Problem: `inoremap <Esc> <Esc>l` starts in insert-mode and ends in normal-mode ("l"). Its normal-mode "l" is not captured as a CmdAtom (and thus doesn't cascade to multicursors in follow-mode). Analysis: CmdFrames only delineate in Normal/Visual/Op-pending (i.e. the `MODE_NORMAL` modes), so a mapping resolved in Insert, Cmdline or Terminal mode does not start a `composite`, then the mapping "tail" is lost. Solution: Start a "deferred" `composite` if state is not `MODE_NORMAL`, instead of skipping it entirely.
* fix(cmdatom): don't capture inputsecret() #41742Justin M. Keyes2026-09-07
|
* fix(multicursor): live-preview ":norm" visual selection #41736Justin M. Keyes2026-09-06
| | | | | | | | | | | | | | Problem: A Visual selection opened programmatically (`:normal! viw`) is not previewed at the other cursors, and a user-typed operator completing it does not cascade. ":normal" keys run as child frames, which are never user input, so the session they build was classified "fed" once and never revisited. Solution: Recalculate the session kind (`vatom.state`) on every frame. Record `vatom.frame` (the last frame to add to the session) so an enclosing frame does not reset or recapture it.
* fix(cmdatom): exclude <expr> EVALUATION input from `CmdAtom.keys` #41718Justin M. Keyes2026-09-05
| | | | | | | | | | | Problem: Input (`getchar()`, `input()`, …) consumed by an expr mapping EVALUATION can leak into `CmdAtom.keys`, depending on count placement ("2dse" vs "d2se"). Solution: Skip payload capture while `expr_map_lock` is active. - Input read during expr EVALUATION is recorded in `CmdAtom.lhs`. - Only input read by mapping EXECUTION is appended to `CmdAtom.keys`.
* fix(multicursor): abandoned Visual selection does not move cursors #41670Justin M. Keyes2026-09-03
| | | | | | | | | | Problem: "v{motion}<Esc>" moves the primary cursor to the selection end, but the other cursors snap back to their anchors unless follow-mode ("q=") is on. The selection preview showed them at the selection end. Solution: Replay the abandoned selection at every cursor, not only in follow-mode. An abandoned typed selection now always emits a "visual" CmdAtom.
* feat(multicursor): MC HAMMER #41587Justin M. Keyes2026-09-01
| | | | | | | | | | | | | | | | | | | | | Other (squashed) commits: fix(tui): emit ui_send output atomically with the frame Problem: tui_ui_send() writes directly to the TTY, bypassing the output buffer. Sequences sent via nvim_ui_send() (e.g. kitty multiple-cursors, or visual-dot-repeat) always arrive in a separate TTY write from the frame they were computed for. This manifests as "tearing", or e.g. in the case of multicursor the terminal renders text with stale cursor overlays. Solution: - tui_ui_send(): while a frame is being assembled (pending invalid regions or buffered output), buffer instead of writing directly. - Out-of-frame sends (tty queries, clear-on-disable) still write immediately. - mcursor.lua: emit the terminal-cursor update at the end of the redraw cycle (`on_end`, when screen positions are final) instead of vim.schedule().
* fix(cmdatom): edit-repeat mapping #41564Justin M. Keyes2026-08-30
| | | | | | | | Problem: The "." example mapping at `:h edit-repeat` doesn't work well with `nvim_feedkeys(…, 'mt', false)`. Solution: Use `vim.b[ev.buf].maxseq` instead of `undotree()`.
* fix(mappings): replaying a deleted Lua mapping is UBJustin M. Keyes2026-08-31
| | | | | | | | | | | | | | | | | | | | | | | | | Problem: Replaying a deleted Lua mapping, may call an arbitrary function. RHS of a Lua mapping embeds its LuaRef (`<K_LUA><ref><CR>`). The raw keys may outlive the ref (redobuff ".", CmdAtom `keys`). If the mapping is deleted, replaying it either (1) dereferences a freed registry slot, or (2) calls whatever callback reused the slot (autocmd, timer, other mapping). Solution: Assign a monotonic (never recycled) id to Lua mappings and encode the mapping keys as `<K_LUA><id><CR>`. Note: in the case of Vimscript, a deleted function raises E117, but if the function is redefined with the same name, the mapping will find it. Alternatives?: - Globally ensure `LuaRef` ids are not recycled. - Problem: could exhaust `int` in a long-lived Nvim session? Also, difficult to impl bc the "recycling" is done by `luaL_ref` itself. ref: 5ac2e47acc999472042df4f10f8f7b5ffa72ba3e
* fix(cmdatom): operator with Lua textobject is not type=operatorJustin M. Keyes2026-08-31
| | | | | | | | | | | | | | | | | | | | | | | | 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...
* refactor(input): exec stuffed keys eagerlyJustin M. Keyes2026-08-29
| | | | | | | | | | | | | | | | | | | 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.
* fix(cmdatom): insert-session entered by feedkeys() ignores typed input #41518Justin M. Keyes2026-08-27
| | | | | | | | | | | | | | | 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.
* fix(cmdatom): terminal-mode keys leak into mapping lhs #41455Justin M. Keyes2026-08-23
| | | | | | | | | | | | | | | | | 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 }
* fix(cmdatom): "!" operator hardcodes its range #41451Justin M. Keyes2026-08-23
| | | | | | | | | | | | | | | | | | | | | | 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.
* feat(cmdatom): mappings capture continuationJustin M. Keyes2026-08-22
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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}
* feat(cmdatom)!: eliminate the need for vim-repeat #41414Justin M. Keyes2026-08-21
| | | | | | | | | | | | | | | | | | | | | | | | | 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)
* fix(cmdatom): lhs not always reported in CmdAtom #41386Justin M. Keyes2026-08-20
| | | | | | | | | | | | | | | | 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.
* fixfix(cmdatom): repeat Visual <Cmd>; CmdFrame + stage #41355Justin M. Keyes2026-08-18
| | | | | | | | | | | | | | | | | 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.
* fix(cmdatom): <Cmd> mappings #41347Justin M. Keyes2026-08-16
| | | | | | | | | | | | 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.
* feat(input)!: CmdAtom event #41297Justin M. Keyes2026-08-14
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.