aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/input.c
Commit message (Collapse)AuthorAge
* [extregs] implement dot redo functionality for extregs.Josh Rahm20 hours
|
* fix(multicursor): v:cascading during on_key, insert-preview #42319Justin M. Keyes7 days
| | | | | | | | | | | Problem: - `v:cascading` is false during the Insert-mode live-preview. - `vim.on_key()` sees the internal keys of the Visual-selection preview and "zq" steps. Solution: - Set `v:cascading` during the Insert-mode preview. - Don't send internal keys to `on_key` callbacks. - Use `v:cascading` as the source of truth for `mc_replaying()`.
* 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(core): count silenced beeps in vim_beep()Justin M. Keyes2026-09-30
| | | | | | | | | | | | | | Problem: `did_beep` counts silenced beeps from `vim_beep()` callers (Normal-mode `<Esc>`, Insert-mode `<BS>`), but not from `beep_flush()`. So a `:silent!` failure cannot (internally) be consistently detected. This matters for the multicursor cascade and `zq`. `zq` runs its steps silently, so it needs to count silenced beeps to detect a failed step (don't retry) from one that didn't move (retry from next char). Solution: Count all beeps (silent + non-silent) in `vim_beep()`.
* fix(multicursor): zq failed motions, "t", first stepJustin M. Keyes2026-09-30
| | | | | | | | | | | | | | | | Problem: - "Vjzqfx" (on a line without "x") places a cursor at the selection start. - 'nowrapscan': `zqN` places a cursor on a non-match; `zq#` mid-word places it mid-word. - `zqfa` places at primary even if not on an "a". - `zqti` stops after one step (bc "ti" before an "i" does not move). Solution: - `did_beep_silent`: counts beeps silenced by `emsg_silent`, so `zq` steps can detect failed motions. - If a `zq` step does not move the cursor, retry it from the next char. - Decide the first `zq` step the same way as `{Visual}zq` already does: the primary is considered a step only if the motion lands on it.
* 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
* perf(mbyte): single decoder loop for both cluster byte length and cell widthbfredl2026-09-25
| | | | | | | | | | | | | | | This applies to the callsites which already used utf_ptr2CharInfo and then utfc_next. There is more work to be done, but this is a lagom bisectable unit. A lot of code is still wounded by the historical distinction between "low" characters (c < 128) and "high" characters, with the UTF-8 specific rules behind compiler flags and virtual function calls. As a result, there are both ptr2cells() and utf_ptr2cells(). My work aims at slowly reintegrating the realms. As such, we consider ptr2cells() the "correct" abstraction as is matches what actually is printed in a displayed message and buffer text without extra bells and whistles, except for tabs.
* feat(multicursor): cgn, failed insert-entry #42054Justin M. Keyes2026-09-23
| | | | | | | | | | | | | | | | | | Problem: - A failed motion in a replay (`vt;` where there is no `;`) flushes the rest of the replay, so `vt;cX` inserts `X` without changing the selection. - If the Insert-entering cmd fails at a cursor (`ct;`, `cgn` with no match), the text is still inserted at the failed cursor. Solution: - `beep_flush()`: Don't flush typeahead (i.e. keys following the failed cmd) during mc-replay. - Aligns with interactively-typed behavior. - Remove a cursor if its Insert-entering command aborts (`beep && !changedtick`). - This is particular to Insert-entering commands (`c{motion}`), because they are tricky. We don't usually remove cursors if a replayed cmd happens to fail at some cursors.
* 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.
* 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(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
| | | | | | | | | | | | | | | | | | 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(ui2): ui2 misinterprets getchar() input as pager_char (CR) #41465Justin M. Keyes2026-08-24
| | | | | | | | | | Problem: `vim.on_key()` callbacks are invoked for keys that `getchar()` consumes, which never reach the main loop, and there is no way to tell such a key apart. With ui2 the `on_key` handler activated after interactive cmdline, handles it as `pager_char` (thus enters the ui2 pager). Solution: Skip `vim.on_key()` callbacks during `getchar()`/`getcharstr()`.
* 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)
* vim-patch: num -> bool patches #41375Justin M. Keyes2026-08-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Notes: - 8.2.1614 also flips searchcount()'s "recompute" default to FALSE; - 8.2.1620 flips it back, so the default stays true here. vim-patch:8.2.1601: Vim9: cannot use 'true" with garbagecollect() https://github.com/vim/vim/commit/2df47310422f4a77e85de7901a5299923a1addd3 vim-patch:8.2.1602: Vim9: cannot use 'true" with getbufinfo() https://github.com/vim/vim/commit/036c2cf719e3de445779a275514030be66e26883 vim-patch:8.2.1603: Vim9: cannot use "true" with getchar() https://github.com/vim/vim/commit/c08cc72947fdfab484f53c5d9bbea662bc5a9c8f vim-patch:8.2.1604: Vim9: cannot use "true" with getcompletion() https://github.com/vim/vim/commit/d217a87755ba01fc626b1c178c682fcd05ab3a28 vim-patch:8.2.1614: Vim9: cannot pass "true" to searchcount() https://github.com/vim/vim/commit/597aaac9d2b090e0eb738ab4e2afd454b7b92393 vim-patch:8.2.1615: Vim9: cannot pass "true" to searchdecl() https://github.com/vim/vim/commit/30788d3d37a90b0702d94b7272ed26672534ba6f vim-patch:8.2.1616: Vim9: cannot pass "true" to synID() https://github.com/vim/vim/commit/fcb6d7082d563acc33866b3d3a659c32ad19a9e1 vim-patch:8.2.1617: Vim9: cannot pass "true" to win_splitmove() https://github.com/vim/vim/commit/4b9bd692bdffba03fda04f9979e25431b53e416b vim-patch:8.2.1619: Vim9: cannot pass "true" to spellsuggest() https://github.com/vim/vim/commit/7c27f337bf278babc1ccdcb66ac975f115d660b7 vim-patch:8.2.1620: searchcount() test fails https://github.com/vim/vim/commit/4140c4f3fff1a441f2837f2f911ab0f0368e412a N/A patches: vim-patch:8.2.1606: has() takes no second argument in Nvim https://github.com/vim/vim/commit/04637e243d39f791cfc97c3ce96d99ef1bf047e1 vim-patch:8.2.1610: list2str()/str2list() take no "utf8" argument https://github.com/vim/vim/commit/a48f786787e8e070c1a611146fd1f0f7e141e60a vim-patch:8.2.1611: nr2char() ignores its "utf8" argument https://github.com/vim/vim/commit/ed6a430fae72c077ec106c9620379b69cfec3035
* perf(input): drop buffheader_T #41353Justin M. Keyes2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: `buffheader_T` is a linked list (Vim's favorite data structure) with complex bookkeeping, optimized for ancient hardware: 1. Memory topology: On old platforms (Amiga 512KB without MMU, MS-DOS 64KB segments), large contiguous allocations were hazardous (no VM). 2. Never-move appends: `add_buff()` fills spare space or links a new block. 3. OOM "recovery": a failed block allocation only loses one append. To avoid OOM, it prefers to allocate small, linked chunks, instead of resizing one continguous slice. Each stuff/drain cycle costs a malloc+free. Solution: Replace Vim's favorite data structure with Nvim's favorite data structure. Nvim doesn't have granular handling of OOM (`xmalloc`), and hardware has changed: caches favor contiguous memory, allocators handle fragmentation. And these buffers are ~kb scale, so OOM is irrelevant on any system that can run Nvim. - Use a flat `StringBuilder` + `read`/`insert` offsets. - Keeps capacity (does not shrink) until `free_buff`. - "Steady state" allocates nothing: 1 fewer malloc+free per dot-repeat.
* refactor(cmdatom): drop RedoBuf #41351Justin M. Keyes2026-08-17
| | | | | | | | | | | Problem: `RedoBuf` is mostly indirection. It has a mild benefit as an "ownership" signal but it counteracts the general goal of unifying how "redo state" is passed throughout the system, tends to sprout redundant interfaces, and reduces clarity. Solution: Add `CmdSpec.body` to hold the "prefixless" key sequence. Reuse `CmdSpec` to represent a "redo" buf.
* refactor(cmdatom): do "redo prep" in one place #41348Justin M. Keyes2026-08-17
| | | | | | | | | | | | | | Problem: Redo prep is scattered/duplicated. - `do_pending_operator()` has 3 prep blocks whose conditions must be in sync with `atom_capture_op()`. - insert.c, spell_suggest() hand-roll `redo_new()` + `redo_append_xx()` sequences. - prep_redo() has 2 roles, decided by `keys != NULL`. Solution: - `atom_capture_op()` is the "operator" entry point: capture, then prep. - Extract `prep_redo_visual()`, `atom_capturable()`.
* 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.
* refactor(cmdatom): atom_redo_keys #41345Justin M. Keyes2026-08-16
| | | | Some names/comments are misleading. Also add some asserts.
* fix(coverity): false positives in kvec usages #41341Justin M. Keyes2026-08-16
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Coverity can't follow kv_ensure_space()'s `kv_roundup32()` bit math, so every kv_concat_len() looks like an overrun; and it doesn't know `kv_push()` allocates when `size == capacity`. _____________________________________________________________________________________________ CID 653191: Memory - illegal accesses (OVERRUN) /src/nvim/input.c: 3536 in paste_store() 3530 3531 if (s > start) { 3532 if (need_redo) { 3533 kv_concat_len(redobuff.cur.keys, start, (size_t)(s - start)); 3534 } 3535 if (need_record) { >>> CID 653191: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "recordbuff.items" at offset corresponding to index variable "recordbuff.size" through dereference in call to "memcpy". 3536 kv_concat_len(recordbuff, start, (size_t)(s - start)); 3537 } 3538 } 3539 3540 if (s < str_end) { 3541 int c = (uint8_t)(*s++); _____________________________________________________________________________________________ CID 653190: Memory - illegal accesses (OVERRUN) /src/nvim/input.c: 730 in redo_append_spec() 724 return; 725 } 726 727 while (*s != NUL) { 728 if ((uint8_t)(*s) == K_SPECIAL && s[1] != NUL && s[2] != NUL) { 729 // Insert special key literally. >>> CID 653190: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "redobuff.cur.keys.items" at offset corresponding to index variable "redobuff.cur.keys.size" through dereference in call to "memcpy". 730 kv_concat_len(redobuff.cur.keys, s, 3); 731 s += 3; 732 } else { 733 sb_add_char(&redobuff.cur.keys, mb_cptr2char_adv(&s)); 734 } 735 } CID 653189: (OVERRUN) /src/nvim/input.c: 3533 in paste_store() /src/nvim/input.c: 3536 in paste_store() _____________________________________________________________________________________________ CID 653189: (OVERRUN) /src/nvim/input.c: 3533 in paste_store() 3527 && *s != NL && !(crlf && *s == CAR)) { 3528 s++; 3529 } 3530 3531 if (s > start) { 3532 if (need_redo) { >>> CID 653189: (OVERRUN) >>> Overrunning dynamic array "redobuff.cur.keys.items" at offset corresponding to index variable "redobuff.cur.keys.size" through dereference in call to "memcpy". 3533 kv_concat_len(redobuff.cur.keys, start, (size_t)(s - start)); 3534 } 3535 if (need_record) { 3536 kv_concat_len(recordbuff, start, (size_t)(s - start)); 3537 } 3538 } /src/nvim/input.c: 3536 in paste_store() 3530 3531 if (s > start) { 3532 if (need_redo) { 3533 kv_concat_len(redobuff.cur.keys, start, (size_t)(s - start)); 3534 } 3535 if (need_record) { >>> CID 653189: (OVERRUN) >>> Overrunning dynamic array "recordbuff.items" at offset corresponding to index variable "recordbuff.size" through dereference in call to "memcpy". 3536 kv_concat_len(recordbuff, start, (size_t)(s - start)); 3537 } 3538 } 3539 3540 if (s < str_end) { 3541 int c = (uint8_t)(*s++); _____________________________________________________________________________________________ CID 653188: Memory - illegal accesses (OVERRUN) /src/nvim/input_cmdatom.c: 216 in atoms_concat_keys() 210 211 /// Concatenates the keys of multiple atoms into one (allocated) string. 212 static String atoms_concat_keys(CmdAtomVec atoms) 213 { 214 StringBuilder keys = KV_INITIAL_VALUE; 215 for (size_t i = 0; i < kv_size(atoms); i++) { >>> CID 653188: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "keys.items" at offset corresponding to index variable "keys.size" through dereference in call to "memcpy". 216 kv_concat(keys, kv_A(atoms, i).keys); 217 } 218 size_t len = kv_size(keys); 219 kv_push(keys, NUL); 220 return (String){ .data = keys.items, .size = len }; 221 } CID 653187: Null pointer dereferences (FORWARD_NULL) _____________________________________________________________________________________________ CID 653186: Null pointer dereferences (FORWARD_NULL) /src/nvim/input_cmdatom.c: 173 in atom_compose_keys() 167 StringBuilder sb = KV_INITIAL_VALUE; 168 redo_prefix(&spec, &sb, false); 169 redo_chars(&spec, &sb, false); 170 if (sb.size == 0) { 171 return NULL; 172 } >>> CID 653186: Null pointer dereferences (FORWARD_NULL) >>> Dereferencing null pointer "((sb.size == sb.capacity) ? (sb.capacity = (sb.capacity ? sb.capacity << 1 : 8UL)) , (sb.items = xrealloc(sb.items, 1UL * sb.capacity)) , 0 : 0) , (sb.items + sb.size++)". 173 kv_push(sb, NUL); 174 return sb.items; 175 } 176 177 /// The pending change as a CmdAtom: the composed keysequence plus the structured fields. 178 /// Caller owns `keys`. _____________________________________________________________________________________________ CID 653185: Null pointer dereferences (FORWARD_NULL) /src/nvim/input.c: 296 in redo_compose() 290 StringBuilder buf = KV_INITIAL_VALUE; 291 redo_prefix(&r->spec, &buf, false); 292 kv_splice(buf, r->keys); 293 if (buf.size == 0) { 294 return (String)STRING_INIT; 295 } >>> CID 653185: Null pointer dereferences (FORWARD_NULL) >>> Dereferencing null pointer "((buf.size == buf.capacity) ? (buf.capacity = (buf.capacity ? buf.capacity << 1 : 8UL)) , (buf.items = xrealloc(buf.items, 1UL * buf.capacity)) , 0 : 0) , (buf.items + buf.size++)". 296 kv_push(buf, NUL); 297 return cbuf_as_string(buf.items, buf.size - 1); 298 } 299 301 String redo_keys(void) _____________________________________________________________________________________________ CID 653184: Memory - illegal accesses (OVERRUN) /src/nvim/input.c: 3533 in paste_store() 3527 && *s != NL && !(crlf && *s == CAR)) { 3528 s++; 3529 } 3530 3531 if (s > start) { 3532 if (need_redo) { >>> CID 653184: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "redobuff.cur.keys.items" at offset corresponding to index variable "redobuff.cur.keys.size" through dereference in call to "memcpy". 3533 kv_concat_len(redobuff.cur.keys, start, (size_t)(s - start)); 3534 } 3535 if (need_record) { 3536 kv_concat_len(recordbuff, start, (size_t)(s - start)); 3537 } 3538 } _____________________________________________________________________________________________ CID 653183: Memory - illegal accesses (OVERRUN) /src/nvim/input.c: 730 in redo_append_spec() 724 return; 725 } 726 727 while (*s != NUL) { 728 if ((uint8_t)(*s) == K_SPECIAL && s[1] != NUL && s[2] != NUL) { 729 // Insert special key literally. >>> CID 653183: Memory - illegal accesses (OVERRUN) >>> Overrunning dynamic array "redobuff.cur.keys.items" at offset corresponding to index variable "redobuff.cur.keys.size" through dereference in call to "memcpy". 730 kv_concat_len(redobuff.cur.keys, s, 3); 731 s += 3; 732 } else { 733 sb_add_char(&redobuff.cur.keys, mb_cptr2char_adv(&s)); 734 }
* 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.
* refactor(exmode): Ex-mode as cmdwin + Lua #40991Justin M. Keyes2026-07-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: POSIX-compatible Ex-mode requires special-cases all over the codebase to match various quirks that don't actually matter to users. - The main utility of *interactive* Ex-mode is its REPL behavior, and that can be achieved with `cmdwin`, which also gains extra UX benefits. - The main utility of *non-interactive* `nvim -es` is for shell scripting, where Ex-mode quirks are mostly unhelpful (e.g. the "Entering Ex mode" message). Solution: - Reimplement *interactive* Ex-mode as a "persistent, insert-mode cmdwin" in Lua. - "nvim -e/-E" is simply an alias to "gQ". - Reframe *non-interactive* Ex-mode (`nvim -es`) as "script mode". - Drop POSIX Ex-mode quirks. Improvements: - "nvim -V1 -es" output ends with a final newline! - "nvim -V1 -es" no longer shows the "Entering Ex mode" msg. (This was pointless noise, unwanted for scripting purposes.) - stdin is no longer typeahead. Scripts (":lua io.read()") can read stdin as data. - Empty line is a no-op: a stray blank line no longer moves the cursor (deviates from POSIX ex "+1"), no longer exits 1 at EOF (E501). Preserved behavior: - cursor starts at "$" - mode()=="cv" (for non-interactive) - multiline commands (:append/:function/heredoc pull continuation lines) - bare-range print - :print=>stdout - -V1=>stderr - CRLF input - continue-after-error and exit codes Dropped (regressed) POSIX behavior (non-interactive): - Event loop only ticks while/between commands, not while blocked waiting for a stdin line. - ":g/pat/visual...Q" - input()/getchar()/":s/x/y/c" no longer consume stdin lines as answers: Nvim stops at end-of-input, skipping the rest of the script, exit 0. Use ":lua io.read()" instead. - If users care about this they should use interactive Ex-mode (`gQ`). - ":@r" stops at end of the register instead of continuing to read cmdline input from stdin.