Problem:
Clearing a register via `:let @a = ""` doesn't persist in shada.
Solution:
Follows the precedent of ee56daebb6 .
Namely, when the live register is empty and *at least as recent* as the copy
that is on disk, drop it instead of writing it back.
Problem:
`:let @/ = ""` doesn't stick after restart (the old search pattern comes back).
Solution:
When the current session cleared the pattern more-recently than stored
shada entry, drop the shada entry instead of restoring it.
Note: similar timestamp-based suppression was used for deleted
marks: ee56daebb6
Problem:
Neovim can permanently stop accepting keyboard input after a large paste, or after
any sufficiently large input burst. The screen still redraws and honours window
resize, but typed keys have no effect and the session must be killed. A paste that
triggers the freeze is applied only partially.
Root cause:
`src/nvim/os/input.c` holds `input_buffer[INPUT_BUFFER_SIZE]` (16386 bytes),
compacted left rather than used as a ring:
- `input_get()` drains the buffer by advancing `input_read_pos`, but never rewinds
the cursors when it empties (`input_read_pos == input_write_pos`).
- The only rewind lives in `input_enqueue_raw()`, which `input_enqueue()` reaches
only inside `while (input_space() >= 19 && ptr < end)` (19 is the maximum
expansion of one `<x>` key form).
So once input fills the buffer to within 19 bytes of the top and is then fully
drained, the cursors are pinned near the top with `input_space() < 19`. The gate
never reopens, `input_enqueue()` never rewinds, and all further input is silently
dropped: `input_available()` stays 0 and the editor blocks forever in
`state_enter()` → `input_get()`. Redraw and resize run on independent paths, which
is why the UI looks alive while the keyboard is ignored.
Solution:
Rewind the read/write cursors when the buffer is empty, at the start of
`input_enqueue()`. The reset moves no data in the empty case and guarantees the
space gate can reopen; the non-empty case self-heals as the editor drains.
Test case:
A reproducer (no terminal required; drives `nvim --embed` over msgpack-RPC via
`nvim_input`) floods the input buffer, lets the editor drain it, then probes with
`:qa!`. It shows a sharp threshold at `INPUT_BUFFER_SIZE − 18`:
| `--fill` | master | with this fix |
|----------|-----------|---------------|
| ≤ 16367 | quits | quits |
| ≥ 16368 | **hangs** | quits |
- On current `master`, the freeze reproduces at `fill=16368`; the patched build
quits for every fill up to 100000.
- Interactive: a ~40 kB bracketed paste into Insert mode or a `:terminal` no longer
freezes.
Problem:
Session files specified at startup `-S [file]`, logically conflict
with `:restart`.
Solution:
Remove `-S [file]` from `v:argv` when doing :restart.
Also for the "bang" variant `:restart!`, just because it's
simpler (if anyone reports a use-case later, we can revisit).
Problem:
Inlay hints used separate global and per-buffer bufstates tables and
bespoke global autocmds for managing the inlay hint state across buffers
and clients, duplicating the lifecycle logic already provided by the
Capability framework. This caused inconsistencies in how client state
was handled and inlay hint state lifecycle was managed compared to other
LSP features.
Solution:
Replace the ad-hoc bufstate tracking and global autocmds in
vim.lsp.inlay_hint with a proper InlayHint subclass of Capability.
This also refactors the way inlay hint state is managed and fixes bugs I
found while doing this:
1. For each line with inlay hints, the list of the hints along with
whether they have been applied is stored in a current result on the
client state. This allows the on_win decorator to clear all inlay
hints for an old document version once, and then re-add the new
version's hints line-by-line as they are drawn to the screen,
modeling the semantic tokens module.
2. It fixes problems with mixing results from multiple clients attached
to the buffer by fully moving each client's state to its own table.
Previously, only the most recent document version used to populate a
line's inlay hints was stored, but there was no distinction for which
client the hints may have come from. (Fixes#36318)
3. It fixes the workspace/inlayHint/refresh server->client notification
behavior. Previously it would only re-request inlay hints for buffers
currently displayed in a window but would not invalidate them in
non-displayed buffers (or provide any mechanism for those buffers to
re-request at a later time). Model semantic token module here again
by invalidating all buffers, and adding a BufWinEnter autocmd to
refresh hints.
4. Add a mechanism to cancel in-flight requests if a new request for a
newer document version is made before the last one returned
5. Handle stale results by simply dropping them.
Problem:
When screen:expect() fails, it renders a snapshot for the error
message. If the grid references a highlight id that was never defined
via "hl_attr_define", the renderer crashes:
screen.lua:1910: attempt to index local 'entry' (a nil value)
This hides the actual failure, and appears "flaky": it only fires on the
failure path, and only when the shared screen is missing an id the grid
still references. A screen created in setup() attaches mid-session, so
highlight ids allocated before it attached (still referenced by stale
grid cells) are never sent to it.
Solution:
- Don't crash while rendering a diagnostic: show undefined highlight ids
as "UNKNOWN_HL_ID(n)", so the real failure and the desync are legible.
- put_spec: fix `visualbell` typo. If it fails again then we can find
the actual root cause.
ref https://github.com/neovim/neovim/issues/36250
Ref #6645
Problem:
When a window is resized it takes space from the window right/below first,
and only falls back to the window left/above when there is no more room.
Sometimes a user wants the space to come from a specific direction.
Solution:
Add nvim_win_resize(win, width, height, {anchor}) which resizes a window
with a choosable anchor edge, letting a window grow leftwards or upwards
by taking space from the window to the left or above first. The default
anchor reproduces nvim_win_set_width()/nvim_win_set_height().
Problem:
`vim.lsp.buf.format()` accepts ranges using nvim indexing, where an
end column of -1 means end of line. LSP ranges cannot use that,
which is confusing for things like range formatting.
Solution:
Resolve -1 end columns to the line length before converting the range to
LSP positions.
Problem:
When buffers are by default `nomodifiable`, such as when Nvim starts with
`-M`, the health buffer cannot be updated.
Solution:
Always set `modifiable` before modifying the buffer.
PROBLEM:
Cursor briefly flickers in cmdline when the "written" message is printed
(very noticeable in Neovide with cursor animation).
SOLUTION:
- Mark UI "busy" (cursor hidden) while emitting the message, as done for
the search message (cb2ca54331).
- Note: ff68fd6b8a moved the message from `filemess()` into
`buf_write..msg_progress`.
- Fix a bug in `tui.c:flush_buf` which manifested after this change. See
ANALYSIS below. https://github.com/libuv/libuv/issues/5182
ANALYSIS:
After this change...
ui_busy_start();
set_keep_msg(msg_progress(IObuff, msg_id, "success", 0, true, true), 0);
ui_busy_stop();
...ASAN analyzer fails on tui_spec.lua test "with non-tty (pipe) stdout/stderr":
2026-07-04T14:09:28.9521023Z = ==32405==ERROR: AddressSanitizer: ABRT on unknown address 0x03e900007e95
...
4 0x7f5506a288fe in abort stdlib/abort.c:79:7
5 0x559afd7113a2 in uv__epoll_ctl_flush
build/src/libuv/src/unix/linux.c:1335:7
6 0x559afd710a81 in uv__io_poll
build/src/libuv/src/unix/linux.c:1448:9
7 0x559afd6f67a7 in uv_run
build/src/libuv/src/unix/core.c:460:5
8 0x559afd2a72cc in flush_buf
nvim/tui/tui.c:2642:5
9 0x559afd2bce9f in tui_flush
nvim/tui/tui.c:1747:3
10 0x559afd2f39a0 in ui_client_event_flush
nvim/auto/ui_events_client.generated.h:64:3
11 0x559afcb8e014 in parse_msgpack
nvim/msgpack_rpc/channel.c:255:11
12 0x559afcb840e7 in receive_msgpack
nvim/msgpack_rpc/channel.c:217:5
13 0x559afc53b2af in read_event
nvim/event/rstream.c:180:23
14 0x559afc53ad52 in invoke_read_cb
nvim/event/rstream.c:233:3
15 0x559afc5382d6 in read_cb
nvim/event/rstream.c:135:3
16 0x559afd707b25 in uv__read
build/src/libuv/src/unix/stream.c:1145:7
17 0x559afd70744a in uv__stream_io
build/src/libuv/src/unix/stream.c:1208:5
18 0x559afd6f727e in uv__io_cb
build/src/libuv/src/unix/core.c:930:5
19 0x559afd710d7a in uv__io_poll
build/src/libuv/src/unix/linux.c:1546:11
20 0x559afd6f67a7 in uv_run
build/src/libuv/src/unix/core.c:460:5
21 0x559afc526e2d in loop_uv_run
nvim/event/loop.c:59:3
22 0x559afc526aa4 in loop_poll_events
nvim/event/loop.c:80:26
23 0x559afd2fdbb7 in ui_client_run
nvim/ui_client.c:172:5
24 0x559afc920d7b in main /home/runner/work/neovim/neovim/src/nvim/main.c:355:5
25 0x7f5506a2a1c9 in __libc_start_call_main
main.h:58:16
26 0x7f5506a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
27 0x559afbd6d254 in _start
The abort requires two rare conditions:
1. a `uv_write` whose buffer array *ends* in a zero-length buffer, which only
happens by "flush while cursor-hidden".
2. an output fd that *epoll cannot watch*. The only CI test with such an fd is
tui_spec.lua "with non-tty (pipe) stdout/stderr", which runs a TUI with
`stdout > /dev/null`, then runs `:w testF`, which triggers the "written"
message.
Our change guarantees trailing-empty flushes (`:w` fires busy + `ui_flush()`
while busy) in the only test that runs a TUI on `/dev/null`.
0. In a normal (non-busy) no-sync flush, post always contains `cursor_normal`.
1. Our change emits:
```
busy_start → msg_progress("…written") → ui_flush → busy_stop
```
2. TUI flushes *while busy*, so `should_invisible() = true` and `flush_buf`
skips the cursor-restore string (`bufs[2]` is zero-length).
3. libuv *cannot complete* a write whose tail is a zero-length buffer: EVERY
trailing-empty write goes through epoll, even when every byte was already
written. https://github.com/libuv/libuv/issues/5182
4. In `tui_spec.lua:3298`, the output fd is `/dev/null`, whose kernel
`file_operations` has no `.poll` method (`drivers/char/mem.c`), so
`EPOLL_CTL_ADD` fails with `EPERM`.
Trailing empty buffers are semantically pointless, and libuv punishes
them: forced async, one epoll round-trip *per trailing empty*, plus
a 0-byte `write()` syscall.
Helped-by: Shougo Matsushita <Shougo.Matsu@gmail.com>
Helped-by: Fred Sundvik <fsundvik@gmail.com>
Problem:
Plugins using RPC sockets cannot detect when the peer closes a
`sockconnect()` channel, so reconnect logic has no reliable trigger.
Solution:
Add a `ChanClose` event with channel info before the channel is removed,
matching the existing `ChanOpen`/`ChanInfo` event model.
Problem:
FAILED …/defaults_spec.lua @ 1286: stdpath() avoids DOS 8.3 filenames for "cache" and "run"
Expected values to be equal.
Expected:
"XTEST_~1"
Actual:
"XTEST_~2"
stack traceback:
…/defaults_spec.lua:1296: in function <…/defaults_spec.lua:1286>
Solution:
Relax the test.
fix(iconv): conversion to utf-16be using iconv is inconsistent
vim-patch:9.2.0769
Problem: enc_canonize function changes utf-16be to utf-16 but in linux
type utf-16 defaults to utf-16le.
Solution: Creating a separate entry for utf-16be in enc_canon_table.
Note: the effect is only visible on iconv implementations that
treat "utf-16" and "utf-16be" differently, so the test does
not necessarily fail on an unpatched Vim. But the bug is
visible in vim.iconv. (Manoj Panda)
from: vim/vim@2a63f74
closes: neovim#40262
Signed-off-by: Manoj Panda <manojpandawork@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
Problem:
The dir.lua "-" mapping cannot be easily overridden (because of autocmd
ordering).
Solution:
- Move it to defaults.lua.
- Also to be extra polite: fall back to builtin `-` motion if the user
disabled the `dir.lua` plugin.
Problem: Some code for 'autocompletedelay' is no longer needed now that
'autocompletedelay' doesn't block redraw (after 9.2.0739).
Solution: Remove unnecessary code. Also remove a duplicate screendump
and an outdated comment in test (zeertzjq)
closes: vim/vim#206860b86b97cc9
Problem: 'autocompletedelay' interferes with i_CTRL-K (after 9.2.0739).
Solution: Clear the pending autocompltion from the previous key when a
new key is typed.
closes: vim/vim#206660d292e2067
Problem: With a non-zero 'autocompletedelay', Insert-mode autocommands
(TextChangedI, TextChangedP, CursorMovedI) are delayed, and
while typing faster than the delay they are dropped entirely,
because the delay blocks the main loop.
Solution: Make 'autocompletedelay' non-blocking: instead of busy-waiting
before showing the popup menu, defer it with an input-wait
timeout (K_COMPLETE_DELAY) modeled on CursorHoldI, so typing
stays responsive and the Insert-mode autocommands fire normally.
The delay timer coexists with 'updatetime': the main loop waits for the
sooner of the two and triggers the event whose deadline was reached, so
'autocompletedelay' no longer shadows CursorHold timing. Changing the
completion leader, for example with Backspace, updates the visible popup
immediately like a zero delay; only the first popup is deferred.
Update the 'autocompletedelay' screendumps for the non-blocking display.
One test opened the menu with CTRL-N right after the delay expired and
could race with the deferred popup, so it now waits a little longer than
the delay before sending the key.
fixes: vim/vim#20591closes: vim/vim#205988ce43ea4e3
Also include some insexpand.c and ui.c changes from patch 9.2.0750.
Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem:
On Windows, `fnamemodify('//foo/C$', ':h')` incorrectly removes `C$`
as a regular file name and returns `//foo`. However, this is a valid
UNC path, `foo` is a server name and `C$` is a share name.
The correct result should be `//foo/C$`.
Solution:
Extend `os_fileinfo2` and `FileInfo` with `prefix_off`, `rest_off` to
identify path types and logical root boundaries. ':h' can use this info
to prevent traversing past the logical root.
Examples:
/foo => /
//foo => // (POSIX)
//foo/bar => //foo (POSIX)
//server/share/foo => //server/share/ (Windows)
C:/foo => C:/
//?/C:/foo => //?/C:/
Co-authored-by: Barrett Ruth <br@barrettruth.com>
Problem:
vim.fs.dir() and vim.fs.find() drop errors returned by uv.fs_scandir().
Solution:
- vim.fs.dir():
- Return root scan failures as a secondary return value.
- Propagate recursive scan failures through the iterator. This allows
callers to distinguish unreadable directories from empty ones.
- vim.fs.find(): Collect errors during search, and return the list as
a second retval.
- Replace newlines in the current cmdline with NULs when opening cmdwin,
and do the reverse when putting a cmdwin line back into the cmdline.
- Escape control characters with Ctrl-V when feeding cmdline.
Problem:
- If cmdwin window is split, ENTER in one does not close the others.
- If cmdwin is put into a different tabpage via <c-w>T, it stops working
(ENTER does not execute the cmd).
Solution:
- Close the buffer instead of the window.
- In the WinClosed handler, skip `M._cleanup()` unless this is the last
cmdwin window.
Problem: changed_lines got a hardcoded 0, so the changelist entry
and '. mark always recorded column 0 instead of where the edit
actually happened.
Solution: pass start_col instead. changelist now tracks the real
column.
Problem:
`:restart` does not preserve window layout, etc.
Solution:
- Change `:restart` to save/restore a session automatically.
- Introduce "bang" variant `:restart!` to restart *without* session
save/restore.
- Introduce `v:startreason`.
- `ZR` maps to `:restart!`.
Problem:
On Windows, channel jobs inherit Nvim's stdio, so a background job
writing to CON (e.g. gutentags) draws onto the TUI and stays until
redraw.
Solution:
Give Windows job stdin/stderr libuv-created pipes (UV_CREATE_PIPE)
instead of inherited fds, so libuv spawns the child with
CREATE_NO_WINDOW and CON writes no longer leak onto the TUI.
Problem:
- Lua<=>API roundtrips
- Although we prefer Lua for most business-logic code, doing this
conversion in C makes sense in this case because:
1. setting options is a hot path
2. most of the options logic lives in C
3. the current arrangement is MORE verbose and requires MORE code
Solution:
Move conversion to a C util.
- nvim_set_option_value passes the raw Object (scalar, Array, or Dict)
to `object_as_optval_for()` which flattens it to the canonical `:set`
string and validates the type.
- drop `convert_value_to_vim`, eliminate its roundtrip.
The added test shows the context, one expected a JSON field
to be a Object but it was a null value
pros: shows `vim.NIL` instead of `a userdata`
cons: the context `field 'foo'` is lost. I think this is generated
with internal magic which is hard to replicate.
Problem:
No lint for strtol().
Solution:
Add lint, and update existing usages.
Callers that previously got a (garbage) large positive value:
- `indent.c`: use getdigits() (intmax, no int truncation) with def=1 so
overflow/too-large values stay positive and fall through to the
existing "too big" check (E475).
- `file_search.c`: use getdigits() with def=255 so overflow/too-large
values keep the "max expand" (else) branch.
The other conversions (api/command.c, eval/window.c, highlight_group.c,
tui/tui.c) only diverge on pathological overflow inputs where strtol's
result was already garbage and the observable outcome is unchanged.
Problem:
`nvim_set_option_value` cannot "update" options similar to `:set opt=`,
`:set opt+=`, etc. The Lua impls of "vim.opt" / "vim.o" have incomplete,
bespoke reimplementations of those operations.
ref #38420
Solution:
- Add `operation` param to `nvim_set_option_value`, which may be "set",
"append", "prepend", or "remove".
- Use this feature to implement `vim.opt` / `vim.o`.
Problem:
`os.exit()` in `nvim -l` exits through normal teardown. But, as #39783 shows,
when it is called from a libuv callback, teardown polling the main loop
when inside `uv_run()` can trip the recursive poll guard and cause a crash.
Fast callbacks are not safe to teardown, so it's better to schedule an exit out
of a callback rather than convolutedly handle this as I tried before.
Solution:
Reject `os.exit()` from fast callbacks with `E5560` error.
Problem: due to totally async install/update/checkout there is no
guaranteed order of `PackChanged{Pre,}` events across different
plugins. This might lead to conflicts when callback for some "main"
plugin relies on features from "dependency" plugin: i.e. callback for
"main" plugin can trigger before installing/updating "dependency"
plugin. The installation order can be enforced by separate
vim.pack.add() calls, but update/checkout can not.
Solution: Trigger events in bulk independently of async execution:
- `PackChangedPre` before any action for all input plugins in order
they are supplied. It will also trigger even if an action will fail.
- `PackChanged` after all actions finished for all sucessfully
affected plugins in order they are supplied.
This also comes with a couple of side effect changes:
- `PackChangedPre kind=delete` is now also triggered even if the
delete won't be done. This makes it more aligned with `kind=install`
and `kind=update`.
- Force update (`:packupdate!`) and "udpate LSP action" now do two
async steps: download/compute updates and apply them. This also
results in two progress reports.
This is mostly a by-product of the implementation (there has to be
a pre-computation of target revision for all plugins before doing
`PackChangedPre` in bulk before possibly applying an update), but I
kind of like it more this way as it is more explicit of what's going
on. If absolutely not acceptable, there might be some hacks to
mitigate it at least in code action, but I'd keep it like this.
Problem:
dir.lua leaves previously-navigated directory buffers around.
This is fine by default, but users need a simple way to opt out.
Solution:
1. Respect `set hidden` (via `'bufhidden'`) as one way to make
previously-navigated dir buffers from showing up.
2. Document a one-liner to hide these buffers
Problem: plugin/shada.lua does not respect the `+` flag of 'cpoptions'
when writing a *.shada buffer into a different file.
Solution: Check the presence of `+` in 'cpoptions' and whether the
buffer is written to a different file in BufWriteCmd for
*.shada before resetting the 'modified' option of the buffer.
This little defect appeared after plugin/shada.vim was rewritten into
Lua in #34725.
Problem: Separation markers (%=) are ignored within item groups. This
lead to a regression when the C implementation of the statusline was
replaced with a default expression. When the user configured a custom
ruler expression with a %= and used the overloaded item group syntax to
set the ruler width, the separation marker worked in the ruler, but not
when the ruler was incorporated into the statusline where the item group
syntax was interpreted in the usual way.
Solution: Analogously to top-level behaviour, expand separation markers
evenly within item groups until `minwid` is reached (if set).
ref https://github.com/neovim/neovim/pull/33036
fix https://github.com/neovim/neovim/issues/39984
ref https://github.com/neovim/neovim/issues/40247
Problem: The recursion offset into the static `stl_items` was not taken
into account when adjusting the item count after truncation.
Steps to reproduce: first prepare `stl_items`:
set stl=%{%repeat('%#Error#',10)%}
then watch how the Error highlight leaks into the recursive call:
set stl=%l%l%l%{%nvim_eval_statusline('test%l%<',{'maxwidth':3,'highlights':1}).highlights%}
ref https://github.com/neovim/neovim/issues/32259
* fix(statusline): consistent truncation at multicell character
Problem 1: truncation of item groups at multicell character didn't take
into account that minwid can be specified as a negative number.
Problem 2: after truncation at top-level from the right at multicell
character, the returned width was always `maxwidth`, even though the
actual width was reduced. In vim, this can be observed as a statusline
that is not fully drawn until the edge of the screen:
vim --clean +"set ls=2 stl=%{%repeat('x',&columns-2)%}🙂x%<"
Problem 3: after truncation at top-level from the left at multicell
character, the resulting gap to reach `maxwidth` again was filled with
fillchars, but then the final NUL was not set correctly.
This can be seen in the following example, where the statuscolumn spills
into the editing area starting from line 10:
nvim --clean +"set number stc=%<x🙂%{repeat('x',43)}%l" +"norm yy10p"
Solution: fix the small errors and, at top-level, consistently reduce
the size instead of compensating with fillchars. In the case of the
statusline and the winbar, the remaining place is filled with the
configured fillchars in `win_redr_custom`, after `build_stl_str_hl` has
returned. In all other cases (title, icon, statuscol, tabline, ruler),
there seems to be no point in adding additional spaces at the end.
* feat(statusline)!: scope %< to item groups
Problem:
Previously, item groups were only truncated at the beginning, which is
often not desired. In the example
%.15(path: %f%)
the group's title/label is truncated away:
<th/to/file.txt
Truncation markers (%<) in item groups were processed at the top-level
in the end, which can be confusing. Only the first %< is used for the
whole string, and it is used even if the containing item group is
hidden. Additionally, in the case of hidden item groups, the marker's
position was not adapted. For example,
%(hidden%<%)%f
had the effect of truncating the path somewhere in the middle:
/path/<file.txt
Solution:
Make truncation consistent with top-level behaviour, which has a better
default of truncating at the first `Normal` item, i.e.
path: <file.txt
and allows for fine-grained control with truncation markers (%<). E.g.
%.15(path: %f%<%)
now yields
path: /path/to>
The original behaviour can be restored like so:
%.15(%<path: %f%)
BREAKING CHANGE: %< is no longer processed at top-level
- the default truncation behaviour has changed: now at first item
- truncation markers inside item groups don't affect truncation outside
of the item group anymore
- several truncation markers can now have an effect when separated with
item groups, whereas previously only the first one globally had
ref https://github.com/neovim/neovim/issues/39984