Problem:
`:restart` doesn't work if UI is local and server is remote (i.e.
different machines).
Solution:
How remote-ssh `:restart` works:
1. The remote server restarts as usual; the new server takes over the
same `--listen` socket path.
2. `vim.net._ssh.serve()` doesn't reattach anything. It doesn't exit
when the old server-PID dies, so ssh session/tunnel stays alive. It
gets the new PID after polling for a bit.
3. So the local `ssh -L` tunnel stays up, and forwards to the same
remote socket path, now owned by the new server.
4. The UI reconnects through the tunnel (the `addr` stored by
`nvim_chan_set`).
Problem:
- remote_ssh.sh detects ssh disconnect by writing a heartbeat every 1s,
which the local side buffers for the whole session.
- The server's stdout (e.g. `io.write()` from a random plugin) writes to
the tunnel, which in theory may contain a bogus "NVIM_READY".
- A nightly install is never updated on the remote.
Solution:
- Supervise the server in `vim.net._ssh.serve()` (Lua). It detects
disconnect via stdin EOF and discards server stdout. The script only
finds or installs `nvim`.
- Also a step toward Windows remotes.
- User may choose "Update Nvim" to reinstall the nightly.
Problem:
- vim.uri.parse() not fully baked, it does not structure some parts of its result (esp. `query`).
- The scheme is not lowercased, which violates the RFC.
- No "constructor" for vim.Uri.
- tostring(vim.Uri) is not fully driven by the vim.Uri fields.
Solution:
- Structure `authority`, `query`.
- Lowercase the scheme.
- Drop `_parse_ssh_uri()`.
- Add `vim.uri.new()`.
- Fix tostring(vim.Uri).
- Validation.
Problem:
`vim.env` (`vim.fn.getenv`) fails in a fast context (luv callbacks, LSP
rpc).
Solution:
Mark `getenv()` as |api-fast|. `vim_getenv()` never tickles `os_breakcheck()`.
Problem:
Can't easily wait on the streamed output of a long-lived process (e.g.
`ssh -L`). remote-ssh used a private `run_wait()` util.
Solution:
Add `cond` to `SystemObj:wait()`: return early (process keeps running)
when `cond(stdout, stderr)` is true. Drop `run_wait()`.
Problem:
- vim.rpcrequest() blocks forever if the peer never responds.
- Nvim crashes on unexpected (unsolicited/late) RPC response (attempts
to index empty call stack).
- rpcrequest()/rpcnotify() are redundant with vim.rpcrequest()/vim.rpcnotify().
Solution:
- `vim.rpcrequest(chan, opts, method, …)` accepts `opts.timeout`.
- Discard (and DLOG) responses to stale (timed-out) requests.
- Deprecate rpcrequest() and rpcnotify().
Problem:
Tests are slow because of:
- kitty graphics check in a non-terminal
- Also affects checkhealth outside of tests (if run headless).
- network call (`git ls-remote`)
Solution:
- Skip the query unless a UI has "stdout_tty".
- Skip the update-check in tests.
Problem:
- The TUI rejects server-to-client API calls, via a hardcoded check.
Other clients, e.g. `nvim --remote` or a `sockconnect()` channel, let
untrusted peers call the local API.
- No per-channel control of this.
Solution:
- Support a per-channel "trust" flag for `sockconnect()` and `nvim_chan_set()`.
- remote-ssh uses `trust=false` instead of hand-encoded msgpack.
- UI and `--remote*` client channels are untrusted by default.
- Report the "trust" flag in `nvim_get_chan_info()`.
- SshHost fields: follow ssh's "first wins" rule, so e.g. a later User
line in the same block no longer overrides an earlier one.
- Local server now exits on its own when the remote session ends.
- Before auto-install, try existing `nvim` found in remote `$PATH`.
- Combine the remote_ssh_x.sh scripts into one remote_ssh.sh script.
- Ignore `RemoteCommand` from ssh_config, which made ssh refuse our command.
- Don't accumulate the remote heartbeat in memory for the whole session:
`run_wait()` captures output only until it returns.
- Before detaching the UI, check that the remote Nvim answers through the
forward: sshd may refuse the forward, but still run the script.
- Stop the session (tunnel, remote Nvim) if any connect step fails, or if
the UI detached meanwhile.
- Run `ssh -G` once per session, instead of for every ssh command.
- Remote script handles signals at once, instead of after the heartbeat's
- Error if :restart is attempted.
FUTURE: Remote :restart prints a marker, with its [command], on
stdout. It also sends the TUI a restart event pointing at the LOCAL
server. The TUI reattaches there. Local server repeats the remote-ssh
connect steps, and sends the TUI a connect event for the new forwarded
socket. Then it runs [command] (e.g. session restore) on the new
remote over RPC.
Problem:
On Windows, path_shorten_fname() treats "c:x" as a root ("c:\"), so it
shortens "c:xyz\foo" to "yz\foo".
Solution:
Only "c:\" (drive and separator) is a root, not "c:x". Also simplify
path_to_absolute().
Problem:
On Windows, a libuv watch may repeat an error forever (e.g. EPERM after the
watched file's parent directory is deleted). The previous fix
77443675ef only stops the watch if it sees the file deleted, so a lost
event (buffer overflow) or filtered event still spins. It also silently
kills the watch on any delete, so a recreated file gets no events.
Solution:
Stop the handle on the first error, which also marks the backend failed so it
is not reused. Don't stop on delete.
Problem:
`2"ar<Tab>` (with 'expandtab') replaces 4 chars, and `2"ag<LeftMouse>` jumps to
the 2nd tag. As with `2"_x`: the stuffed keys already hold the count, but a
nested command executed by exec_stuffed() sees the register, so it multiplies
by `opcount` again.
Solution:
Clear `opcount` in exec_stuffed() instead of only in nv_optrans().
`dd2"a.` now deletes 2 lines, as documented at `:help .` (Vim: 4).
Problem:
UI event handlers may clobber `IObuff` while the caller is using it.
With ui2 `targets='msg'`, `i_CTRL-O :edit` shows "'replacement string'
item" instead of the file info, because `check_string_array()` writes
`IObuff` on every `nvim_buf_set_lines()`.
Solution:
- Save and restore `IObuff` around UI event handlers. This is 1KB, but
negligible vs the other work that UI handlers do (msgpack, Lua calls).
- `check_string_array()`: don't use `IObuff`.
Problem:
- `json_encode()` leaks the conversion stack if a dict key nested more
than 8 levels deep is invalid UTF-8, and leaks the string of an
invalid special string (`v:msgpack_types.string`).
- Lua conversion passes the total stack size to `lua_checkstack()`,
which expects the extra slots, so it fails at half the real limit.
- If `nlua_push_typval()` fails, the Lua stack is not restored: e.g.
`vim.g.x` returns part of the half-built value.
Solution:
- `encode_vim_to_*()` owns the conversion state, so converter macros
may return at any point.
- Check the Lua stack once per value, for 3 extra slots.
- Restore the Lua stack and push nil on failure.
Problem:
- An invalidated `conceal_lines` mark conceals the line it collapses
onto.
- Treesitter: rechecking the last row of a `conceal_lines` mark deletes
it, and that row's query may not return the capture (`#offset!`, or
a capture shrunk by an edit). The rows stay unconcealed until a
redraw.
- `screenpos()` of a char fully hidden by extmark-conceal returns
`endcol` = `col - 1`.
- `screenpos()` of a char whose inline virt_text or TAB starts on a row
above reports that row, with `curscol`/`endcol` past the window
width.
- `winrestview({topline=N})` without "skipcol" keeps the skipcol of the
previous topline.
Solution:
- Skip invalid marks in `decor_row_has_mark()`.
- Query the rows of a deleted `conceal_lines` mark that the row's query
did not re-add.
- Report the cursor's row; clamp `col` to the row's first cell, and
`endcol` to `col`.
- Reset skipcol when "topline" changes, unless "skipcol" is given.
Problem:
- `vim.fs.abspath('fugitive:///a')` returns "<CWD>/fugitive:/a", so
filetype functions get a mangled path for URI buffers.
- `vim.fs.root()` doesn't accept "file://" URIs.
- No URI parser, so modules use ad-hoc scheme patterns, e.g.
`vim.ui.open('~/a:b')` treats the path as a URL.
Solution:
- Introduce `vim.uri.parse()`, and use it everywhere.
- DWIM: a one-letter "scheme" is a drive letter.
- `vim.fs.abspath()` returns URLs as-is.
- Expose `vim.uri` a module. Deprecate the old `vim.uri_xx()` names.
Problem:
Various stop paths skip clients still waiting for an "initialize" response.
- Quitting Nvim leaves their servers running/dangling.
- `vim.lsp.enable(name, false)` and `:lsp stop|restart|disable` don't
stop them.
- Extra `stop()` sends extra "shutdown" (instead of force-stopping as
documented).
- Restart scheduled before exit *starts a server during exit*.
- A late "initialize" response initializes a client that is already
being force-stopped...
- Force-stopping warns "Client X quit with exit code N".
Solution:
- Include `_uninitialized` clients in all stop paths.
- Extra `stop()` force-stops. The exit handler skips clients that are
already stopping, so `exit_timeout=false` is still respected.
- Skip a scheduled restart during Nvim exit.
- Ignore the "initialize" response of a stopping client.
- Don't warn about the exit status of a stopped server.
Problem:
The "exrc from cwd" tests leave the :terminal child running. clear()
hangs it up, and it writes ShaDa to Xstate while after_each() deletes
it, so the rename fails and logs E136:
WRN … getout:880: Exiting with error: "E136: Do not forget to remove
Xstate/nvim/shada/main.shada.tmp.a or rename it manually to
Xstate/nvim/shada/main.shada."
Solution:
Quit the child before clear().
Problem:
Deleting the last line (the cursor-line) with `'conceallevel' > 1` and
empty 'concealcursor', makes check_cursor_moved() check if the old
cursor-line is concealed.
Decoration provider "conceal_line" (ns=nvim.treesitter.highlighter):
Lua: …/treesitter/highlighter.lua:410: Index out of bounds
stack traceback:
[C]: in function 'nvim_buf_get_lines'
…/treesitter/highlighter.lua:410: in function 'on_range_impl'
…/treesitter/highlighter.lua:639: in function <…/treesitter/highlighter.lua:618>
Solution:
decor_conceal_line(): return false for a row past the end of the buffer.
Problem:
After a resize, a decoration provider's "on_start" calling line('w0')
makes curs_columns() scroll a window whose grid offsets are stale: it
shifts cells of another window and sends a wasted grid_scroll.
Solution:
Don't scroll a window that is due for a full redraw.
Problem:
With multicursor, cascade sets `v:count`, e.g. after `Qj` an Insert-mode
`<expr>` mapping sees `v:count=1` instead of 0.
Solution:
Save/restore `kCtxCount` in the replay sandbox.
Problem:
With 'list' and 'linebreak', a TAB after extmark-concealed text is drawn
as `>-` plus spaces instead of `>---…`: the linebreak code absorbs the
hidden cells before the TAB code sizes the tab.
Solution:
Count the hidden cells in the 'list' TAB width; let "Tab alignment"
absorb them.
Problem:
With ui2 `msg.targets='msg'`, a message (e.g. `print()`) from a mapping
resets `v:count`: the msg_show handler scrolls the msg window with
`:normal`, which sets `v:count`.
Solution:
Add `kCtxCount` to Context. Save/restore it around Lua UI event
handlers.
Problem:
A truncated shada file (crash, or conflicting Nvim sibling) fails with
E576 on every startup and exit, forever: the merge treats it as "not
a shada file" and refuses to replace it. And when main.shada is missing
or unreadable, or with `:wshada!`, shada is written in-place, not
atomically.
Solution:
- If a valid shada _header_ is found, we know it is a (damaged) shada
file, so we shouldn't just throw away the entire thing. Keep the
entries gathered so far and replace the file.
- Always write a temp file and rename it.
- Flush (and fsync) the temp file before renaming it. Check the final
flush.
Problem:
Each failed shada write leaves a `.tmp.X` file, which is never removed.
This is not uncommon on Windows: rename() fails if another process (AV,
indexer, another Nvim, …) has the file open. Once the `.tmp.a…z`
fallback names are exhausted, every write fails with E138.
Solution:
- Reuse the oldest `.tmp.X` when all names are taken.
- Windows: retry rename() briefly on EPERM/EBUSY.
Problem:
libuv timers count from the loop's cached "now", updated once per loop
iteration. libuv caches it to avoid a clock read per timer start
(historically a syscall on older kernels, etc.). Nvim runs Lua/Vimscript
in loop callbacks, so after blocking code a timer fires at the next loop
wakeup: thus e.g. `vim.system()` timeout kills the process immediately,
`jobstop()` sends SIGKILL before the 2s grace period, etc.
Solution:
Call `uv_update_time()` before starting these timers.
Problem:
`wait()` and `vim.wait()` return -1 without checking the condition again
if an event processed during the last poll exceed the timeout, though
the docs say the condition is evaluated on internal events. So -1 may
mean the condition is already true.
If we "document" this, we would essentially be documenting a race
condition.
Solution:
Check the condition once more after the last poll, like Python's
`Condition.wait_for()`.
Problem:
- Reflow is bolted-on beside the raw-column code: conceal-aware copies
of the motion, mouse and vcol walks, a third "layout column" kind,
parallel state (walkers, caches, flags), and rows that
redraw-and-rollback.
- Numerous bugs: wrong cursor/click/TAB positions, stale heights (Visual
area, skipcol), reads of freed marktree nodes, and every line on the
slow path if ANY conceal mark exists.
Solution:
- Change the existing code: one displayed->raw vcol map feeds the base
coladvance(), vcol2col() and nv_screengo(); one conceal_replacement()
rule serves :syntax, matchadd() and extmarks.
- Calculate, don't track: conceal is read from the marktree where it
is needed, and a column is either a raw or a displayed vcol.
- Eliminates `ConcealWalk`, `LinebreakState`, `ConcealOffState`,
`ConcealSizeState`, `conceal_cursor_win`, the provider-namespace
map, etc. Rows without conceal marks stay on the fast path.
- Flat control flow: a row ends at the line's displayed size, with no
"roll-back".
- One name per concept: "extmark-conceal", "displayed vcol", defined
once at `CharsizeArg.extconceal_off`.
- Invalidate what changed: when 'concealcursor' reveals or conceals
lines again, only those line-heights are invalidated, only in the
windows that reveal them.
- Treesitter: one row-hook parses each row before concealing it, so rows
not yet drawn are measured concealed. The redraw parse is synchronous
only when a window shows conceal.
- Tests: extend existing ones, table-driven, `screen:expect`.
PERF COST: a row's first conceal check parses and queries that row
alone (~28 µs per row in `:help`). One CTRL-F page in a 50-line window
takes 1.6 ms in options.txt (master: 0.3 ms) and 2.9 ms in lua.txt
(master: 0.45 ms). Markdown pays the same on master via
`_on_conceal_line`. Potential fixes:
- #36030: LanguageTree keeps injected trees across parse ranges (the
per-row parse cost).
- #41913: checks conceal a window-height of rows at a time (the per-row
query cost).
DESIGN NOTE: `_on_conceal` lets a row be measured concealed before it is
drawn (`G`, scrolling, nvim_win_text_height()), extending base's
`_on_conceal_line`.
- COSTS:
- One Lua call, parse and query per row.
- `_conceal_checked`, a cache that edits, tree changes and injection
changes must invalidate.
- The marktree changes during measuring and drawing.
- Conceal is written twice: ephemeral marks for drawing, stored marks
for layout.
- A per-buffer opt-in (nvim__buf_set_conceal_provider()), because
decoration providers are global.
GOAL(?): one range hook, `_on_conceal(win, buf, srow, erow)`, replacing
`_on_conceal_line` and `_on_conceal` (see #41913). After #36030 only
the query cost is left, which batching removes.
Problem:
- wait() never times out if each check returns in under 1 ms (e.g. the
condition closes a timer), because each pass truncates the elapsed time
to whole milliseconds.
- jobwait() with a timeout returns -1 for all jobs after the first one
that exits.
- CursorHold drops sub-millisecond waits, so frequent events postpone it.
- wait() leaks the condition result if it is a String, List, etc.
Solution:
- Compute the remaining time from a fixed start time.
- Track the CursorHold wait time in nanoseconds.
- Free the condition result.
Problem:
Tests may fail locally but pass on CI, because the test runner may use
the Nvim runtime installed on the system.
The test runner (`nvim -l test/runner.lua`) starts without $VIMRUNTIME,
so Nvim falls back to the runtime under the system install (e.g.
/usr/local/share/nvim/runtime). `runner.lua` sets $VIMRUNTIME, but only
after startup has built 'runtimepath', so the runner's own Lua (e.g.
`vim.glob` in glob_spec) loads stale modules from the system install.
Solution:
Set `$VIMRUNTIME` before starting the runner.
Problem:
After a deadly signal, or UI-disconnect during exit, a prompt waits for
a key that never arrives, so the server never exits. For example,
a multiline Lua error queued at exit opens "-- More --". The prompt's
input-wait continually runs libuv timers but not `main_loop.events`, so
a repeating timer that calls `vim.schedule()` queues callbacks (and Lua
refs) without bound (GB of memory...).
Solution:
During exit without a UI, or after a deadly signal, prompts don't wait:
- hit-enter and "-- More --" are skipped (as in headless-mode).
- confirm() returns its default button (same as without a UI).
- Other prompts (input(), getchar()) are interrupted as if CTRL-C was
typed. Like a typed CTRL-C this sets `got_int`, so code that prompts
_again_ (`while getchar() != 'q'`) terminates; remaining autocmds are
skipped.
Note:
- This also interrupts a prompt already waiting when the last UI
detaches.
Problem:
- Stale responses are checked three times: in the response handler, at
the start of process_response(), and in is_current().
- is_current() also compares highlighter and client_state identity,
because on_detach() drops the client state without clearing the active
request.
- process_response() takes `version`, which is already
`request.version`.
- merge_highlight_ranges() allocates a vim.Range for every token it
checks.
- No test covers a synchronous response whose token processing yields.
Solution:
- on_detach() cancels active requests, so `state[field] == request` also
covers detach.
- Drop the redundant checks and the `version` param.
- Compare positions directly in merge_highlight_ranges().
- Tests: set `_yield_interval_ns` so tests are deterministisc
(synchronous response that yields on every token).
Problem:
On macOS, resuming Nvim after a suspend under `fish -c` sometimes exits instead:
FAILED test/functional/terminal/buffer_spec.lua @ 696: :terminal buffer can resume suspended PTY process running in fish
Row 1 did not match.
Actual:
|*[Process exited 0] |
On macOS, `kill(0, SIGSTOP)` stops the members of the process group one
at a time. fish can see Nvim stop while fish itself is still running;
when the group resumes, fish exits instead of waiting for Nvim.
Solution:
If the parent is in our process group, stop it first.
Problem:
On Windows, a test that runs a Git `usr/bin` tool (`cat`, `diff`, …) sometimes stalls 15s and
then fails:
FAILED test/functional/vimscript/system_spec.lua @ 470: systemlist() passing string with linefeed characters as input splits the output on linefeed characters
Expected:
{ "abc", "def", "ghi" }
Actual:
{ ' 0 [main] cat (2736) C:\\Program Files\\Git\\usr\\bin\\cat.exe: *** fatal error - add_item ("\\??\\C:\\Program Files\\Git", "/", ...) failed, errno 1' }
The MSYS runtime sets up per-user shared state on first use. If a test kills an MSYS process
(CTRL-C) during that setup, the next MSYS process (in any parallel test group) waits 15s for
it, then sets it up again and dies.
Solution:
Keep an MSYS process (`sleep infinity`) alive for the test run, so the setup happens only once.
Problem:
On Windows, the server sometimes dies after the TUI client is killed:
FAILED test/functional/terminal/tui_spec.lua @ 303: TUI :detach detach! keeps UI attached, sets detach=true
retry() attempts: 149
test\client\uv_stream.lua:90: ENOENT
Analysis:
The server shares the client's console until rpc_close_event() swaps it
to a hidden console (os_detach_ui_console). If the :terminal closes the
console (on client exit) first, the server gets SIGHUP (CTRL_CLOSE_EVENT)
and exits.
Solution:
Delay the :terminal's job-exit handling (`vim.wait(fast_only=true)`)
until the server has dropped the UI.
Problem:
On Windows, the remote TUI client sometimes exits 0 instead of 42:
FAILED test/functional/terminal/tui_spec.lua @ 5179: TUI client exits when server quits with :cquit
Failed to match any screen lines.
Expected (anywhere): "Process exited 42"
Actual:
|[Process exited 0] |
Analysis:
Possibly the client drops the unread "error_exit" event when a write to
the closed pipe fails.
Solution:
Set `retries = 2`.
Problem:
screen_resize() at the pager flushes after repeat_message(), before
do_more_prompt() redraws. With synchronized output (:terminal mode
2026), that frame shows on its own: garbled top row, full "-- More --"
hint. Also on Windows, input can overtake the resize.
FAILED test/functional/terminal/tui_spec.lua @ 1241: TUI accepts resize while pager is active
test/functional/terminal/tui_spec.lua:1241: Row 1 did not match.
Expected:
|*{101:FAIL 1} |
|{101:FAIL 2} |
|{101:FAIL 3} |
|{101:FAIL 4} |
|{101:FAIL 5} |
|*{102:-- More --}^ |
|{5:-- TERMINAL --} |
Actual:
|* {101:IL 1} |
|{101:FAIL 2} |
|{101:FAIL 3} |
|{101:FAIL 4} |
|{101:FAIL 5} |
|*{102:-- More --^ SPACE/d/j: screen/page/line down, b/u/k}|
|{5:-- TERMINAL --} |
Solution:
- Don't flush in screen_resize() at the pager; do_more_prompt() will
redraw it.
- Test: wait for VimResized before sending more input.
Problem:
termkey treats ESC followed by a response (DCS/OSC/APC) as ALT + the
response, so the ESC is dropped. E.g. a slow reply to a remote UI's
UIEnter queries can land after a typed `<Esc>`, which then never leaves
Insert-mode.
Solution:
Emit the ESC as `<Esc>`, like for repeat/release events.
ref 03f5f25a
Problem:
The test assumes async parse() finishes within its first 3ms time slice.
On slow CI it doesn't, so parse() returns nil:
parser_spec.lua:603: attempt to index a nil value
Solution:
Wait for the on_parse callback.
Problem:
If a Progress handler changes the buffer during a slow `:write`, the
written file mixes old and new lines, or gets "???" lines (E315) if
lines were deleted. 89e915b67a only keeps 'modified'.
Solution:
Abort the write with E787 and restore the original file ('writebackup').
Problem:
When reading a file is slow (`:edit` of a huge log) there is no UI
feedback.
Solution:
- Similar to `:write` (eb25a1192a), emit a progress-message (with %)
if the file-read takes longer than `slow_io_ns()`.
- Lock the buffer while emitting, to prevent accidental buffer-unload by
a Progress handler.
- Tests: abuse `$NVIM_TEST` + `v:testing` to activate "all file IO is
considered slow", so tests can exercise while still being fast.
Problem:
If a (naughty) autocmd (e.g. a `Progress` handler) changes the buffer
during a slow `:write`, buf_write() still unsets 'modified', even though
the buffer has unwritten changes. `:bdelete` would silently lose the
changes.
Solution:
Reset 'modified' only if `b:changedtick` did not change while writing.
Problem:
- Every :write emits a progress-message #41222. Progress events fire and
terminals flash a progress spinner (OSC 9;4), even for a fast write.
- The "running" Progress event fires before `b_saving` is set, and the
"failed" one after it is cleared, so a Progress handler could unload
the buffer being written (use-after-free).
Solution:
- Emit a progress-message only if `:write` is slow (longer than 0.5s),
otherwise emit only a "bufwrite" msg-kind.
- Clear `b_saving` after reporting errors.
Note: a Progress handler can still change the buffer text during a slow
write: the file gets mixed content, and 'modified' is reset anyway (no
memory corruption though).
Problem:
3b8c19ea46 only hid the cursor around the "written" message.
Solution:
Hide the cursor from the start of the write (filename is shown) until it
finishes. Skip if filtering or :silent (no message).
Problem:
Autogenerating a title based on the "foo.txt" filename is not great. And
currently, the PDF result has no title because we aren't doing that for
the "print all" codepath.
Solution:
Add a validation check that ensures every doc has a title.
Drop the filename-based autogenerated title.
Work towards enabling ["flow" layout](https://github.com/neovim/neovim/blob/9244da12e400ec0291450a5e9400d995e7a29b83/src/gen/gen_help_html.lua#L86-L88)
in all `:help` files.
Problem:
The final boss of vimdoc pseudo-structure: Vim's quasi-headings on
function signatures and excmd docs is visually distinctive but too
inconsistent for a nice parse rule.
Solution:
- Define the quasi-heading format as an "h4" variant, specified at
`:help help-tag-heading`.
- For simplicity and predictability, it is defined as any line that
ends with a help tag(s) preceded by 2 or more spaces (or 1 tab).
- Update existing docs.
- This one-time update has a lot of changes, but is very unlikely to
conflict with ported Vim-patches because (1) Vim rarely adds new
*excmds* that we want, and (2) *function* docs are auto-generated.
ref https://github.com/neovim/tree-sitter-vimdoc/issues/110
Problem:
By default, 'termpastefilter' removes TAB chars when pasting to a :terminal.
Very annoying, and does not improve "security".
Solution:
Drop "HT" from the 'termpastefilter' default.
Problem:
No "spec" for various :help format (vimdoc) conventions, including the
new "table" support.
Solution:
Introduce `:help help-spec`.
Also, bump the vimdoc parser.
Problem:
- `<` before a listitem (`< - foo`) is not concealed.
- Codeblock HTML has extra blank lines.
- Codeblock in a listitem ends the listitem, so HTML docs drop the rest
of the item and its nesting. Example from `:h nvim_open_win()`:
```
• border: … the double-box style can be specified as: >
[ "╔", "═" ,"╗", "║", "╝", "═", "╚", "║" ].
<
If fewer than eight chars are given, they will be repeated.
• bufpos: …
tree-sitter-vimdoc v4.1.0:
(block
(line_li (prefix) (line) (codeblock (code (line)))))
(block
(line) ; "If fewer…"
(line_li (prefix) (line) (line))) ; "bufpos"
tree-sitter-vimdoc v4.2.0:
(block
(line_li (prefix) (line) (codeblock (code (line)))
(line)) ; "If fewer…"
(line_li (prefix) (line) (line))) ; "bufpos"
```
Solution:
- Bump tree-sitter-vimdoc to v4.2.0. https://github.com/neovim/tree-sitter-vimdoc/pull/174
- gen_help_html: get listitem indent from its first line
- gen_help_html: don't double codeblock newlines.
- highlight query: conceal `<` in `line_li`.
- docs: fix doc sources where a blank line was missing after listitems
followed by non-listitem text.
Problem:
- nvim_buf_call() aborts (assert) when called from an expression evaluated
for a window in another tabpage ('foldexpr', 'statusline'), because
`curwin` is not in `curtab`.
- After it returns, `curwin` is wrong if it switched windows.
- `vim._with{win=…, keepcwd=true}` does not undo `:lcd` if `win` is in
another tabpage.
Solution:
- Allow `curwin` outside `curtab`, and restore if it was outside
`curtab` at `ctx_switch()`.
- Find the target window in any tabpage when restoring its dirs.
Problem:
`nvim_set_option_value('fo', {'c','r','o'}, {operation='remove'})` does
nothing on "jcqrol": the items are joined into one `:set-=` value, which
only matches a contiguous substring. Removing map keys, e.g. {'eol','space'}
from 'listchars', never matches. `vim.opt:remove{…}` inherits this.
(Regression from f34ee3da80e8; 429338e74b fixed the case where the
flags are adjacent and in order.)
Solution:
Remove each item separately.
Problem:
If a job exits with cursor at last row, the `[Process exited]` message
covers the text of that row.
Solution:
Start a new line before showing the message, like the old
`\r\n[Process exited]` did.
Problem:
With 'ruler' in the cmdline ('laststatus=0') and ext_messages (ui2), a
'rulerformat' "%m" does not update the "[+]" indicator after ":write",
until a cursor-move.
Analysis:
`redraw_buf_status_later()` only marks windows with a statusline or
winbar. The symptom is hidden by the legacy message grid because the
write message overwrites the cmdline, which redraws the ruler.
Solution:
Also mark the cmdline 'ruler', like `status_redraw_buf()`.
Problem:
A Visual-mode mapping that reads input (`getchar()`) does not include
that input when cascading to other cursors (they only see ESC). OTOH,
Normal-mode mappings do replay the input.
Solution:
Append the payload to the Visual session's Lua-mapping subatom, and to the
session-ending command (`:<C-u>…` mapping).
Problem:
- `v:cascading` is false during the Insert-mode live-preview.
- `vim.on_key()` sees the internal keys of the Visual-selection preview
and "zq" steps.
Solution:
- Set `v:cascading` during the Insert-mode preview.
- Don't send internal keys to `on_key` callbacks.
- Use `v:cascading` as the source of truth for `mc_replaying()`.
Problem:
After `/\%Vfoo<CR>`, `zqn` places no cursors. `mc_zq_target()` clears
`'< '>` before `gn` (to detect a failed match), so `\%V` never matches.
Solution:
Keep the user's `'< '>` during `gn`; detect failure by its beep.
Problem:
With multicursors, an Insert-mode edit by a Lua/`<Cmd>` mapping or API
(nvim_put(), …) is live-previewed at the other cursors, but dropped at
ESC: the cascade replays _redo_, which does not contain such edits.
Solution:
When `<Cmd>`/Lua/RPC changes the buffer during Insert-mode, sync the
previews and restart the capture (like a non-captured cursor-move). The
edit stays as text.
- The mapping runs only at the primary. Its text is mirrored as-is
(similar to insert-completion). Re-running it per cursor would require
a "partial-remap" span replay, and breaks single-session plugins
(vim.snippet).
- Not during completion; redo already captures the pre-inserted
candidate on accept.
- Text typed before the edit is kept as mirrored, not replayed, so
per-cursor effects ('textwidth' wrapping, …) don't apply to it.
Known issues:
- "o"/"O"/"cc" with 'autoindent' followed by the mapping: the per-cursor
insert-entry replay is still pending, so the edit is lost.
Problem:
Handlers defer `show()` on unloaded buffers with a buffer-local BufRead
autocmd whose id is stored per-namespace:
- `noautocmd bwipeout` deletes the autocmd, so the next show/hide fails.
- Showing another buffer in the same namespace cancels the pending show.
Solution:
Handlers skip unloaded buffers. The existing global BufRead autocmd
shows cached diagnostics when the buffer loads.
Problem:
On Windows, a server started by :restart attaches to the TUI's console.
Closing the host-terminal kills the server, even after :detach or
:detach!: the swap to a hidden console only happens when a stdio UI
disconnects, but after :restart the TUI is on a socket.
Solution:
Track whether the console is inherited from the parent. When the last UI
disconnects and the server keeps running, swap an inherited console for
a hidden one.
Problem:
`nvim --embed --listen addr` always treats stdin as an RPC channel. If
stdin is /dev/null (e.g. backgrounded over ssh), the channel closes
before a UI can attach, so Nvim exits (detach=false behavior). If stdin
is a tty, keystrokes are read as RPC.
Solution:
With `--embed --listen`, open the stdio RPC channel only if stdin is
a pipe or socket. Else wait for a UI at the `--listen` address.
$ nvim --embed --listen ./nvim.sock
Nvim server (pid: 71984) waiting for UI, listening on: "./nvim.sock"
Note:
- No change when stdin is a pipe. So the test harness, TUI, and :restart
all behave exactly as before.
- The #29634 use-case of configuring a UI that attaches *after* startup
(a second UI, or reattach) still requires #25377 (reliable UIEnter).
Not affected by this change.
Problem:
Only the stdio channel can "own" a server (self-exit on close). After
:restart the TUI reconnects over a socket, so the restarted server does
not have "self-exit" behavior when the channel closes (e.g. by UI
disconnect, host-terminal close, etc.), which leaves a dangling server.
Solution:
- Introduce `nvim_chan_set`. (`nvim_set_client_info` doesn't fit:
exposing it to Lua would require a `channel` param, which is not
backwards-compatible, plus it's awkward because it doesn't merge info.)
- Broadcast per-channel properties in the "restart" UI event as an
opaque dict (a potentially useful pattern for other UI-events in the
future).
- UIs are expected to restore the channel properties on restart.
- `nvim_get_chan_info()` reports "detach".
- Remove `nvim__chan_set_detach()`.
Problem:
nvim_open_term() sizes the terminal from the current window, which may
have 0 rows (bordered float, 'cmdheight' taking all rows). libvterm
aborts when resizing from 0 rows.
Solution:
In `terminal_alloc()`, treat zero width/height as the pty default
(80x24), like jobstart(term=true).
Open question:
nvim_win_get_height() on such a float returns -1 (winheight() returns 0).
CID 657024: Error handling issues (CHECKED_RETURN)
/src/nvim/fileio.c: 2720 in rename_with_tmp()
>>> Calling "os_rename" without checking return value (as is done elsewhere 4 out of 5 times).
2720 os_rename(tempname, from);
If moving the file back fails, log where it is (the temp name).
Problem:
set_options_default() expands `$VAR` in the value but not the stored
default. set_init_expand_env() then expands the value (already
expanded), sees no change, and leaves the default unexpanded, so `:set`
lists 'helpfile' as changed.
This was accidentally hidden until 211623c1388d because env var depended
on 'isident', which was not initialized at set_options_default() time.
Solution:
set_init_expand_env() expands the default, and sets both from it.
Problem:
'isident' decides how env var names are parsed, which is more
"confusing" than "useful":
- Changing 'isident' (for 'define' or "\i") can break `$NAME` expansion.
- Its default byte ranges let multibyte names work on Windows (`$aあ`)
but not Unix (`$é`).
Solution:
Ignore 'isident' when parsing env var names.