100 Commits
Author SHA1 Message Date
Justin M. Keyes b107154ba2 docs: misc, cwd, vimscript.txt #41356
Extract vimscript.txt from repeat.txt
2026-08-17 13:37:08 -04:00
Justin M. Keyes 98053bfecf perf(input): drop buffheader_T #41353
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.
2026-08-17 07:55:47 -04:00
Justin M. Keyes 5f07fc91c2 refactor(cmdatom): drop RedoBuf #41351
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.
2026-08-17 05:37:06 -04:00
Justin M. Keyes 6947c8501d Merge #41349 from janlazo/na-patch-8.2.0000
build(vim-patch): n/a patch for error messages, FEAT_ guards, vim9 interface/types, static funcs
2026-08-17 02:07:15 -04:00
Justin M. Keyes ea6abf15fe refactor(cmdatom): do "redo prep" in one place #41348
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()`.
2026-08-17 01:55:14 -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 0e436350a5 refactor(cmdatom): atom_redo_keys #41345
Some names/comments are misleading.
Also add some asserts.
2026-08-16 16:14:33 -04:00
Justin M. Keyes aaf57a053d fix(coverity): false positives in kvec usages #41341
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         }
2026-08-16 11:37:51 -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 83730db647 perf(marktree): binary search the node intersect array #41331
Problem:
Undo of a change spanning many paired marks is quadratic. A node's
"intersect" array holds every pair crossing that node, and both
intersect_node() and unintersect_node() walked it linearly. Undoing an
edit over 1M paired marks spends 68% of its time in unintersect_node()'s
scan alone.

Solution:
The array is sorted, so binary search it.

    marks    undo before    after
    200k          831ms     385ms
      1M        14006ms    3820ms

Redo is unaffected: it is dominated by marktree_move() actually
repositioning the marks.
2026-08-16 08:29:58 -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 fb180287d0 fix(cwd): nvim_win_set_buf of :bcd buf, changes caller CWD #41329
Problem:
Setting a :bcd buffer into another window, modifies the caller's CWD.

    local b = vim.api.nvim_create_buf(true, true)
    vim.api.nvim_buf_call(b, function() vim.cmd.bcd('..') end)
    vim.cmd('vsplit')
    vim.api.nvim_win_set_buf(vim.fn.win_getid(2), b)

    :echo haslocaldir(0) haslocaldir(-1,0) haslocaldir(-1,-1,0)
    0 0 0
    :echo getcwd() ==# getcwd(-1,-1)
    0

Analysis:
`ctx_dirs_save` only saves CWD if it predicts the switch can change it.
But win_set_buf() replaces the target window's buffer *after* the
switch, which cannot be "predicted" from `ctx_dirs_save`.

Solution:
Always snapshot whenever the switch enters another window.
Skipping `os_dirname` was a micro-optimization.
2026-08-15 13:15:37 -04:00
Justin M. Keyes cd2db7913a fix(cwd): nvim_win_set_buf changes global CWD #41328
Problem:
nvim_win_set_buf() on a non-current win, while the current win has
a win-local dir, changes the global CWD:

    :vsplit | lcd ..
    :call nvim_win_set_buf(other_win, buf)
    :wincmd l
    :verbose pwd
    [global] /parent        " expected: the initial cwd

Analysis:
`globaldir` is where to return when no local dir applies; NULL means the
process CWD is already there. Switching to a window with no local dir
makes update_cwd() chdir back to `globaldir` and clear it. kCtxKeepCwd
restores the process CWD but not that bookkeeping, so the restored
window-local dir is mistaken for the global one.

Solution:
Save/restore `globaldir` with the CWD.
2026-08-15 11:45:44 -04:00
Justin M. Keyes 9cca923ab4 fix(extmarks): redo of a mark created during an edit #41324
Problem:
A mark created by `nvim_buf_set_extmark()` while an undo block is open
never comes back on redo.

Analysis:
Undo deletes the text it covers and collapses the range; redo replays
the splices, which re-insert the text but cannot re-expand the mark.
`extmark_set()` records a position only for a mark it moves, not for one
it creates.

Solution:
Record the created position for redo; undo leaves the mark to the splice
replay, since it did not exist before the edit. Each side of a paired
mark gets its own entry. Redo also revives a mark that undo invalidated,
else its position returns but its highlight does not.
2026-08-15 10:13:32 -04:00
Justin M. Keyes a458bfb595 Merge #41316 from janlazo/na-patch-tcd
build(vim-patch): detect more n/a patches for popupwin and terminal
2026-08-15 07:22:16 -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
Justin M. Keyes a1ea2f35be Merge #41075 from echasnovski/pack-packspec-part1 2026-08-13 08:53:49 -04:00
Justin M. Keyes 5287a04be4 perf(mbyte): annotate pure functions #41251 2026-08-10 04:27:49 -04:00
Justin M. Keyes 2546741d1b fix(extmarks): undo-redo of a mark explicitly moved during an edit #41252
Problem:
A mark moved by nvim_buf_set_extmark() during an edit is misplaced by
undo and redo. Only splices ("edits") are recorded, and replaying them
reproduces the shifts they caused, never the explicit set: the mark ends
up wherever the text pushed it.

Solution:
When an open undo block moves an existing mark, record both positions.
Undo restores the pre-set position, redo re-applies the set.

Partially reverts 18334a4a0c ; ExtmarkSavePos.row/col were unused
because nothing recorded an explicit move, but now `extmark_set()` does.
2026-08-10 04:21:31 -04:00
Justin M. Keyes abc271d8f3 fix(logging): vimscript API calls #41253
Problem:
":call nvim_get_mode()" logs a nonsense channel-id:

    RPC: ch 9223372036854775808: invoke nvim_get_mode

Solution:
Check `is_internal_call`.
Also, skip this logging for RPC calls, because it's redundant with
`log_request`/`log_notify`.

    API: vim -> nvim_get_mode
2026-08-09 17:33:53 -04:00
Justin M. Keyes e16d577f16 Merge #41224 from justinmk/fixbuild 2026-08-08 03:59:15 -04:00
Justin M. Keyes 9e283f273b test(terminal): unreliable "spawns in CWD effective at time of invocation"
FAILED  …/terminal/ex_terminal_spec.lua @ 270: :terminal (fake shell) spawns in CWD effective at time of invocation
    Expected values to differ.
    Value:
    "~/work/neovim/neovim/build/Xtest_xdg_terminal"
    stack traceback:
   …/terminal/ex_terminal_spec.lua:276: in function <…/terminal/ex_terminal_spec.lua:270>
2026-08-08 02:23:55 +02:00
Justin M. Keyes eecc4b73ff fix(coverity): UNINIT, FORWARD_NULL
CID 652778:         Uninitialized variables  (UNINIT)
    /src/nvim/context.c: 353             in ctx_dirs_save()
    347         if (curbuf->b_sfname != NULL && curbuf->b_fname == curbuf->b_sfname) {
    348           cs->cs_save_sfname = xstrdup(curbuf->b_sfname);
    349         }
    350         do_autochdir();
    351         char autocwd[MAXPATHL];
    352         if (os_dirname(autocwd, MAXPATHL) == OK) {
    >>>     CID 652778:         Uninitialized variables  (UNINIT)
    >>>     Using uninitialized value "*cwd" when calling "strcmp".
    353           cs->cs_apply_acd = strcmp(cwd, autocwd) == 0;
    354         }
    355       }
    356     }
    357
    358     /// Restores the dir state saved by ctx_dirs_save(), undoing any chdir made while switched. The

    CID 652777:         Null pointer dereferences  (FORWARD_NULL)
    /src/nvim/context.c: 556             in ctx_switch()
    550         cs->cs_target_win = wp->handle;
    551         cs->cs_target_old_pos = wp->w_cursor;
    552       }
    553       // The CWD-state snapshot is only for a real window target; hidden-buffer target is handled by the
    554       // ctx_win machinery (ctx_win_prep).
    555       if (buf == NULL || wp != NULL) {
    >>>     CID 652777:         Null pointer dereferences  (FORWARD_NULL)
    >>>     Passing null pointer "wp" to "ctx_dirs_save", which dereferences it.
    556         ctx_dirs_save(cs, wp, tp == NULL ? curtab : tp, buf);
    557       }
    558
    559       // Save the current state.
    560       cs->cs_curwin = curwin->handle;
    561       cs->cs_prevwin = prevwin == NULL ? 0 : prevwin->handle;
2026-08-08 02:23:55 +02:00
Justin M. Keyes b53c00b425 fix(progress): ins-compl progress-msg during pum #41226
Problem:
The insert-mode completion progress-message is in "running" state while
the user is selecting an item. That is noisy and unwanted UX; it was
only intended for the "Scanning..." task.

Solution:
End the progress-msg just after `ins_compl_show_statusmsg`.
2026-08-07 22:49:44 +00:00
Justin M. Keyes 33a688f9fe fix(messages): dangling progress-messages #41222
Problem:
Some builtin features emit progress-messages which never "complete".
- On failure, `:write` does not complete the progress-msg it started.
- ins-completion never ends its "Scanning..." message.

Solution:
- `buf_write()` emits "failed" status on failure.
- `ins_compl_stop()` ends the completion one.
2026-08-07 16:10:02 -04:00
Justin M. Keyes d0e8ebbaf3 Merge #41223 from janlazo/vim-8.2.1550
vim-patch:8.2.{1550,1571}
2026-08-07 16:07:20 -04:00
Justin M. Keyes 0a2676e54a fix(messages): :read starts a "bufwrite" progress #41219
Problem:
filemess() treats an empty suffix as "a buffer write is starting", but
readfile() calls it that way too. So ":read" (and ":edit", …) opens a
`nvim.bufwrite "<file>"` progress that is never completed.

Users of e.g. ghostty will see a stuck "progress" spinner.

Solution:
Only `buf_write()` starts the progress, via `filemess_progress()`.
2026-08-07 13:21:17 -04:00
Justin M. Keyes 7e53d3ba4d fix(cwd): keep buffer-local dir when re-editing #41215
Problem:
Re-editing a buffer (`:edit!`, re-reading a dir.lua buffer, etc.) drops
its `:bcd` directory, so the CWD falls back to the global one. Whereas
other buffer-local state (`b:` vars, local options) survives a reload.

Solution:
Don't clear buf dir in `buf_freeall()`; `do_ecmd()` calls that when
reloading/re-editing. `free_buffer_stuff()` still clears them when
a buffer is freed or reused for another file.
2026-08-07 14:10:21 +00:00
Justin M. Keyes 72907fe0f7 Merge #41200 from janlazo/na-patch-821300
build(vim-patch): misc n/a hunks since patch v8.2.1300
2026-08-07 07:45:11 -04:00
Justin M. Keyes 2e0a5a596a fix(terminal): spawn in effective CWD #41211
Problem:
`:terminal` does not respect the invocation-time CWD.
This wasn't noticeable with `:lcd` because the window-local CWD gets
applied to the new terminal buffer. But it is noticeable with `:bcd`.

Solution:
Specify `cwd` in the job spec.
2026-08-07 07:44:19 -04:00
Justin M. Keyes a4a544032a feat(cwd)!: :lcd! (bang), rearrange :bcd/:lcd/… scope precedence #41194
Problem:
- buf-local CWD scope is lower priority than :lcd, which is weird.
  ```
  win > buf > tab > global
  ```
- No way to clear current CWD at a given scope.

Solution:
- Rerrange scope precedence to:
  ```
  buf > win > tab > global
  ```
- Introduce "bang" variants (`:bcd!`/`:lcd!`/`:tcd!`) which clears the
  local CWD for the given scope.
2026-08-07 04:41:37 -04:00
Justin M. Keyes 3a1cbaddc6 Merge #41176 :bcd fixes 2026-08-06 07:51:35 -04:00
Justin M. Keyes eb19a52b7c feat(vim._with): keepcwd 2026-08-06 13:17:07 +02:00
Justin M. Keyes 1c1dc0558f feat(cwd): support explicit chdir (:bcd/:tcd/…) in temp context
Problem:
- Explicit `:bcd` (etc.) persists from `nvim_buf_call()` but not from an
  autocmd handler targeting a hidden buf (`LspAttach`, `TermRequest`, …),
  which needs a `vim.schedule()` workaround.
- `vim._with()` is supposed to work as a "sandbox", discarding
  side-effects, but it leaks CWD changes: `:lcd` from a `win` context,
  any chdir from a visible-buffer context.

Solution:
- Explicit :cd/:tcd/:bcd during a temp context persists by default.
  - "Ambient" directory changes ('autochdir', existing win-local CWD,
    etc.) are still undone, as before.
- Add `kCtxKeepDirs`: snapshot/restore the target's full CWD state
  (w/b/tp-local, global, cwd). Used by `vim._with()` and `'inccommand'`,
  which must not leak state.
2026-08-06 13:17:07 +02:00
Justin M. Keyes 93696ca903 Merge #41184 from janlazo/na-patch-821000 2026-08-06 04:46:07 -04:00
Justin M. Keyes 7ce7a8d58c Merge #41113 from justinmk/doc2 2026-08-06 04:38:40 -04:00
Justin M. Keyes 9a93ac6533 refactor(vim.fs): slug() minor cleanup 2026-08-06 10:27:29 +02:00
Justin M. Keyes 7d2249c579 docs: misc, :bcd, slug() 2026-08-06 10:27:29 +02:00
Justin M. Keyes ab80ea92cc fix(ui2): pager handling #41179
- Avoid shared state. Pass `focus` to set_pos()/expand_msg() instead of
  a shared `pager_focus` flag: the flag is only cleared when set_pos()
  actually enters the pager, so ":messages" from inside the pager left
  it set.
- pager_shown(): the pager window is invalid after leaving it with "q".
- Reuse pager_shown() in expand_msg().
2026-08-05 15:07:19 -04:00
Justin M. Keyes 2f9ef98a33 test(chdir): cleanup #41161
Problem:
Some assertions are erroneously skipped for `not is_os('win')`.

Solution:
Update tests. Deduplicate logic.
2026-08-04 15:38:22 -04:00
Justin M. Keyes 90786766d2 Merge #33320 :bcd (buffer-local directory) 2026-08-04 15:10:30 -04:00
Justin M. Keyes 9cd4dd1c19 fix(:bcd): do not "inherit" buffer-local dir
Problem:
Buffer-local CWD (:bcd) is "sticky", similar to window-local CWD (:lcd).
But this contradicts one of its main benefits: per-buffer "project root"
for LSP, OSC7.

Other problems:
- A buffer created with :edit/:enew/:new silently inherits b_localdir
  (and b_prevdir) from the previous buffer.
- curbuf_reusable() refuses to recycle a scratch buffer that has
  `b_localdir`.
- After :new/:vnew/:tabnew the CWD sticks to previous buffer's
  `b_localdir` even though the new curbuf has none, so :new is not
  equivalent to ":split | enew", and getcwd() disagrees with
  haslocaldir().
- Requires "which buffer spawned this buffer" semantics that no other
  buffer-local state has.

Solution:
Drop sticky/inherit behavior of buffer-local CWD (:bcd).

- do_ecmd: always apply the new curbuf's dir (`fix_current_dir`), like
  `do_autochdir` already does. :tabnew from a :bcd buffer now reverts to
  global CWD (and fires DirChanged), same as :tabnew from a :lcd window.
- curbuf_reusable(): recycling a scratch buffer frees its b_localdir.

To get sticky/inherit behavior of CWD, use `:lcd`.
2026-08-04 20:48:44 +02:00
Justin M. Keyes 46ca236525 fix(cwd): validate getcwd(…, -1) 2026-08-04 20:48:44 +02:00
Justin M. Keyes db2e86fba4 refactor(editor): cleanup change-directory (:bcd) logic 2026-08-04 20:48:05 +02:00
Justin M. Keyes 838f130e6a fix(types): emmylua warning about params field 2026-08-04 20:09:48 +02:00
Justin M. Keyes 9c5afe6606 test: unreliable "nvim.zip … incorrect password" #41159
Problem:

    FAILED   …/plugin/zip_spec.lua @ 442: nvim.zip reports an incorrect archive password
    Expected values to be equal.
    Expected:
    true
    Actual:
    false
    stack traceback:
    …/plugin/zip_spec.lua:453: in function <…/plugin/zip_spec.lua:442>

Solution:
The message is scheduled, so poll for it.
2026-08-04 18:00:13 +00:00
Justin M. Keyes ceccca7780 fix(options): crash on ":let &l:autoread = v:true" #41158
Problem:
Assigning to the local value of a global-local boolean option
('autoread', 'autocomplete', 'fsync') aborts:

    Assertion failed: (curval.type == newval.type), function
    ex_let_option, file vars.c, line 1408.

    ex_let_one
    ex_let_vars
    ex_let
    execute_cmd0
    do_cmdline
    call_user_func
    ...
    eval_map_expr
    vgetorpeek
    vgetc
    state_enter
    main

A global-local option may have local value `kObjectTypeUnset`, but
`ex_let_option()` guards only `kObjectTypeNil`.

Solution:
When curval is Unset, resolve it to the inherited global value.
2026-08-04 13:28:26 -04:00
Justin M. Keyes 597555ebc0 fix(options): crash on ":let &t_Co = v:true" #41152
Problem:
Assigning a Boolean or special value (v:true/v:false/v:null/v:none) to a TTY
option aborts:

    Assertion failed: (curval.type == newval.type), function ex_let_option, file vars.c, line 1408.
    3   libsystem_c.dylib   __assert_rtn + 284
    4   nvim                ex_let_one + 3308
    5   nvim                ex_let_vars + 112
    6   nvim                ex_let + 2356
    7   nvim                execute_cmd0 + 252
    8   nvim                do_cmdline + 9076
    9   nvim                call_user_func + 3320
    10  nvim                call_func + 2076
    11  nvim                get_func_tv + 696
    12  nvim                eval_func + 380
    20  nvim                eval_to_string_eap + 276
    21  nvim                eval_map_expr + 444
    22  nvim                vgetorpeek + 3172
    23  nvim                vgetc + 764

Solution:
Apply the string-type check to TTY options too, so a Boolean/special value
gives "E928: String required" instead of aborting. Valid string/number
assignments to `t_*` pseudo-options still silently no-op.
2026-08-04 11:06:47 +00:00
Justin M. Keyes dab5fab948 Merge #41114 from epithet/default-ruler-as-expression 2026-08-04 06:51:34 -04:00
Justin M. Keyes 8589447159 feat(detach): opt-in to "server keeps running" #41133
Problem:
By default, `nvim` does not survive if its host terminal dies. This is
inconvenient if you want to use Nvim as a "session manager" (like tmux).

Solution:
Let users opt-in to the "survive" behavior via `:detach!` (bang "!").
This marks the current UI as "detachable", so the server will not
self-exit if the UI channel closes.
2026-08-04 06:20:16 -04:00
Justin M. Keyes c875b16714 Merge #41112 from erdivartanovich/dir-failed-open-recovery 2026-08-04 05:04:07 -04:00
Justin M. Keyes 434792787b Merge #41093 from barrettruth/feat/zip-finish 2026-08-04 04:46:33 -04:00
Justin M. Keyes 794886aae7 Merge #41088 from barrettruth/feat/zip-health-encryption
feat(zip): checkhealth and encrypted entries
2026-08-03 16:05:41 -04:00
Justin M. Keyes f7fbe0e0c8 Merge #41106 from altermo/selection-refactor-fix 2026-08-02 05:33:17 -04:00
Justin M. Keyes 825cb3790e Merge #41103 from janlazo/na-hunk-header 2026-08-02 05:23:38 -04:00
Justin M. Keyes fe10c5bc36 test(fs): "fs.find() follows symlinks" #41105
Problem:
Test fails if other "build*/" dirs exist with a "nvim" file, e.g.
`build-asan/bin/nvim`.

Solution:
Do the test in an isolated dir.
This also fixes the test for the zig build.
2026-08-02 05:09:58 -04:00
Justin M. Keyes 98d767cd53 docs: func/expr options, misc #41102 2026-08-01 17:48:32 -04:00
Justin M. Keyes d060d91ef8 fix(bufwrite): coverity "uninitialized member" #41104
CID 652079:           (UNINIT)
    /src/nvim/bufwrite.c: 1413             in buf_write()
    1407         // only makes sense at the start of the file.
    1408         if (buf->b_p_bomb && !write_bin && (!append || perm < 0)) {
    1409           write_info.bw_len = make_bom(buffer, fenc);
    1410           if (write_info.bw_len > 0) {
    1411             // don't convert
    1412             write_info.bw_flags = FIO_NOCONVERT | wb_flags;
    >>>     CID 652079:           (UNINIT)
    >>>     Using uninitialized value "write_info.bw_first" when calling "buf_write_bytes".
    1413             if (buf_write_bytes(&write_info) == FAIL) {
    1414               end = 0;
    1415             } else {
    1416               nchars += write_info.bw_len;
    1417             }
    1418           }
    /src/nvim/bufwrite.c: 1453             in buf_write()
    1447               *s = c;
    1448             }
    1449             s++;
    1450             if (++write_info.bw_len != bufsize) {
    1451               continue;
    1452             }
    >>>     CID 652079:           (UNINIT)
    >>>     Using uninitialized value "write_info.bw_first" when calling "buf_write_bytes".
    1453             if (buf_write_bytes(&write_info) == FAIL) {
    1454               end = 0;                        // write error: break loop
    1455               break;
    1456             }
    1457             nchars += bufsize - write_info.bw_len;
    1458             s = buffer + write_info.bw_len;
2026-08-01 12:15:02 -04:00
Justin M. Keyes e02755cd9f fix(options): ":set foo?" for Lua callbacks #41100
Problem:
`:set foo=<tab>` and `:set foo?` for func/expr options set to a Lua
function, always displays "v:lua".

The existing "<Lua N: file:line>" form was avoided because the ref id
N changes on every read of the same option.

Solution:
- Don't attempt to tab-complete Lua functions.
- Show "<Lua file:line>" (or "<Lua>" if source file is unknown).

Example:

    :set operatorfunc?
      operatorfunc=<Lua ~/.config/nvim/init.lua:42>
2026-08-01 10:07:26 -04:00
Justin M. Keyes 4836ecad1f Merge #41065 from justinmk/optionfunc
feat(options): Lua closure/function options
2026-07-31 18:01:51 -04:00
Justin M. Keyes 102e806320 refactor(options): simplify "unset" logic 2026-07-31 21:50:04 +02:00
Justin M. Keyes 3e01dcddbe refactor(options): memory management
Problem:
Numerous callers have to manually check optval ownership (i.e. whether,
and how, to release) via `is_callback_option`, `option_is_global_local`,
etc. This is fragile, hard to use correctly; and if we introduce another
optval variant in the future, we'll have to redo all of these careful
checks and boilerplate again.

Solution:
Provide a unified system and use it everywhere:

    optval_free_owned
    optval_is_owned
    optval_own
2026-07-31 21:50:04 +02:00
Justin M. Keyes 726d1a92d2 feat(options): lua closure/function options
Problem:
Cannot assign Lua functions/closures to "func" ('completefunc',
'tagfun', …) or "expr" ('foldexpr', 'indentexpr', …) options.

Solution:
- Store "func"/"expr" options as `Callback` instead of string.
- Delete oceans of copy-pasted code.
- BREAKING: LuaRef returned via RPC/Vimscript is now represented as
  `"<Lua N: file:line>"` (like what `:map` shows) instead of `nil`.
- Note: `man.vim` still uses `v:lua` string, bc it's a vimscript ftplugin.

Helped-by: Lewis Russell <lewis6991@gmail.com>
2026-07-31 21:50:04 +02:00
Justin M. Keyes 917232b508 Merge #40953 from barrettruth/feat/zip-extract
feat(zip): extract archive entries
2026-07-31 15:45:14 -04:00
Justin M. Keyes 6cdab150bc docs: misc #41063 2026-07-30 15:09:19 -04:00
Justin M. Keyes a7f422fa48 refactor(options): naming #41046
- "obj" refers to structured (non-scalar `Object`) option values.
- "optval" generally refers to the internal scalar (legacy ":set"-style)
  option value.
2026-07-30 09:50:26 -04:00
Justin M. Keyes b5c623112e Merge #41057 from janlazo/vim-na-c-plus-docs 2026-07-30 05:48:42 -04:00
Justin M. Keyes b9d732c249 Merge #41045 from justinmk/refactoroptions 2026-07-29 09:57:19 -04:00
Justin M. Keyes 1a755e4890 fix(options): latent codegen bug
Problem:
Latent bug from 7f6c14ed54 (2015).
If an option default is "false" (`o.defaults.if_false = false`, e.g.
'fileignorecase'), codegen does not give it a `.def_val` on the platform
where its `#if` condition is undefined; it is zero-initialized.

This wasn't noticed until the parent commit, where zero value is
kObjectTypeNil, which `set_option_varp()` rejects.

Solution:
Check `if_false == nil` insteada of "falsey", so the `#else` branch gets
generated.
2026-07-29 15:27:45 +02:00
Justin M. Keyes cb7c019167 refactor(options): drop OptVal, use Object
Problem:
The object subsystem has an intermediate representation for no real
reason. Besides the code cost, this also adds an extra (api <=> OptVal)
conversion step, which is a (small) perf cost.

Solution:
We already have `Object`, so use it instead.

- drop `OptVal`, `OptValData`, `OptValType`, and related boilerplate.
- add `kObjectTypeUnset`.
2026-07-29 15:27:45 +02:00
Justin M. Keyes fbcb7a056c feat(exmode): "1q:", :exmode #41010
Problem:
Want `gQ` for _le multicursor_.

Solution:
- Don't use `gQ` for exmode.
- Introduce `:exmode`.
- Introduce `[count]q:` as an alias to `:exmode`.
2026-07-27 11:12:47 -04:00
Justin M. Keyes 80cd482577 fix(lua): vim.regex():match_str abort in Luv callback #41008 2026-07-27 11:01:54 +00:00
Justin M. Keyes 8ee8869a91 Merge #40971 from janlazo/vim-na-helphelp-tags-pt2
build(vim-patch): skip doc for n/a features
2026-07-27 06:58:38 -04:00
Justin M. Keyes 359459dec6 refactor(exmode): Ex-mode as cmdwin + Lua #40991
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.
2026-07-27 06:25:21 -04:00
Justin M. Keyes 6539bd6602 Merge #40910 from barrettruth/fix/dir-literal-env-paths 2026-07-26 06:11:01 -04:00
Justin M. Keyes 256a88d0ac Merge #40579 from barrettruth/fix/del-keymap-lhs-only-30258 2026-07-25 16:00:25 -04:00
715d8887ec docs: misc #40847
Co-authored-by: Barrett Ruth <br@barrettruth.com>
Co-authored-by: Nathan Zeng <nathan.j.zeng@gmail.com>
2026-07-25 12:47:51 -04:00
Justin M. Keyes bf3d4210c3 refactor: 'previewpopup' #40945
- drop w_maxwidth/w_maxheight, calculate them on-demand from
  'previewpopup' (this is cheap bc of `opt_keyset`)
2026-07-24 13:49:47 -04:00
Justin M. Keyes e533ec5160 Merge #40924 from justinmk/optdict 2026-07-24 07:36:52 -04:00
Justin M. Keyes c244a4bace refactor(options): rename, simplify 2026-07-24 13:05:20 +02:00
Justin M. Keyes 9604c758b3 perf(options): no-alloc keysets 2026-07-24 13:05:20 +02:00
Justin M. Keyes e8195baaef refactor(options): 'previewpopup' validation
Use a schema instead of manual parsing.
2026-07-24 13:05:20 +02:00
Justin M. Keyes 3fceb84560 refactor(options): store dict options in :set-string form
The parent commit made a valiant effort to store "dict options" in their
reified form, but this is more trouble than it's worth:
- inconsistent model for developers to understand.
- string lifetime issues (the "varp" convention is to pass around an
  aliased, long-lived option value).
- lots of extra plumbing to deal with the 2 different option-storage
  paradigms.
2026-07-24 13:05:20 +02:00
Justin M. Keyes f8cfd7f06a feat(options): schema, "dict" options, messages
Problem:
Options parsing is still painful for dict-style options.

Solution:
schema-maxxing => better `opt:get()` (will be the basis for `vim.o()`),
unified (and more-detailed) err msgs.

- Drop bespoke structure-builder in `_core/options.lua`.
- Define `schema` for all non-primitive options (except 'guicursor' and
  statusline-style options); generate reified keysets `OptKeyDict`).
  - Generate 'fillchars' => `fcs_tab`, 'listchars' => `lcs_tab`.
- `nvim_set_option_value`:
  - Return the improved structures. Also from `vim.opt.x:get()`.
  - Eliminate api <=> lua roundtrip, centralize option structure
    handling.
- Improve/unify errors.
  - Bump ERR_BUFLEN 80 → 256 so the "one of" list isn't truncated.
- Eliminate old 'diffopt' order-dependence (`iwhiteall` before `iwhite`)

Error samples:

    Typed-key path (opt_strings_check → diffopt/mousescroll/breakindentopt):
    E474: Unknown item 'foo'
    E474: 'context' requires a number
    E474: 'ver' number is out of range
    E474: 'algorithm' must be one of: myers, minimal, patience, histogram
    E474: 'filler' does not take a value

Related:

- #31084
- #34661
- #31820
- #14739
- #20107
- fix #18875
  - :get() returns `{ sbr = true, shift = '3' }` (reified-keyset) instead of `{'sbr', 'shift:3'}`
  - Setting via table now works too. `object_as_optval_for` `is_map` now recognizes struct options.
- fix #30296
  - instead of `E474: Invalid argument`, errors now look like:
    ```
    E474: Invalid value 'x', expected one of: single, double: ambiwidth=x
    E474: Unknown item 'foo': diffopt=foo
    E474: 'context' requires a number: diffopt=context:x
    ```

simplify `win_float_parse_option` from #26799.
2026-07-24 13:05:20 +02:00
Justin M. Keyes 411c82d108 Merge #40883 from janlazo/na-hunk-runtime-doc_vimfn
build(vim-patch): N/A runtime/doc/*.txt updates
2026-07-23 15:24:57 -04:00
Justin M. Keyes 008ea4dd65 Merge #40873 fix(ui): handle combining chunked float titles 2026-07-21 11:45:44 -04:00
Justin M. Keyes 6d5b774362 build(luals): drop src/.luarc.json, test/.luarc.json #40887
Problem:
emmylua reports `unresolved-require` for `require('test.testutil')` and
friends, and a file under `test/` cannot even resolve its own siblings.

This happens because `test/.emmyrc.json` and `test/.luarc.json`, cause
vim.lsp to root the workspace at `test/` instead of the repo top-level.

Solution:
Drop the nested configs:

    src/.luarc.json
    test/.luarc.json
    test/.emmyrc.json

Outcomes:
- No "libraries" needed: luv types come from `runtime/lua/uv/_meta.lua`,
  not `${3rd}/luv` (which would only duplicate them).
- Drop `test/` from the root `.luarc.json` `ignoreDir` so its modules are
  resolvable by LuaLS/Emmylua.
  - Note: this means that LuaLS diagnostics will now be reported for
    `test/`, but that is not a bad thing...
2026-07-21 08:56:42 -04:00
Justin M. Keyes f907614088 Merge #40865 from justinmk/fixci 2026-07-21 08:04:16 -04:00
Justin M. Keyes 1250821ba8 test: drop assert.is_true() and friends 2026-07-21 13:22:39 +02: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 86fbcd67be test: unreliable "CursorHold not triggered after only K_EVENT on startup" #40863
Problem:

    FAILED   …/cursorhold_spec.lua @ 73: CursorHold is not triggered after only K_EVENT on startup
    Expected values to be equal.
    Expected:
    1
    Actual:
    0
    stack traceback:
    …/cursorhold_spec.lua:79: in function <…/cursorhold_spec.lua:73>

Solution:
Retry instead of hardcoding sleep(50).
2026-07-20 11:12:53 -04:00
Justin M. Keyes 317c5ddda6 fix(lsp): incorrect rendering of markdown codeblock #40861
Problem:
LSP hover erroneously drops blank lines before a 4-space-indented
codeblock, which is not valid Markdown. This causes incorrect parsing
and wrong display.

Solution:
Fix `split_lines` so that it doesn't drop the blank line just before
a 4-space-indented codeblock.

fix https://github.com/neovim/neovim/issues/40860
2026-07-20 10:03:11 -04:00
Justin M. Keyes 61abef3564 ci: combine PR-labeler jobs #40842
Problem:
We have 5 "labeler" jobs, this is kind of nuts, it bloats the CI report,
and also introduces race conditions...

Solution:
Merge 3 jobs into 1 `label` job with multiple sequential steps.
- Eliminates the `needs` chain
- Drops unused `contents:write` permission.
- GITHUB_TOKEN is scoped to the single `gh` step.
  - Note: is the automatic job token (`contents:read` + `pull-requests:write`,
    not a PAT), so worst case is PR/label mischief.

The `ai-assisted` and `request-reviewer` jobs stay separate, bc they are
triggered on different events (not only "opened").
2026-07-19 12:52:49 -04:00
Justin M. Keyes 40ab941f79 test: improve Nvim EOF handling (for Windows CI) #40841
Problem:
Mysterious Windows CI failure points to resource exhaustion, but we have
no visibility/instrumentation:

    RUN      T610 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <S-Left>: 93.70 ms OK
    RUN      T611 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <F12><F2><Tab>: 49.48 ms FAIL
    …\testnvim.lua:144: EOF was received from Nvim. Likely the Nvim process crashed.
    stack traceback:
            D:/a/neovim/neovim/test/functional/testnvim.lua:144: in function 'nvim_set_keymap'
            …/api/keymap_spec.lua:769: in function <…/api/keymap_spec.lua:768>
    RUN      T612 nvim_set_keymap, nvim_del_keymap can set mappings with special characters, lhs: <S-Left>, rhs: <Space><Tab>: 18.49 ms FAIL
    …\testnvim.lua:144: EOF was received from Nvim. Likely the Nvim process crashed.
    stack traceback:
            D:/a/neovim/neovim/test/functional/testnvim.lua:144: in function 'nvim_set_keymap'
            …/api/keymap_spec.lua:769: in function <.../build/Xtest_xdg_api/test/functional/api/keymap_spec.lua:768>

Solution:
Append the child exit-code and signal to the "EOF … crashed" message.

Example:

    Nvim EOF (crash?) exit code: 42 (0x0000002A)

On the next failing Windows run, the first crash line will classify the failure:
- 0xC0000005 → access violation (nvim bug)
- 0xC0000374 → heap corruption (nvim bug)
- spawn/resource error or "clean" exit-code → system resource exhaustion
2026-07-19 12:45:40 -04:00
d1b3a9924d feat(api): mark nvim_create_autocmd as api-fast #40836
Problem:
nvim_create_autocmd() isn't |api-fast|, so modules that create autocmds
(e.g. vim.treesitter.query) can't be require()d in a fast event context.

Solution:
Mark it |api-fast|. Compile autocmd patterns with RE_NOBREAK so
aucmd_next() won't os_breakcheck() mid-iteration, where a fast
nvim_create_autocmd() could realloc the autocmds vector and dangle the
caller's AutoPat/AutoCmd.

RE_NOBREAK is low-risk because:
- aucmd_next()'s loop checks CTRL-C: `(for (… i < apc->ausize && !got_int; …)`.
- `line_breakcheck()` (`autocmd.c:1912`) runs once per matched autocmd.
- Each autocmd _execution_ runs through `do_cmdline`, which has its own
  breakchecks.

However this does admit risk of a pathological case:
a catastrophic-backtracking glob matched against a very long `User`
event-pattern.

Co-authored-by: Riley Bruins <ribru17@hotmail.com>
Co-authored-by: zeertzjq <zeertzjq@outlook.com>
2026-07-19 11:38:14 -04:00
Justin M. Keyes 851656c628 fix(coverity): STRING_OVERFLOW #40835
CID 651340:         Security best practices violations  (STRING_OVERFLOW)
    /src/nvim/keycodes.c: 383             in get_special_key()
    377             data->key = *s;
    378             data->key_alt = (String){ NULL, 0 };
    379           }
    380         }
    381
    382         if ((int)s->size + idx + 2 <= MAX_KEY_NAME_LEN) {
    >>>     CID 651340:         Security best practices violations  (STRING_OVERFLOW)
    >>>     You might overrun the 33-character fixed-size string "string + idx" by copying "s->data" without checking the length.
    383           STRCPY(string + idx, s->data);
    384           idx += (int)s->size;
    385         }
    386       }
    387       string[idx++] = '>';
    388       string[idx] = NUL;
2026-07-19 14:07:18 +00:00
Justin M. Keyes 239125b8c8 feat(eval): declare more "fast" functions #40834 2026-07-19 10:03:53 -04:00
Justin M. Keyes d09a00126b Merge #40819 from janlazo/na-hunk-c_dev-vimpatch
build(vim-patch): N/A memory/char/string functions, N/A whitespace-only patches
2026-07-18 16:48:03 -04:00
Justin M. Keyes 6f5fae3f8c fix(lua): vim.keycode cleanup #40817 2026-07-18 14:31:36 -04:00
Justin M. Keyes c6d885b0a6 Merge #40813 test: local failures, "succeeded immediately" 2026-07-18 10:48:43 -04:00