Commit Graph

255 Commits

Author SHA1 Message Date
Justin M. Keyes
6cbde6a95f fix(multicursor): Visual-mode mapping loses its commands #41741
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"
2026-09-06 18:50:48 +00:00
Justin M. Keyes
2ac9dfc45d fix(multicursor): live-preview ":norm" visual selection #41736
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.
2026-09-06 17:15:48 +00:00
Justin M. Keyes
bd43394d4e fix(multicursor): skip insert live-mirror on conflicting edits #41730
Problem:
Cursors placed same-line before the primary, or producing multiline
edits on lines above the primary, shift the text in a way that breaks
the insert-mode live-mirroring.

Solution:
Skip live-mirroring under those conditions.

TODO (future): we could support live-mirroring if ins-completion's
internal bookkeeping tracked its regions more precisely.
2026-09-06 10:04:47 -04:00
Justin M. Keyes
455b6a810c fix(multicursor): cursors may be "deduped" incorrectly #41721
Problem:
Freeing an unrelated buffer deletes the cursor under the primary.
Scratch-buffer cleanup triggers this unexpectedly.

Solution:
Separate dead-cursor cleanup from dedupe/merge. Merge overlapping
cursors only at cascade boundaries, not during buffer/namespace cleanup
or cursor removal.
2026-09-05 13:18:22 -04:00
Justin M. Keyes
c543e9e1de fix(cmdatom): exclude <expr> EVALUATION input from CmdAtom.keys #41718
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`.
2026-09-05 11:52:25 -04:00
Justin M. Keyes
0c642196b9 fix(multicursor): cascade mapping atoms with insert entry #41714
Problem:
Pending mapping atoms prevent live insert-cascading (`nnoremap i ^i`).
Flushing them at insert-session start (3a02a39957) adds a separate
cascade boundary, which muddles the architecture.

Solution:
Let the first insert-span cascade pending mapping atoms on insert-entry.
This is an alternative fix for #41692.

Composite children (`.atoms`) BEFORE:

    { { 'motion', '^' }, { 'insert', '1iX<Esc>' } }

Composite children (`.atoms`) AFTER:

    { { 'motion', '^' }, { 'insert', '1i<Esc>' }, { 'insert', 'iX<Esc>' } }
2026-09-05 09:53:41 -04:00
li
739a44753d fix(cwd): :bcd completion shows non-directories #41661
Problem:
Completion for `:bcd` shows files, not just directories.

Solution:
Set the ctx properly.
2026-09-05 07:56:27 -04:00
Justin M. Keyes
3a02a39957 fix(multicursor): live-mirror mapped Insert entry #41692
Problem:
A Normal-mode mapping that moves before entering Insert, does not
live-mirror. Its pending motion defers insert-cascade until Insert ends.

Solution:
Cascade pending atoms before the Insert entry replay, then start the
live insert span.
2026-09-04 20:03:19 -04:00
Samiul Islam
b7a0c71a54 fix(multicursor): prevent clipboard crash after cursor jump #41698
Problem:
Changing from an empty-line cursor after a multicursor jump can crash
while flushing a deferred clipboard update. Exact context restore can
leave an omitted register with a null array and stale non-zero size.

Solution:
Make free_register() fully reset the register after freeing its contents,
so it always leaves a valid empty register. Add a regression test.

Signed-off-by: sami <samiulsami7786@gmail.com>
2026-09-04 13:06:50 -04:00
Justin M. Keyes
2124f968a6 feat(multicursor): "]C"/"[C" leave a cursor at old position #41681
Problem:
`]C` jumps the primary onto another cursor, which dedupes at the next
edit. Every `]C` consumes a cursor.

Solution:
`]C` adds a cursor at the current position before jumping. This
effectively "rotates" the primary cursor.
2026-09-04 06:28:42 -04:00
Justin M. Keyes
18f5d47543 fix(multicursor): follow cursor-move by API #41675
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.
2026-09-04 07:37:48 +00:00
Justin M. Keyes
5209695703 fix(multicursor): abandoned Visual selection does not move cursors #41670
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.
2026-09-03 15:20:30 -04:00
Justin M. Keyes
da4355ab8f fix(multicursor): 'guicursor' uses Normal shape in Visual mode #41669
Problem:
'guicursor' is not respected when extra cursors exist.

Per-cursor selection toggles Visual off, and the sandbox restores it
only after restore_current_state(), whose ui_cursor_shape() caches the
Normal shape.

Solution:
Restore Visual before restore_current_state().
2026-09-03 13:22:50 -04:00
Justin M. Keyes
e1a28e52b2 fix(editor): shada "force" load clears v:oldfiles #41662
Problem:
`shada_read()` rebuilds `v:oldfiles` for any "forced" read, which first
*clears* the list. For an in-memory caller such as multicursor, the list
is never repopulated. So `v:oldfiles` is empty after `Q` + edit.

Solution:
Rebuild `v:oldfiles` only when `kShaDaGetOldfiles` is requested.
`shada_read_everything()` (`:rshada[!]`, startup) always requests it.

old bug from 411a06c8b6
2026-09-03 11:32:20 -04:00
Justin M. Keyes
3beb15da57 fix(multicursor): live-mirror 'autocomplete' <BS> #41660
Problem:
During 'autocomplete', a completion-ending `<BS>` restarts completion
immediately. Multicursor insert-cascade skips replay (`edit()` refuses
to nest if compl/pum is active), so the mcursors do not update until
ESC.

Solution:
Flush pending keys in `TRIGGER_AUTOCOMPLETE`, just before autocomplete
restarts again.
2026-09-03 10:28:17 -04:00
Justin M. Keyes
e1cde28ba2 fix(multicursor): live-mirror Visual-mode operator mapping #41655
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.
2026-09-03 08:44:22 -04:00
Justin M. Keyes
f1d89e874a fix(multicursor): 'operatorfunc' motion not followed #41627
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.
2026-09-02 12:00:54 -04:00
Justin M. Keyes
c75aae2bfb fix(multicursor): 'autocomplete' during insert-cascade raises E565 #41625
Problem:
With mcursors and 'autocomplete', a non-literal key (`<BS>`) during an
active completion attempts to insert-cascade, which attempts a nested
`edit()`, which refuses with E565.

Solution:
Defer the flush while completion is active; the pending keys will be
handled later.
2026-09-02 11:20:55 -04:00
Justin M. Keyes
80e9708afa fix(multicursor): atom may cascade in another buffer #41624
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.
2026-09-02 10:38:27 -04:00
Justin M. Keyes
350fa5ad7c fix(multicursor): act on fold contents, not the fold itself #41622
Problem:
If an edit cascades to cursor(s) that happen to be within a closed fold,
the edit acts on the fold itself (`:h fold-behavior`). E.g. "dd" deletes
the entire fold, not the cursor's line within the fold.

Solution:
Check `mc_replaying()` in `hasAnyFolding()`, so folds are ignored during
a multicursor cascade. This means multicursor always replays relative to
text *within* the fold.
2026-09-02 09:42:40 -04:00
Justin M. Keyes
7168e0d12f fix(mcursor): drop q= "follow motion" message 2026-09-02 13:09:30 +02:00
Justin M. Keyes
9a29622b54 feat(multicursor): MC HAMMER #41587
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().
2026-09-01 15:17:22 +00:00
Aryan Pandey
ad42ee1c41 fix(cmdwin): handle UTF-8 characters containing 0x80 #41566
Problem:
Confirming cmdwin with a UTF-8 character containing 0x80 does not complete the
command.

Solution:
Escape K_SPECIAL bytes while feeding the cmdwin input after confirmation.
2026-08-31 06:52:20 -04:00
Justin M. Keyes
b542f3a8f8 fix(cmdatom): edit-repeat mapping #41564
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()`.
2026-08-30 23:38:34 +00:00
Justin M. Keyes
25f7c87a70 fix(mappings): replaying a deleted Lua mapping is UB
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: 5ac2e47acc
2026-08-31 00:16:01 +02:00
Justin M. Keyes
bc16be3cd9 fix(cmdatom): operator with Lua textobject is not type=operator
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...
2026-08-31 00:16:01 +02:00
Justin M. Keyes
2514256d95 refactor(input): exec stuffed keys eagerly
Problem:
The Vim "stuff" concept breaks the ability to reason about the call
stack and thus the boundaries of a `CmdAtom`: a stuffed translation ("x"
=> "dl") defers to the main loop. This "continuation" must be modeled in
`CmdAtom`, by checking global flags at undefined times, during undefined
circumstances.

Solution:
- After a stuffed "translation", eagerly execute the stuff buffer
  (`exec_stuffed()`).
- Delete the CmdAtom "continuation" junk.

Note:
- op_colon runs its cmdline "nested", but that's fine because operators
  already nest interactive sessions there (op_change runs edit()), and
  the cmdline is frameless so the operator's frame can own/capture it.
2026-08-29 01:06:26 +02:00
Rob Pilling
61958f2335 fix(cmdwin): allow a user to switch to other buffers #41199 2026-08-28 13:34:31 -04:00
Rob Pilling
f5d4b5975d fix(tabpage): focusing a tab while closing it, fails assert #41475
Problem:
If an autocommand handler focuses the tab page we're closing during a
`:tabonly` (with some conditions), we hit an assert failure in
`win_close_othertab()`.

For this to occur, we need:
- `nvim_buf_delete()` to trigger `close_windows()` (as is done in
	`cmdwin.lua`'s `_cleanup()`)
- `close_windows()` then calls `win_close_othertab()`, removing the
	window (`win_free_mem()`)
- then in the caller, `tabpage_close_other()`, the loop continues (we
	don't detect `tp_lastwin == wp` since we've unlinked the window)
- the loop assumes that `curtab != tp`

but we've refocused `curtab` so the `ex_win_close()` call passes `tp` as
`curtab`, causing the assert to fail

Solution:
Detect the focus of `curtab` and abort closing the tab.
2026-08-28 08:39:16 -04:00
Justin M. Keyes
7e2e3f8c25 feat(editor): undo restores cursor position #41520
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).
2026-08-27 16:12:11 -04:00
Justin M. Keyes
61df463c5e fix(cmdatom): insert-session entered by feedkeys() ignores typed input #41518
Problem:
An insert-session entered by a scheduled `feedkeys('i','n')` is
classified on entry (as "not typed"), and not reevaluated after that,
thus user input following it is not captured.

Repro: an `:imap` that does `feedkeys('<esc>','n')` and schedules
re-entering insert, emits one CmdAtom for the first press, then nothing
else.

Solution:
Sample `maptick` (ticked by `gotchars()` on typed input, including
mappings) at session start; if it advanced by session end, the session
is user input.
2026-08-27 10:48:33 -04:00
glepnir
f0146bcbe7 vim-patch:9.2.1009: duplicate dict code in ins_compl_dict_alloc() (#41497)
Problem:  ins_compl_dict_alloc() builds the same dict as
          fill_complete_info_dict().
Solution: Call fill_complete_info_dict() instead (glepnir).

closes: vim/vim#21140

303a153694
2026-08-26 08:37:44 +00:00
Justin M. Keyes
69bf8c7792 fix(cmdatom): terminal-mode keys leak into mapping lhs #41455
Problem:
A mapping that enters terminal-mode (`:FzfLua files` via `:startinsert`)
never reaches a composite-end: terminal-mode runs no "normal" CmdFrames.
The composite collects the entire terminal session (and more) into `lhs`.

E.g. if I have `<M-/>` mapped to open `:FzfLua files`, then interact
with fzf-lua UI, the emitted CmdAtom looks like:

    <M-/>… => { type='mapping', lhs='<M-/><C-N><C-N><CR>', keys=nil }

Solution:
End the composite when terminal-mode is entered, which emits a more
meaningful and repeatable atom:

    <M-/>… => { type='mapping', lhs='<M-/>', keys=nil }
2026-08-23 15:04:11 -04:00
Justin M. Keyes
bb82f9612c fix(cmdatom): "!" operator hardcodes its range #41451
Problem:
The "!" operator stuffs its cmdline continuation (`:.,.+1!`), so its
frame ends before capture (stuff pending) and the redo-prep disappears
with it.

    !ipsort<CR> => { type='excmd', lhs=':.,.+1!sort<NL>', keys=':.,.+1!sort<NL>' }

Compare to builtin "." which works bc `do_bang()` completes the redo
(`!ip` + `sort<NL>`).

Solution:
Appoint the stuffed continuation frame as the "redo-prep" frame.

    !ipsort<CR> => { type='operator', operator='!', lhs='!ipsort<NL>', keys='!ipsort<NL>' }

Notes:
- atom_cmd_end(): a frame ending with stuff pending re-points its
  redo-prep to the next frame.
- atom_cmd_start(): a stuffed continuation frame (`KeyStuffed`) keeps
  the redo-prep; flushed stuff discards it.
2026-08-23 13:58:45 -04:00
Justin M. Keyes
47cd769ed5 feat(cmdatom): mappings capture continuation
Problem:
A mapping that ends mid-operation (`nnoremap ,D d`) emits a content-free
"mapping" atom plus a `pending` field, and the "continuation" motion
arrives as a sibling atom. Consumers must stitch the two together (which
has broken cases, e.g. Insert-opening mappings (",i") lose their session
entirely).

- ",i": the session atom is dropped bc the mapping RHS is consumed
  before the session starts (typebuf_maplen()==0), so
  atom_is_user_input()=false.
- ":normal"-in-opfunc: the opfunc internal "v..y" session (a) became
  kVatomTyped just because the deferred composite was open, masking the
  real operator capture via atom_captures, and (b) its nested frames
  re-derived the outer redo.
- "Motion" based on `moved=true`, has false negatives.
- `CmdAtom.remap` is unnecessary, and clutters the docs/usage.

Solution:
- Introduce `frame_id` to identify CmdFrames.
- Classify `type=motion` better, via `NV_MOTION` flag on the `nv_cmds` table.
- Drop `CmdAtom.pending`, `CmdAtom.remap`.
- Defer atom_composite_end() at the clock edge while an operator is
  pending, Visual is active, or `restart_edit` is set: the composite
  keeps collecting, so the continuation is captured in the mapping atom.
- ",i": Now an open composite counts as user input.
- ":normal"-in-opfunc: Now handled correctly.
- `remap` is now decided by `composite.payload || 0 subatoms`.
  atom_payload_mark() records the read the resolution never captures.
- `toplevel` is now decided by `CmdFrame.parent == NULL`.

before/after:

    INPUT       BEFORE                             AFTER
    ---------------------------------------------------------------------
    ,D w        {mapping lhs=,D pending=operator}  {operator lhs=,Dw keys=dw}
                + {operator keys=dw}
    ysiw"       {mapping lhs=ys pending=operator}  {operator lhs=ysiw" keys=g@iw"}
                + {operator lhs=g@iw"}
    ,v d        {mapping pending=visual}           {visual lhs=,vd keys=viwd}
                + {visual lhs=viwd}
    ,i XY<Esc>  {normal keys=i lhs=,iXY<Esc>}      {insert keys=1iXY<Esc> text=XY}
2026-08-22 17:29:34 +02:00
Justin M. Keyes
6423657352 feat(cmdatom)!: eliminate the need for vim-repeat #41414
Problem:
- `lhs` is not fully realized. E.g. for a "payload" mapping
  `lhs` omits the `getchar()` payload during a mapping (vim-surround
  `ds'` reports `lhs="ds"`). This means plugins like vim-repeat are
  still needed...
- The "delta" fields of a CmdAtom are calculated too late.
  - `<abuf>` and `changed` check whatever (wrong) buffer a command
    (":bnext") might land in.
  - CTRL-W_w between two windows on the same buffer reports type="motion".

Solution:
- `CmdOrigin` samples (buf/win/cursor/changedtick) at each "scope" entry
  (CmdFrame, composite, Visual session, insert session).
- `dd<C-w>l` reports `changed=true` for the buffer it edited, regardless
  of where the cursor ends up.
- New fields:
  - `pos`: cursor position at command start.
  - `moved`: indicates whether the cursor moved (in same buffer).
  - `undoseq`: undo state at settlement.
- lhs now includes the payload: "ds)" reports lhs="ds)" instead of "ds".
  - Easy for users to "replay" any atom.
- Rename: type "command" => "normal", "ex" => "excmd"; `arg` => `cmdarg`
- Drop `cascade` field (no reason to expose it)
2026-08-21 13:42:17 -04:00
Justin M. Keyes
31de0d69fc fix(cmdatom): lhs not always reported in CmdAtom #41386
Problem:
An unreplayable Visual operation does not emit a CmdAtom event. That's
maybe not super important, but it hints at a flaw in how `vatom`
"voiding" is plumbed: `vatom.state=kVatomVoid` replaces the "kind", so
that info is lost to later parts in the lifecycle.

Solution:
- Define `VatomState` as "flags", so `kVatomVoid` can "poison"
  `vatom.state` without losing its kind flag.
- Unify `lhs`: always report the original user input in `CmdAtom.lhs`,
  for all kinds of user actions: visual, "translated"/"stuffed" cmds,
  and dot-repeat (".") itself.
- Unreplayable Visual atom emits CmdAtom with non-empty `lhs` and empty
  `keys`, like a mapping/macro composite.
2026-08-20 05:18:36 -04:00
Justin M. Keyes
f6bf814378 fixfix(cmdatom): repeat Visual <Cmd>; CmdFrame + stage #41355
Problem:
- Dot-repeat of a Visual selection prepared by a `<Cmd>` mapping,
  results in E1255 and leaves Visual mode active.
- Visual-mode capture is implemented as a second, parallel "capture
  engine": it re-composes a CmdSpec from cmdarg_T per key and decides
  replayability from its own table of "void" key classes.

Solution:
- Model nested `normal_execute()` as a `CmdFrame` stack,
  instead of a single module-scoped `stage`.
  - Capture `<Cmd>` ":norm …" commands as subatoms from its nested frames.
- Produce every command exactly once; atom_push_raw() routes the atom to
  the Visual composite while a selection is open, like it already does
  for mapping composites.
  - `v/pat<CR>d` is now repeatable and cascades.
2026-08-18 07:50:27 -04:00
Justin M. Keyes
581ce0b3da fix(cmdatom): <Cmd> mappings #41347
Problem:
`<cmd>` mappings do not emit `CmdAtom.text`.
`<cmd>` and Lua-callback mappings that edit the buffer apply only at the
primary cursor, not cascaded (multicursor).

Solution:
Capture the `<cmd>` command in getcmdkeycmd().
Add kKeyOpaque ("no capturable keys"); narrow kKeySynthetic ("not
a keystroke") to K_EVENT/K_IGNORE, so an opaque mapping's edit still
sets `map_edit` and cascades via LHS-replay.
2026-08-16 18:00:55 -04:00
Justin M. Keyes
214bcf24cc fix(undo): crash on corrupted undo file #41339
Problem:
`:rundo` on a corrupted undo file crashes or hangs, instead of failing
with E825. Patching one 4-byte field is enough:

    ue_size = 0xFFFFFFFF  " walks a NULL ue_array
    ue_size = 0x7FFFFFF0  " 17 GB xmalloc + memset, then preserve_exit()
    ue_top  = 0xFFFFFFFB  " negative lnum reaches ml_delete()

Analysis:
Every count in the file is read with `undo_read_4c()` and then checked,
differently at each site. None bounds the value by what the file can
hold, so a 2 GB count reaches `xmalloc()`.

Note:
- Vim doesn't have `bi_fsize` because it checks `U_ALLOC_LINE` result
  everywhere (thus doesn't crash, but may thrash...); those checks were
  dropped when Nvim moved to `xmalloc()`, and the `ue_size` loop counter
  became unsigned.
- Vim *does* have the negative line numbers bug: `u_undoredo()` checks
  `top > ml_line_count || top >= bot || bot > ml_line_count + 1`, which
  rejects none of them.

Solution:
- Introduce `undo_read_len()` and use it to fail early instead of
  continuing with nonsense.
- Validate `ue_top`/`ue_bot`/ `ue_lcount`.
- Use `xcalloc()`, so no site can proceed with a NULL array.
- Report a truncated "U" line, distinguish EOF from a 0xFFFFFFFF field,
  and free the header on the extmark error path.
2026-08-16 10:24:24 -04:00
Justin M. Keyes
37c670e682 fix(marks): undo reverts a mark set after the change #41330
Problem:
A named mark updated after a change is moved back (treated as the
original mark) by undo:

    :1mark d
    :$
    dw
    :2mark d   " 'd is on line 2
    :undo      " 'd is back on line 1

The undo header snapshots `b_namedm` when the change is recorded, and
`u_undoredo()` restores that snapshot indiscriminately.

Solution:
Update the pending header's snapshot when a mark is set explicitly.
Marks that the change itself moved go through mark_adjust(), not
setmark_pos(), so those are still reverted.

Similar to 2546741d1b (for extmarks): an explicit set inside an undo
block is confused with an edit-driven adjustment. But the extmarks case
is dealing with mid-edit moves, whereas named/regular marks only need
the stale snapshot dropped.
2026-08-15 13:34:51 -04:00
Justin M. Keyes
64a301184e feat(input)!: CmdAtom event #41297
Problem:
There is no unified notion of a "user action".

Vim processes input by one-char-at-a-time, and mostly throws away any
hints it might gather about the user's action, with one exception: it
stores the last _edit_ action (the "redo buffer", encoded as
unstructured `["x][v][count]body` bytes).

Plugins can only observe individual keys (vim.on_key) and high-level
effects (TextChanged, CursorMoved).

Solution:
- Users can subscribe to `CmdAtom` events to handle any user action.
  - Event is deferred; handlers cannot cancel or interfere with user
    actions.
- Capture `CmdSpec` from the normal/insert/visual subsystems.
  - typeahead/readahead stay unstructured (`buffheader_T`): they are key
    streams, not commands.
  - the redo/record buffers become `StringBuilder`: fewer
    allocations/copies.
- Repurpose the input/redo engine to accept `CmdSpec` objects.

"atom": one repeatable unit of user input, as a resolved (post-mapping)
keysequence plus structured fields. Only user actions, not `:normal`,
API calls, or non-"t" `feedkeys`.

BREAKING: dot-repeat of an Insert session, replays the entire session
including cursor-moves (:help ins-repeat).

BREAKING: dot-repeat of a Visual operation, replays the selection
instead of operating on a fixed-size region.
2026-08-14 09:30:31 -04:00
Sébastien Hoffmann
259e6fa9cc fix(ruler)!: consistent width in last line and statusline
Problem: Traditionally, the ruler in the last line is one cell shorter
than in the statusline, leaving the last cell of the screen blank.
According to code comments, this is in order to prevent unwanted
scrolling on "some" (unspecified, but presumably ancient) terminals.
Berkeley vi is more specific in its `vs_modeline` function: dumb
terminals with hardware scroll, SunOS 4.1.1 and Ultrix 4.2 curses.
(n)curses still has a similar limitation in `(w)addstr`, but apparently
only for historical reasons.

Maintaining the different widths leads to awkward inconsistencies when
the ruler is configured with 'rulerformat', except for the special case
where it contains a top-level `%=`. Shifting the ruler in the last line
to the left would be a solution, but the empty cell at the end doesn't
seem to be relevant anymore.

Solution: extend the ruler in the last line all the way to the right
edge of the screen, just like in the statusline. The exact same amount
of place will be available to the rest of the UI as before.

BREAKING CHANGE:
- the default ruler width is now 18 cells
- the last cell of the screen is no longer empty

Closes #41076
2026-08-04 06:36:58 +02:00
glepnir
916a6e9546 fix(eval): complete_info() equal, preselect, commit_chars #41022
Problem:  complete_info() does not report "equal", "preselect" or "commit_chars".

Solution: Report them for the items that set them.
2026-07-28 07:24:58 -04:00
glepnir
0b3b12da8d fix(completion): "preselect" ignores "noinsert", menu order #41007
Problem:  A preselected item is inserted even with "noinsert", and the
          first added preselected item wins over the first one shown
          in the menu.
Solution: Use K_DOWN when "noinsert" is set; pick the first preselected
          item that made it into the menu.
2026-07-27 08:49:12 -04:00
mityu
26fed075ae fix(cmdwin): q: in visual-mode does not insert visual range #40973
Problem: `q:` in visual mode does not insert `'<,'>` automatically in
the current line on cmdwin.

Solution: Insert `'<,'>` in the current line on cmdwin when `q:` is used
in visual mode.
2026-07-25 15:50:31 -04:00
Justin M. Keyes
6d8c8b18d5 test(harness): migrate away from magic globals
Problem:
The magic globals `it`, `describe`, etc., are more trouble than they are
worth.

- Hooking into `after_each` requires `getfenv()` hacks.
- They confuse luals/emmylua, because the top-level `.luarc.json` isn't
  merged with `test/.luarc.json` (apparently a luals limitation?)
- They totally defeat discoverability because the user just has to
  "know" about the various magic symbols.

So they harm DX, which means they serve no purpose at all.

Solution:
- Expose the test API from `testutil`, so tests can call `t.it()`,
  `t.describe()`, etc., in the conventional way.
- Drop `getfenv()` hacks.
- Drop the `setfenv()` injection in `load_chunk`.
- Drop `test/_meta.lua`.
2026-07-21 13:22:39 +02:00
Justin M. Keyes
6f5fae3f8c fix(lua): vim.keycode cleanup #40817 2026-07-18 14:31:36 -04:00
altermo
a2dfa195b2 feat(lua): vim.keycode() gets structured parse result #38636
Problems:
1. Can't get individual parts of a key-chord separately: modifiers, key.
2. Can't separate a key-combo into individual key-chords.

Solution:
Enhance `vim.keycode()` to optionally return a structured parse result
as a list of key-chords:
- `key_raw` the key-chord (problem 2)
- `mod` the modifiers of `key_raw` (problem 1)
- `key_orig` the key part of the key-chord, only here if differing from `key`
  (this doesn't solve any of the above mentioned problems, but it may provide
  useful and it's (in terms of code) free)
- `key` a normalized version of `key_orig` (solving problem 1), example(the
  first is `key_orig` and second is `key`): `lt` and `<`, `Bar` and `|` (in
  `<C-Bar>`)
2026-07-18 12:20:41 -04:00
Justin M. Keyes
99a16121d4 test: unreliable "put command . register special … ring the bell" #40812
Problem:

    FAILED   …/put_spec.lua:894: …/put_spec.lua @ 898: put command . register special tests should ring the bell when deleting if not appropriate
    …/put_spec.lua:894: …/put_spec.lua:898: Bell was not rung after action

    Snapshot:
    screen:expect([[
      ^ine of words 1                                       |
      Line of words 2                                      |
      {1:~                                                    }|*4
      {3:[No Name] [+]                                        }|
                                                           |
      {1:~                                                    }|*4
      {UNKNOWN_HL_ID(4):[No Name]                                            }|
      {UNKNOWN_HL_ID(1):                                                     }|
    ]])
    stack traceback:
    …/ui/screen.lua:917: in function '_wait'
    …/ui/screen.lua:540: in function 'expect'
    …/editor/put_spec.lua:894: in function 'bell_test'
    …/editor/put_spec.lua:940: in function <…/editor/put_spec.lua:932>

Analysis:
The bell was asserted via the UI 'bell' event (screen.bell), but
vim_beep() rate-limits that to 3 per 500ms. These tests share one
long-lived session, so beeps from earlier tests could exhaust the window
and drop the asserted beep.

Solution:
Use assert_beeps()/assert_nobeep(), which check called_vim_beep (set
before the rate-limit gate).
2026-07-18 07:48:36 -04:00