aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/input_cmdatom.h
Commit message (Collapse)AuthorAge
* fix(multicursor): don't dedupe primary-overlapping cursor #42060Justin M. Keyes2026-09-24
| | | | | | | | | | | | | | Problem: A primary-overlapping cursor is deduped before an edit. But if a Visual/Insert action moves the primary first, the cursor is not skipped (e.g. `vipQva)c` replays at the primary-overlapping cursor). Solution: Skip _replay_ of a primary-overlapping cursor, but don't _dedupe_ it. Keep its position synced with the primary. Sample the primary-overlapping cursor (`CmdOrigin.mcursor`) at the action start (first root frame, or Visual session-start), instead of current frame start.
* fix(multicursor): edit applied twice at cursor overlapping the primary #42050Justin M. Keyes2026-09-23
| | | | | | | | | | | Problem: A cursor overlapping the primary is deduped by comparing positions at the clock edge, i.e. after the primary edit executed. A "replace" at the cursor cell ("rx", "~", "g~iw") shifts the overlapping extmark, so it no longer matches and gets its own replay. E.g. "vipQg~~" applies twice to the 2nd line, which inverts the effect. Solution: Decide "overlaps the primary" at cmd start, before the edit.
* 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): 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(multicursor): "q=" before an API cursor-move is ignored #41899Justin M. Keyes2026-09-13
| | | | | | | | | | | | | Problem: A mapping that toggles follow-mode before/after an API cursor-move cascades the move if follow-mode was on from the start. Why: Follow-mode is sampled only at command start, and the API move has no CmdFrame of its own. Solution: Record follow-mode at the time of the move, not the start of the frame. Update `CmdFrame.follow` on all "undecided" parent frames. A frame that already moved keeps its old value (not retroactive).
* 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.
* refactor(cmdatom): merge `curcmd` into root CmdFrameJustin M. Keyes2026-09-11
| | | | | | | | | | | | | | | | Problem: - `curcmd` has the lifetime of the root CmdFrame, but lives in its own container with a reset (atom_redo_reset) that must mirror "toplevel user command". - `curcmd.follow` is also the wrong scope: the clock-edge is deferred across a mapping's keys, each a toplevel command, so `nnoremap <F1> 1q=j2q=` runs "j" in follow-mode, yet the edge reads the trailing "2q=" and does not cascade the queued motion. Solution: - Eliminate `curcmd`. Use the root `CmdFrame` as the source of truth. - Record follow-at-first-motion on `composite`, which spans exactly the frames the deferred clock-edge closes.
* fix(multicursor): q= (toggle follow) is not synchronousJustin M. Keyes2026-09-11
| | | | | | | | | | | | | | | | | Problem: The clock-edge that decides whether queued motions cascade, reads the global `mc_follow_motion` at the end, which may differ since the command started/executed. Examples: - A mapping like `2q=gg1q=` (off, move, on) cascades the "gg" and merges the cursors, because follow=on when the clock-edge resolves stuff. - `{Visual}Q` after a mapping enables follow at the end, and the clock-edge mistakenly cascades the mapping's keys. Solution: Save `CmdFrame.follow` in `curcmd`, and pass it to `mc_clock_edge()`. Innermost-frame wins, so `q=` performed by a subatom/subcmd wins.
* fix(multicursor): failed motion at primary is still cascaded #41812Justin M. Keyes2026-09-09
| | | | | | | | | | | | | | | Problem: In follow-mode, a motion that fails at the primary cursor (e.g. "j" at EOB) still cascades to other cursors, so they move while the primary does not, which causes them to be deduplicated... Also affects Visual sessions: "Vjd" at EOB deletes 1 line at the primary but (potentially) 2 lines at every multicursor. Solution: If a motion "beeps", don't replay it. "3w" near the end still replays (it moved), as does "0" at column 0 (does not beep). This feels like a usable and intuitive compromise.
* 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().
* 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.
* feat(editor): undo restores cursor position #41520Justin M. Keyes2026-08-27
| | | | | | | | | | | | | | | | | | Problem: Undo places the cursor wherever the cursor happened to sit at "save time" (`uh_cursor` is sampled lazily on the first change). Examples: - `i` preserves, but `a` does not - `diw`, `atest<Esc>`, `d^` abandon the original position - `D`, `o` restore it (by accident). Solution: `composite` tracks the pending atom (and its `origin`) across frames. A `stuffed` continuation frame inherits the `origin` + prepped redo. Store `origin` info in the undo header, so undo can restore it. - Not for a mid-command undo break (i_CTRL-G_u). - Undoing a mapping restores where the mapping started (which technically may be different than where the "edit" started).
* 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)
* 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.
* 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.