Commit Graph

8622 Commits

Author SHA1 Message Date
Justin M. Keyes
9a29622b54 feat(multicursor): MC HAMMER #41587
Other (squashed) commits:

fix(tui): emit ui_send output atomically with the frame

Problem:
tui_ui_send() writes directly to the TTY, bypassing the output buffer.
Sequences sent via nvim_ui_send() (e.g. kitty multiple-cursors, or
visual-dot-repeat) always arrive in a separate TTY write from the frame
they were computed for. This manifests as "tearing", or e.g. in the case
of multicursor the terminal renders text with stale cursor overlays.

Solution:
- tui_ui_send(): while a frame is being assembled (pending invalid
  regions or buffered output), buffer instead of writing directly.
  - Out-of-frame sends (tty queries, clear-on-disable) still write
    immediately.
- mcursor.lua: emit the terminal-cursor update at the end of the redraw
  cycle (`on_end`, when screen positions are final) instead of
  vim.schedule().
2026-09-01 15:17:22 +00:00
Jaehwang Jung
ca992e82d4 fix(buffer): keep trailing slash significant in URI names #41577
Problem:
Literal path comparison ignored one trailing slash for every buffer name,
including URIs. Generic URI syntax does not make a non-empty path
equivalent to the same path with a trailing slash, so distinct URI
buffers collapsed into one.

Solution:
Require equal lengths when comparing URI buffer names, while retaining
trailing-separator normalization for filesystem paths.

AI-assisted
2026-09-01 01:04:57 +00:00
Justin M. Keyes
9d2a31b05e fix(cwd): flicker after temp context-switch #41561 #41582
Problem:
`shorten_fnames()` always redraws the statusline/tabline, even if no
buffer name changed. Since b296666e a temp context-switch
(`win_execute()`, `vim._with{win=}`) restores the CWD, so every such
switch flickers the message area.

Solution:
Redraw only if `shorten_buf_fname()` actually changed a name.
2026-08-31 20:49:50 -04:00
wrvsrx
c275b5de5a fix(lsp): separate adjacent nested folding ranges #41428
Problem:
Folding range markers are overwritten while ranges are evaluated. A range ending on a row can hide another range starting there, and multiple nested ranges ending together emit only the innermost ending level.

Solution:
Track starts and the number of ends per row before emitting markers. Prefer starts on shared boundary rows and use the outermost level when nested ranges end together.

AI-assisted
2026-08-31 08:41:19 -04:00
Aryan Pandey
ad42ee1c41 fix(cmdwin): handle UTF-8 characters containing 0x80 #41566
Problem:
Confirming cmdwin with a UTF-8 character containing 0x80 does not complete the
command.

Solution:
Escape K_SPECIAL bytes while feeding the cmdwin input after confirmation.
2026-08-31 06:52:20 -04:00
Justin M. Keyes
b542f3a8f8 fix(cmdatom): edit-repeat mapping #41564
Problem:
The "." example mapping at `:h edit-repeat` doesn't work well with
`nvim_feedkeys(…, 'mt', false)`.

Solution:
Use `vim.b[ev.buf].maxseq` instead of `undotree()`.
2026-08-30 23:38:34 +00:00
Justin M. Keyes
25f7c87a70 fix(mappings): replaying a deleted Lua mapping is UB
Problem:
Replaying a deleted Lua mapping, may call an arbitrary function.

RHS of a Lua mapping embeds its LuaRef (`<K_LUA><ref><CR>`). The raw
keys may outlive the ref (redobuff ".", CmdAtom `keys`). If the mapping
is deleted, replaying it either (1) dereferences a freed registry slot,
or (2) calls whatever callback reused the slot (autocmd, timer, other
mapping).

Solution:
Assign a monotonic (never recycled) id to Lua mappings and encode the
mapping keys as `<K_LUA><id><CR>`.

Note: in the case of Vimscript, a deleted function raises E117, but if
the function is redefined with the same name, the mapping will find it.

Alternatives?:
- Globally ensure `LuaRef` ids are not recycled.
  - Problem: could exhaust `int` in a long-lived Nvim session? Also,
    difficult to impl bc the "recycling" is done by `luaL_ref` itself.

ref: 5ac2e47acc
2026-08-31 00:16:01 +02:00
Justin M. Keyes
bc16be3cd9 fix(cmdatom): operator with Lua textobject is not type=operator
Problem:
An operator completed by a Lua `:omap` textobject emits
`CmdAtom.type="mapping"` (lhs-only, no keys) instead of `type="operator"`.

Analysis:
`atom_redo_set()` declined K_LUA, though the prepped redo
("op" + K_LUA + id + CR) is exactly what "." replays. A no-edit "g@"
emits nothing at all.

Solution:
- `atom_redo_set`: don't decline K_LUA; the redo route now captures the
  operator atom.
- `atom_capture_cmd`: don't early-return if the frame has prepped redo.
- op_function(): save/restore redobuff when invoking 'operatorfunc',
  like `call_user_func()` does for Vimscript. (Else the Lua callback
  may clobber the prepped "g@" redo / dot-repeat.)

fix #41482

TODO:
- async Lua (timer/vim.schedule) can still clobber the pending dot-repeat...
2026-08-31 00:16:01 +02:00
Nathan Zeng
39862231b2 fix(snippet): cancel session on ESC in insert-mode #41555
Problem:
If a snippet does not have a placeholder, we use insert mode instead of
select mode. From here <Esc> leaves the session and highlight active.

Solution:
Cancel the session on <Esc>.
2026-08-30 08:32:55 -04:00
Justin M. Keyes
eb18f29748 test(tui): unreliable "exits immediately when stdin is closed" #41547
Problem:
0c091cedc2 fixed the "immediate exit" but the 2s `nvim_get_proc()`
check still fails on slow (ASAN) CI.

Solution:
Check the pid after the `screen:expect`. Anyway, `assert_nolog` is the
"meaningful" part of the test since 0c091cedc2.
2026-08-30 07:13:02 -04:00
not_compiled
fbc5a769aa fix(ui2): don't draw MsgSeparator if cmdheight=0 #41531
Problem:
When 'cmdheight' is 0, the MsgSeparator can obscure the statusline when
a message is displayed, making its contents disappear until
the next redraw.

Solution:
Skip the separator when 'cmdheight=0' available.
2026-08-29 11:22:37 -04:00
Yanze Li
ab707262b1 fix(lsp): blank line before Content-Length breaks header parsing #41524
Problem:
A blank line before the Content-Length header makes header parsing
fail with "Content-Length not found in header". An LF in the 'name'
state falls through to the 'invalid' state, which only return to
'name' at the *next* LF. That LF terminates the next line, so the
line with Content-Length is swallowed.
The same issue happens when a junk line is a partial match of the
header name (e.g. "Cont\nContent-Length: ...").

Solution:
When seeing an LF in the 'name' state, reset the cursor and stay in
'name' rather than entering 'invalid'.
2026-08-29 09:13:04 -04:00
Justin M. Keyes
bdbd99ab10 test(tui): unreliable "TUI :detach % detaches other UIs" 2026-08-29 01:06:29 +02:00
Justin M. Keyes
2514256d95 refactor(input): exec stuffed keys eagerly
Problem:
The Vim "stuff" concept breaks the ability to reason about the call
stack and thus the boundaries of a `CmdAtom`: a stuffed translation ("x"
=> "dl") defers to the main loop. This "continuation" must be modeled in
`CmdAtom`, by checking global flags at undefined times, during undefined
circumstances.

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

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

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

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

Solution:
Detect the focus of `curtab` and abort closing the tab.
2026-08-28 08:39:16 -04:00
Justin M. Keyes
7e2e3f8c25 feat(editor): undo restores cursor position #41520
Problem:
Undo places the cursor wherever the cursor happened to sit at "save
time" (`uh_cursor` is sampled lazily on the first change).
Examples:
- `i` preserves, but `a` does not
- `diw`, `atest<Esc>`, `d^` abandon the original position
- `D`, `o` restore it (by accident).

Solution:
`composite` tracks the pending atom (and its `origin`) across frames.
A `stuffed` continuation frame inherits the `origin` + prepped redo.
Store `origin` info in the undo header, so undo can restore it.

- Not for a mid-command undo break (i_CTRL-G_u).
- Undoing a mapping restores where the mapping started (which
  technically may be different than where the "edit" started).
2026-08-27 16:12:11 -04:00
Justin M. Keyes
61df463c5e fix(cmdatom): insert-session entered by feedkeys() ignores typed input #41518
Problem:
An insert-session entered by a scheduled `feedkeys('i','n')` is
classified on entry (as "not typed"), and not reevaluated after that,
thus user input following it is not captured.

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

Solution:
Sample `maptick` (ticked by `gotchars()` on typed input, including
mappings) at session start; if it advanced by session end, the session
is user input.
2026-08-27 10:48:33 -04:00
Justin M. Keyes
02b0c80422 fix(lua): blast radius of broken _G.debug #41507
Problem:
`nlua_pcall()` references `_G.debug.traceback`. If user code deletes it
or breaks it some other way, various Lua features are broken.

    _G.debug = nil
    vim.schedule(function() end)
    vim.wait(100)

    E5113: Lua chunk: attempt to index a nil value
    stack traceback:
            [C]: in function 'loop_poll'
            [string "vim/_core/editor"]:176: in function 'wait'
            crash.lua:3: in main chunk
    PANIC: unprotected error in call to Lua API (attempt to index a nil value)

Solution:
Check `_G.debug.traceback` before using it as errfunc. If it's broken,
omit the traceback and say so in the error message.

Note: We could cache `_G.debug` in LUA_REGISTRYINDEX on startup, but
that would prevent plugins from providing custom functionality there
(and we happen to do so in `tui_spec.lua` for example).
2026-08-27 06:14:24 -04:00
KBS
0346958153 fix(terminal): truecolor SGR with colour space id #41491
libvterm reads the first three sub-parameters of an SGR 38:2 or 48:2 sequence
as R:G:B, so a colour space id shifts the channels: the empty slot becomes red
via CSI_ARG_MISSING truncating to 255, and green and blue move over one.

Skip the colour space id when the colon-separated group holds more than three
arguments. The group length comes from CSI_ARG_HAS_MORE rather than the raw
argument count, so a following semicolon-separated parameter is not consumed.
2026-08-26 10:10:57 -04:00
Stefan VanBuren
8d9a798d6d fix(man): don't grow a highlight into the previous line #41490
Problem:
With these two lines (`\b` marking a backspace):

    f\bfoo
    xb\bb

line 1's bold run ends at byte 1 and line 2's begins at byte 1, so line 1
renders "fo" in bold rather than "f", and line 2's "b" is not bold at all.

Solution:
Only grow a highlight group that is on the current row.
2026-08-26 09:09:17 -04:00
Justin M. Keyes
e7ae1b3c10 fix(ui2): cmdwin is not special #41508
Problem:
Unreliable ui2 test:

    FAILED   …/ui/messages2_spec.lua @ 277: messages2 multiline messages and pager
    …/ui/messages2_spec.lua:277: Row 1 did not match.
    Expected:
      ...
      |*{1::}echo "foo" | echo "bar\nbaz\n"->repeat(&lines)      |
      |*{1::}messages                                            |
      |*{1::}^                                                    |
      ...
    Actual:
      ...
      |*{9:vim.schedule callback: ...ork/neovim/neovim/runt [+7]}|

Solution:
ui2 is doing contortions to handle the old cmdwin behavior; stop doing
that, it's no longer necessary since b2bf7bcfb1.
2026-08-26 08:32:58 -04:00
zeertzjq
d393abad77 test: fix wrong order of args to eq(), neq() #41499 2026-08-26 07:18:22 -04:00
glepnir
f0146bcbe7 vim-patch:9.2.1009: duplicate dict code in ins_compl_dict_alloc() (#41497)
Problem:  ins_compl_dict_alloc() builds the same dict as
          fill_complete_info_dict().
Solution: Call fill_complete_info_dict() instead (glepnir).

closes: vim/vim#21140

303a153694
2026-08-26 08:37:44 +00:00
Justin M. Keyes
604bda445a test(harness): avoid overlong socket filename #41488
Problem:
Test sockets live under `$TMPDIR`, which the harness points at the build
dir.  On macOS/BSD `sockaddr_un.sun_path` is 104 bytes, and a CI build
path plus "nvim.<pid>.<n>" leaves little room:

    /Users/runner/work/neovim/neovim/build/Xtest_tmpdir_terminal/nvim.runner/aBcDeF/nvim.12345.0

Solution:
Point XDG_RUNTIME_DIR (`stdpath('run')`) at "/tmp/nvim_<pid>".  28 bytes:

    /tmp/nvim_19916/nvim.19919.1

TODO?:
- `TEMP_DIR_NAMES` prefers `$TMPDIR` over `/tmp`, so on macOS
  `stdpath('run')` defaults to the long `/var/folders/<xx>/<…>/T/` path
  instead of the short `/tmp` alias.

Note:
- The 104-byte limit applies to the `bind()` arg, not its "realpath",
  to, so `/tmp/…` symlinks can be used to workaround the limit.
2026-08-25 14:15:48 -04:00
Justin M. Keyes
aae43789d7 revert: "fix(lsp): do not expand $VAR in tagfunc filenames" #41489
revert c5cb0350a6
2026-08-25 18:09:02 +00:00
Justin M. Keyes
8adf6e769f Merge #41484 from justinmk/retrytest
test(harness): support `{retries=…}`
2026-08-25 11:05:16 -04:00
Justin M. Keyes
3aa323584e test(shell): unreliable "throttles shell-command output…" 2026-08-25 16:37:32 +02:00
Justin M. Keyes
03dfbe333c test(channel): unreliable "chansend sends lines…" 2026-08-25 16:34:21 +02:00
Justin M. Keyes
8ca36b9783 test(harness): support {retries=…}
Problem:
Cannot retry a test with its full `after_each`/`before_each` lifecycle.

Solution:
- Overload `it()` to accept an `opts` param:
  ```
  it('flaky', { retries = 2 }, function(ctx) end) -- 3 attempts.
  ```
- Pass `ctx` to test functions.
- Fix a bug in `t.read_file_list()`.
2026-08-25 16:34:21 +02:00
Justin M. Keyes
fb5d466a35 fix(help): :helptags on CRLF helpfiles
Problem:
The vimdoc parser does not treat "\r" as whitespace, so in a CRLF
helpfile the codeblock rule fails and "*" pairs inside examples become
tags. On Windows this generates duplicate "." and "/" tags:

    D:/a/neovim/neovim/build/bin/nvim.exe -u NONE -i NONE -e --headless -c "helptags ++t doc" -c "exe 'cquit' !empty(v:errmsg)""
    Error in command line:
    E154: Duplicate tag "." in gui.txt and repeat.txt
    E154: Duplicate tag "/" in pattern.txt and usr_08.txt
    E154: Duplicate tag "/" in usr_08.txt and pattern.txt

Note: The old C parser was unaffected because it read helpfiles in text
mode (`os_fopen(…, "r")`).

Solution:
Strip "\r" before parsing.
2026-08-25 13:36:01 +02:00
Justin M. Keyes
1ef030f162 fix(help): :helptags regressions
Problem:
Parent commit regressed some behavior of the old C helptags-gen impl:
- helpfiles in sub-directories are named by basename, so :help fails
- "help-tags" always names "tags", never "tags-nl"
- E150 E151 E152 E153 are never reported
- existing tags file is not overwritten if no tags were found
- a duplicate tag aborts the run, skipping the remaining directories
- `*.TXT` and `*.FRX` (uppercase) are not recognized as helpfiles
- tree-sitter-vimdoc accepts tags that the C parser rejected: `*a|b*`
  (breaks |links|) and unterminated `*tag`
- PUC Lua "<" compares with (localized) `strcoll()`, so the tags file is
  not sorted by byte value (E432)
- requiring `vim.treesitter` at load time breaks :help itself, not just
  :helptags, where the module is unavailable
- `vim.pack` runs :helptags for plugins that have no "doc/" directory,
  so every install/update emits E150

Solution:
- Fail the build if generating helptags reports any `v:errmsg`.
- Restore old behavior: tags generated for runtime/doc are now identical
  to those from the C implementation. Errors are non-fatal messages
  instead of exceptions, so all directories are still processed.
- Load treesitter lazily, report a plain error if parser is missing.
2026-08-25 13:32:25 +02:00
Yochem van Rosmalen
b36b3d7f3a feat(help): generate :helptags using Treesitter
Problem:
Tags are manually parsed in C which is not flexible and prone to errors.
Extending the help system to allow for other formats (e.g. Markdown)
would require a large rewrite in the C core, while with Treesitter it
only needs a query update.

Solution:
Use the power of treesitter to extract the tags from helpfiles.

- build: set `$VIMRUNTIME` when generating helptags, like
  `cmake/Util.cmake` already does for other generators.
- fix(help): only accept tags delimited by whitespace. The old C parser
  only accepted a `*tag*` preceded by start-of-line or whitespace and
  followed by whitespace or end-of-line. The vimdoc parser also captures
  tags followed by other text, e.g. `*$XDG_STATE_HOME*/.../logs` in
  starting.txt, which caused an E154 duplicate tag error for docs that
  were previously fine.
2026-08-25 10:52:12 +02:00
Justin M. Keyes
a1de07418b feat(ui2): lift ui2 options into 'messagesopt' (part 1) #41474
Problem:
In order for ui2 to graduate to the main "messages ui" its configuration
needs to graduate into actual options.

Solution:
Migrate some of its config to 'messagesopt':
- "maxheight" (note: currently this is an integer treated as
  a "percentage"; if we want to support a row count we could allow
  values with units, like `"42%"`)
- "pager"
- "timeout"

Also improves error messages:

    messagesopt=hit-enter,history:500,bogus       E474: Unknown item 'bogus'
    messagesopt=hit-enter,history:500,progress:x  E474: 'progress' must be one of: , c
    messagesopt=hit-enter,history:abc             E474: 'history' requires a number
2026-08-25 03:58:17 -04:00
Justin M. Keyes
510f61555b fix(ui2): pager_char does not match keycode aliases #41468
Problem:
`pager_char` compares the `keytrans()` result exactly. E.g. `pager_char
= "<cr>"` does not work, it expects `<CR>` (uppercase).

Solution:
Normalize `pager_char` in enable().
2026-08-24 15:01:50 -04:00
Étienne Robert
d1b811b6c2 fix(lsp): advance inline completion range over matching input #41434
Problem:
`Completor:accept()` applies the item against the range the server
answered with. Characters typed after the request but before accepting
lie inside neither that range nor the replacement, so they are left
behind by the insertion: typing `a` after `foob` and accepting `foobar`
within the 200 ms debounce yields `foobara`.

Solution:
Grow the item's range over the characters typed since the request, so
that `accept()` replaces them. Only while the candidate still matches,
decided by the longest common prefix `show()` already computes, so input
that contradicts it is left alone. Snippet items are unaffected, since
`accept()` ignores the range for them.
2026-08-24 10:51:39 -04:00
Justin M. Keyes
8c440c469b fix(ui2): ui2 misinterprets getchar() input as pager_char (CR) #41465
Problem:
`vim.on_key()` callbacks are invoked for keys that `getchar()` consumes,
which never reach the main loop, and there is no way to tell such a key
apart.  With ui2 the `on_key` handler activated after interactive
cmdline, handles it as `pager_char` (thus enters the ui2 pager).

Solution:
Skip `vim.on_key()` callbacks during `getchar()`/`getcharstr()`.
2026-08-24 10:18:47 -04:00
Étienne Robert
143bbfbb12 fix(lsp): clamp inline completion range to the line on accept #41435
Problem:
`Completor:accept()` hands the item's range straight to
`nvim_buf_set_text()`. That range is resolved when the response arrives,
so text deleted before accepting (backspacing within the 200 ms
debounce) leaves the range end past what is left of the line, and the
accept aborts with `Invalid 'end_col': out of range`.

18d6436 fixed the same staleness in `Completor:show()`, which drops an
item once its range *start* falls outside the buffer. That guard does
not cover the range end, and `accept()` was never given one, so this is
a hole left by that fix rather than a regression of it.

Solution:
Clamp the end of the range to the end of the line before writing. The
start needs no clamp: `show()` dropped the item one event loop tick
earlier if it had gone out of range.
2026-08-24 08:41:57 -04:00
glepnir
2a382ffb61 fix(lsp): completion overwrites text before the word boundary #41463
Problem:
One item with an edit range before the word boundary changes the start
column for all items. For tsserver `str.`, completing `str.char` can
become `strcharAt` and break filtering.

Solution:
Prepend the missing text and filter on it.
2026-08-24 06:14:49 -04:00
Justin M. Keyes
eeefe7ca65 fix(cwd): E812 when a message opens a window during a file read #41458
Problem:
When ui2 is enabled, opening an already-open file in another Neovim
instance results in the error `E812: Autocommands changed buffer…`.

Analysis:
On E812 the file is not loaded.  The default SwapExists handler notifies
`W325: Ignoring swapfile…`, and ui2 shows that message by opening
a window, which is a temp context switch.  `ctx_dirs_restore()`
re-shortens every buffer name on the way out, and `shorten_buf_fname()`
always frees and reallocates `b_sfname`.  `readfile()` aliases `b_fname`
across `check_need_swap()` and compares the pointer to detect a rename.

Regression by b296666e41, which replaced the `cs_save_sfname` restore
(that kept curbuf's pointer) with `shorten_fnames(true)`.

Solution:
Keep the allocation in `shorten_buf_fname()` when the short name is
unchanged.  Pointer stability is what the E200/E201/E812 guards actually
assert.
2026-08-23 19:32:07 -04:00
not_compiled
716835c232 fix(ui2): dismiss msgs after mapped motions #41450
Problem:
Mapped keys can produce on_key callbacks with an empty `typed` value.
so mapped motions such as `j -> gj` are not recognized as typed input
and do not dismiss messages

Solution:
Track whether an empty `typed` callback follows typed input, allowing
mapped motions to dismiss messages like directly typed motions.
2026-08-23 19:09:26 -04:00
Justin M. Keyes
69bf8c7792 fix(cmdatom): terminal-mode keys leak into mapping lhs #41455
Problem:
A mapping that enters terminal-mode (`:FzfLua files` via `:startinsert`)
never reaches a composite-end: terminal-mode runs no "normal" CmdFrames.
The composite collects the entire terminal session (and more) into `lhs`.

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

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

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

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

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

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

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

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

Notes:
- atom_cmd_end(): a frame ending with stuff pending re-points its
  redo-prep to the next frame.
- atom_cmd_start(): a stuffed continuation frame (`KeyStuffed`) keeps
  the redo-prep; flushed stuff discards it.
2026-08-23 13:58:45 -04:00
Shubh
c5cb0350a6 fix(lsp): do not expand $VAR in tagfunc filenames #41398
Problem:
CTRL-] via lsp tagfunc passes URI paths through :tag, which expands
\$VAR, so files like people_.\$personId.tsx fail with E429.

Solution:
fnameescape() the filename in tag items so \$ is treated as literal.
2026-08-23 11:30:17 -04:00
Shubh
bca9c4afa7 fix(man): allow :Man to open by absolute path #41401
Problem:
man -w echoes any existent file back unchanged. :Man rejected all such
paths, so :Man /usr/share/man/man1/bash.1 failed even for real man pages.

Solution:
Accept an echoed path when it looks like a man page (.../man1/foo.1).
2026-08-23 11:28:22 -04:00
Olivia Kinnear
8ce1aac477 feat(pos): mutable vim.Pos, vim.Range fields #41318
Allow plugins to set the human-readable fields.
2026-08-23 07:13:41 -04:00
Justin M. Keyes
b296666e41 fix(cwd): stale buffer names after temp context-switch #41433
Problem:
1. `ctx_dirs_restore()` is the only chdir site that doesn't re-shorten
   buffer names; `post_chdir()`, `update_cwd()` and `do_autochdir()` all
   call `shorten_fnames(true)`.  So after a temp window-context switch
   that moved the CWD, every buffer's `b_fname` is still relative to the
   other directory.  ui2 renders messages in a float, and entering it is
   such a switch, so with a `:bcd` in a `BufReadPost` handler 'statusline'
   "%f" shows ".config/nvim/init.lua" while the CWD is already
   `~/.config/nvim`, and :write resolves the name against it:
   ```
   E212: Can't open file for writing: no such file or directory
   ```
2. `msg_multihl()` leaves `msg_ext_id` pointing at the caller's storage
   when nothing was emitted (e.g. 'msg_silent'): the reset only ran on
   a flush that produced chunks.  The next message then ships a dead
   stack frame as its msg_show id.  After ":silent write" the id is
   buf_write()'s `msg_id[MAXPATHL + 32]`:
   ```
   id = "\0\0\0\0\0\0\0\0\29\0(<C6>k\24R\17p<C7><C7><C7>\1\0\0\0..."
   ```

Solution:
1. `shorten_fnames(true)` after restoring the CWD.  Drop `cs_save_sfname`,
   which was a partial workaround for the same bug.
2. Release the id in `msg_multihl()`, where it is set and the caller's
   frame is still alive. Stop reading `.data.integer` out of a String
   union member.
2026-08-22 16:24:05 -04:00
Freddie Haddad
9b0bc7edef fix(plines): 'linebreak' ignores inline virtual text width #41227
Problem:
With 'linebreak', a word is moved to the next screen line when it does
not fit on the current one. The check measures only the buffer text, so
inline virtual text anchored inside the word is not counted. The word is
kept on a line it cannot fit, and the virtual text is split across the
screen line boundary.

Solution:
Count the width of inline virtual text anchored within the word, so the
check uses the width that is actually displayed.
2026-08-22 19:48:16 +00:00
Justin M. Keyes
7037e1effe fix(write): :write can target a garbage filename #41432
Problem:
`buf_write()` captures the buffer's `fname`/`sfname`/`ffname`, then emits its
progress-message before opening the file. This may run user code
synchronously: the `Progress` autocmd, and the `msg_show` handler of an
in-process UI (ui2). Either can change the CWD, and `shorten_fnames()`
then frees/reallocs every buffer's short name. The rest of `buf_write()`
reads the freed name...

    "foldtext()" [New] 41L, 997B written
    E212: Can't open file for writing: illegal byte sequence

ASAN, with ui2 enabled and a `BufEnter` handler that runs `:lcd`:

    READ  path_skip_sep <- path_tail <- match_file_list <- buf_write
    FREE  shorten_buf_fname <- shorten_fnames <- update_cwd <- set_curbuf
          <- win_set_buf <- nvim_open_win <- ui2 msg_show handler
          <- ui_call_msg_show <- msg_ext_ui_flush <- buf_write

Solution:
Copy the names after the `*Pre` autocmds.

Note: `readfile()` has the same shape, but its messages pass no
progress-id, so they skip `msg_progress()`. Safe, for now...
2026-08-22 15:07:45 -04:00
Justin M. Keyes
47cd769ed5 feat(cmdatom): mappings capture continuation
Problem:
A mapping that ends mid-operation (`nnoremap ,D d`) emits a content-free
"mapping" atom plus a `pending` field, and the "continuation" motion
arrives as a sibling atom. Consumers must stitch the two together (which
has broken cases, e.g. Insert-opening mappings (",i") lose their session
entirely).

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

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

before/after:

    INPUT       BEFORE                             AFTER
    ---------------------------------------------------------------------
    ,D w        {mapping lhs=,D pending=operator}  {operator lhs=,Dw keys=dw}
                + {operator keys=dw}
    ysiw"       {mapping lhs=ys pending=operator}  {operator lhs=ysiw" keys=g@iw"}
                + {operator lhs=g@iw"}
    ,v d        {mapping pending=visual}           {visual lhs=,vd keys=viwd}
                + {visual lhs=viwd}
    ,i XY<Esc>  {normal keys=i lhs=,iXY<Esc>}      {insert keys=1iXY<Esc> text=XY}
2026-08-22 17:29:34 +02:00