aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/input_cmdatom.c
Commit message (Collapse)AuthorAge
* refactor(cmdatom): add payload in atom_from_frame() #42325Justin M. Keyes7 days
| | | | | | | Problem: Redundant code led to bugs like 2323746bfe4af534979249b051e7ada11c63b389 Solution: Append the payload in atom_from_frame().
* fix(multicursor): Visual-mode mapping drops getchar() payload #42324Justin M. Keyes8 days
| | | | | | | | | | Problem: A Visual-mode mapping that reads input (`getchar()`) does not include that input when cascading to other cursors (they only see ESC). OTOH, Normal-mode mappings do replay the input. Solution: Append the payload to the Visual session's Lua-mapping subatom, and to the session-ending command (`:<C-u>…` mapping).
* 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').
* refactor(multicursor)!: drop nvim_mcursor #42156Justin M. Keyes2026-09-30
| | | | | | | | | | | | | | Problem: `nvim_mcursor` is not fully baked, and not strongly motivated, currently. The main programmatic interface for cursors is the "nvim.multicursor" extmarks namespace. Solution: Drop `nvim_mcursor`. Add an internal hook so that multicursor can react when extmarks are added to the "nvim.multicursor" namespace. TODO: If extmarks/namespaces are clumsy to work with, we should _fix that_ rather than adding wrappers.
* 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.
* fix(coverity): kv_ensure_space overflow #42091Justin M. Keyes2026-09-25
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: gcc and Coverity report potential `size + len `overflow kv_concat_len(). CID 656305: Null pointer dereferences (FORWARD_NULL) /src/nvim/input_cmdatom.c: 474 in atom_push_raw() 468 atom_captures++; 469 } 470 if (collect) { 471 composite.queued = true; 472 } 473 kv_push(vatom.atoms, *atom); >>> CID 656305: Null pointer dereferences (FORWARD_NULL) >>> Dereferencing null pointer "cur_frame". 474 vatom.frame = cur_frame->id; 475 return; 476 } 477 atom_captures++; 478 if (atom->type == kAVisual && kv_size(atom->atoms) > 0) { 479 // The completing operator is the only subatom that could have edited. CID 656304: Memory - illegal accesses (OVERRUN) /src/nvim/input.c: 718 in sb_add_spec() 712 /// bytes. Unlike sb_add_lit(), no CTRL-V: for text read raw (<Cmd>), not typed into a cmdline. 713 void sb_add_spec(StringBuilder *buf, const char *s) 714 { 715 while (*s != NUL) { 716 if ((uint8_t)(*s) == K_SPECIAL && s[1] != NUL && s[2] != NUL) { 717 // Insert special key literally. >>> CID 656304: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "buf->items" at offset corresponding to index variable "buf->size" through dereference in call to "memcpy". 718 kv_concat_len(*buf, s, 3); 719 s += 3; 720 } else { 721 sb_add_char(buf, mb_cptr2char_adv(&s)); 722 } 723 } Solution: Both are false positives; assert or check to avoid the warnings.
* fix(multicursor): unsynced primary-overlapping cursor #42089Justin M. Keyes2026-09-25
| | | | | | | | | | | | | | | | | | Problem: - The primary-overlapping cursor is synced to primary only after cascade, so it may display at a shifted position (e.g. after "rx") during cascade. - LHS-replay edits apply twice if there is a primary-overlapping cursor. Solution: - Sync the primary-overlapping cursor _before_ replay. This ensures the mark is correct _during_ the cascade. - Drop the post-cascade sync; this is safe because the overlapping mark and the primary-tracking mark are both right-gravity, so they get shifted the same way. - Set `origin` on LHS-replay atoms, so the cascade can compare it. Bonus: a buffer-switching mapping no longer LHS-replays if it ends up in a different buffer.
* fix(multicursor): insert-mode <c-r>x per-cursor registers #42076Justin M. Keyes2026-09-24
| | | | | | | | | | | | Problem: Insert-mode `<c-r>x` stuffs the literal text, so multicursor cascade puts the same (primary) text at each cursor. Note: `<c-r>-` happens to work because `insert_reg()` special-cases it. Solution: Update `insert_reg` so that it preps redo with `<c-r>x` instead of the literal text. Update `atom_key_class` so that CTRL-R flushes the insert-span.
* fix(multicursor): mapped jump merges cursors in follow-mode #42063Justin M. Keyes2026-09-24
| | | | | | | | | | | | Problem: Jumps (intentionally) don't cascade, thus they don't queue atoms. But in follow-mode, a mapping that moves the cursor but queues no atoms, LHS-cascades (this is a general fallback for the case where `keys` cannot be resolved). Thus `nnoremap <Down> ]C` replays `]C` at each cursor, which causes overlapping cursors, which are then merged/deduped. Solution: If a composite contains a "jump", set `composite.queued` to disable LHS-replay.
* 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.
* 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): undo/redo mapping in follow-mode #42051Justin M. Keyes2026-09-23
| | | | | | | | | | Problem: In follow-mode, a mapping that moves the cursor but doesn't queue an atom, cascades LHS. Undo moves the cursor and (intentionally) does not queue an atom, so `nnoremap u u` replays undo per-cursor. Solution: - Set `composite.queued`, to skip LHS-cascade. - Also treat "U" (line-undo) as a "global op".
* 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): 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(multicursor): empty insert-session adds a no-op undo-state #41979Justin M. Keyes2026-09-18
| | | | | | | | | Problem: An insert-session (`mc_ins_cascade_start`) saves a preliminary "covering" undo-entry, to cover all cursors. If the insert-session exits without changes, that no-op entry remains, requiring an extra "u". Solution: Drop the entry at insert-session end if there were no changes.
* 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.
* fix(multicursor): mapping that creates initial cursors is not replayed #41788Justin M. Keyes2026-09-08
| | | | | | | | | Problem: A Visual-mode mapping that creates cursors (`xmap I Q0i`) replays its commands only if cursors already existed when it started. Solution: Always collect `composite`. Consumers are still decided per frame (`CmdFrame.consumers`) and at emit/cascade.
* fix(cmdatom): don't capture inputsecret() #41742Justin M. Keyes2026-09-07
|
* fix(multicursor): Visual-mode mapping loses its commands #41741Justin M. Keyes2026-09-06
| | | | | | | | | | | | | | | | | Problem: A Visual-mode mapping that leaves Visual mode ("xmap I Q0i") does not replay its motions/edits at the extra cursors, though an insert session it starts does. atom_map_start() skips while Visual is active, so the mapping has no `composite`, and atom_capturable() then rejects its commands. Solution: Open the `composite` for Visual-mode mappings too. Bonus: the mapping atom is now reported as: before: type=visual, keys="viwcFOO<Esc>" after: type=mapping, keys="viwc<Esc>iFOO<Esc>", lhs=",c"
* 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): follow cursor-move by API #41675Justin M. Keyes2026-09-04
| | | | | | | | Problem: A non-edit mapping that moves the cursor by API (Lua, `:call nvim_win_set_cursor()`, …) is not cascaded. Solution: Fallback to LHS-replay if the cursor moved and follow-mode is enabled.
* 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.
* fix(multicursor): live-mirror Visual-mode operator mapping #41655Justin M. Keyes2026-09-03
| | | | | | | | | | Problem: Insert entered by a Visual-mode operator mapping (`xnoremap c c`) does not live-mirror at cursors. Solution: Decide `typed` from the Visual session's own provenance (`vatom`, which spans the selection and knows whether it was user input) instead of only negating it.
* fix(multicursor): 'operatorfunc' motion not followed #41627Justin M. Keyes2026-09-02
| | | | | | | | | Problem: With follow-mode ("q="), an 'operatorfunc' that only moves the cursor does not cascade. Solution: Update the `effect` condition to include cursor-moves if follow-mode is active.
* fix(multicursor): atom may cascade in another buffer #41624Justin M. Keyes2026-09-02
| | | | | | | | | | | Problem: An atom queued in buffer A cascades on B's cursors if the mapping ends in B ("nnoremap X x:bnext<CR>"). Solution: Check the atom's origin buffer (`CmdAtom.origin.buf`). Note: This does not preclude mappings etc from doing work in temporary throwaway buffers, as long as they return to the origin buffer.
* 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(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.
* feat(editor): undo restores cursor position #41520Justin M. Keyes2026-08-27
| | | | | | | | | | | | | |