Problem:
A Visual-mode mapping that leaves Visual mode ("xmap I Q0i") does not
replay its motions/edits at the extra cursors, though an insert session
it starts does.
atom_map_start() skips while Visual is active, so the mapping has no
`composite`, and atom_capturable() then rejects its commands.
Solution:
Open the `composite` for Visual-mode mappings too.
Bonus: the mapping atom is now reported as:
before: type=visual, keys="viwcFOO<Esc>"
after: type=mapping, keys="viwc<Esc>iFOO<Esc>", lhs=",c"
Problem:
TSHighlighter._on_conceal_line() parses with the range { row, row }. A
Range2 has an exclusive end, so that is the empty range, and no injected
region ever intercepts it.
The root tree is parsed regardless, because its region is empty, so only
injections are affected: conceal_lines metadata coming from an injected
language's highlights query is dropped. on_range_impl() then records the
row in _conceal_checked, so the miss persists until the buffer changes.
For a markdown code block nested in a markdown code block, the inner
fence delimiters stay visible and nvim_win_text_height() reports 5 rows
where 3 are displayed.
Solution:
Pass the one-row range, as the on_range_impl() call below already does.
AI-assisted
Problem:
A Visual selection opened programmatically (`:normal! viw`) is not
previewed at the other cursors, and a user-typed operator completing it
does not cascade.
":normal" keys run as child frames, which are never user input, so the
session they build was classified "fed" once and never revisited.
Solution:
Recalculate the session kind (`vatom.state`) on every frame. Record
`vatom.frame` (the last frame to add to the session) so an enclosing
frame does not reset or recapture it.
Problem:
cmd_on_key() tests the key it was handed against the literal string
'<MouseMove>', but at that point "typed" still holds raw key bytes. Thus
the comparison never holds and the branch its own comment describes
is dead. Moving the mouse over an expanded cmdline collapses it.
Solution:
Translate the key once, up front, so every comparison in the function
sees the same form.
AI-assisted
Problem:
The #has-parent? predicate indexes the result of node:parent() without
checking it, so a capture that matches a tree's root node raises
query.lua:600: attempt to index a nil value
instead of simply not matching. In a highlights query that breaks
highlighting for the whole buffer. The sibling #has-ancestor? predicate
handles the same situation.
Solution:
Treat a missing parent as "does not match".
AI-assisted
Problem:
`getdigits_int(pp, strict, def)` returns `def` only when the number
fails to parse or overflows, never when the text parses to a literal
`0`, so `%.0` leaves `maxwid` at 0. Both render paths then break down:
`l > maxwid` is always true and `while (l >= maxwid)` consumes the whole
string, so `%.0f` comes out as `<` and `%.0l` as `0>3`.
Vim guards for this right after the same call. Nvim had the guard too
until 3344cffe7b ("getdigits: introduce `strict`, `def` parameters"),
which folded it into the new `def` argument and so changed what a
literal zero means. The neighbouring `minwid` call was translated
correctly.
Solution:
Fall back to 50 when the parsed width is not positive, as Vim does.
AI-assisted
Problem:
Cursors placed same-line before the primary, or producing multiline
edits on lines above the primary, shift the text in a way that breaks
the insert-mode live-mirroring.
Solution:
Skip live-mirroring under those conditions.
TODO (future): we could support live-mirroring if ins-completion's
internal bookkeeping tracked its regions more precisely.
Problem:
`get_scrolloffpad_value()` takes a window, but its else branch reads
`curwin->w_p_sop` instead of `wp->w_p_sop`.
Reachable from Vimscript, since `line('w0', winid)` runs
`update_topline()` on another window without making it current.
Solution:
Read the option from the window that was passed in.
AI-assisted
Problem:
Freeing an unrelated buffer deletes the cursor under the primary.
Scratch-buffer cleanup triggers this unexpectedly.
Solution:
Separate dead-cursor cleanup from dedupe/merge. Merge overlapping
cursors only at cascade boundaries, not during buffer/namespace cleanup
or cursor removal.
Problem:
Input (`getchar()`, `input()`, …) consumed by an expr mapping EVALUATION
can leak into `CmdAtom.keys`, depending on count placement ("2dse" vs
"d2se").
Solution:
Skip payload capture while `expr_map_lock` is active.
- Input read during expr EVALUATION is recorded in `CmdAtom.lhs`.
- Only input read by mapping EXECUTION is appended to `CmdAtom.keys`.
Problem:
Pending mapping atoms prevent live insert-cascading (`nnoremap i ^i`).
Flushing them at insert-session start (3a02a39957) adds a separate
cascade boundary, which muddles the architecture.
Solution:
Let the first insert-span cascade pending mapping atoms on insert-entry.
This is an alternative fix for #41692.
Composite children (`.atoms`) BEFORE:
{ { 'motion', '^' }, { 'insert', '1iX<Esc>' } }
Composite children (`.atoms`) AFTER:
{ { 'motion', '^' }, { 'insert', '1i<Esc>' }, { 'insert', 'iX<Esc>' } }
Problem:
Both handle lookups write into the same `Error` before either one is
checked, and `api_set_error()` unconditionally `xmalloc`s `err->msg`
without freeing what is already there. When both handles are invalid,
the message allocated for the window is silently overwritten by the one
for the buffer and leaked.
Solution:
Check the window before looking up the buffer. As a side effect the
reported error no longer names the buffer when the window was wrong too.
AI-assisted
Problem:
A Normal-mode mapping that moves before entering Insert, does not
live-mirror. Its pending motion defers insert-cascade until Insert ends.
Solution:
Cascade pending atoms before the Insert entry replay, then start the
live insert span.
Problem:
The "spill" indicator that ui2 appends when messages overflow the
available height is drawn with whatever highlight the message tail
happens to have, so it is indistinguishable from the message text.
Solution:
Give the [+x] chunks an explicit `MoreMsg` highlight, and only fall back
to the message tail highlight for chunks that don't carry one of their
own.
AI-assisted
Problem:
Changing from an empty-line cursor after a multicursor jump can crash
while flushing a deferred clipboard update. Exact context restore can
leave an omitted register with a null array and stale non-zero size.
Solution:
Make free_register() fully reset the register after freeing its contents,
so it always leaves a valid empty register. Add a regression test.
Signed-off-by: sami <samiulsami7786@gmail.com>
Problem:
`unpack_string()` validates the declared length against `*size`, the
size *before* `mpack_rtoken()` consumed the token header, rather than
`size2`, the remainder after it. The header is one to five bytes, so any
declared length in the window `(size2, *size]` slips through. The
returned `String` then covers up to five bytes past the end of the
buffer, and `size2 - tok.length` underflows, leaving `*size` near
`SIZE_MAX` so every later unpack call on that entry believes it has an
unbounded buffer.
Reachable from ShaDa, where a history entry ending in a five-byte string
header followed by four bytes is enough, so a corrupted or hostile
`main.shada` triggers it at startup.
Solution:
Check the remainder left after the header.
AI-assisted
Problem:
The early return for an invalid window handle bypasses the
`set_destroy()` at the end of the block, so the set's backing allocation
leaks whenever an earlier iteration already called `set_put()`.
Triggered by `{ wins = { valid_win, 9999 } }`.
Solution:
Destroy the set before returning.
The new test passes either way, since the leak is only visible to a
sanitizer; it is there so the ASAN job covers the path.
AI-assisted
Problem:
`find_window_by_handle()` returns NULL only after `VALIDATE_INT` has
already set `err`, so the `api_set_error()` that followed allocated a
second message over the first pointer and leaked it.
Solution:
Drop the redundant call and keep the helper's message, which is already
set and better worded.
AI-assisted
Problem:
The ui2 dialog implements paging for the arrow keys, Home/End and the
page keys, but not for the mouse wheel. When `mouse` contains `"c"`,
turning the wheel does nothing at all.
Solution:
Handle <ScrollWheelUp>/<ScrollWheelDown>, scrolling by the `mousescroll`
`"ver"` amount.
AI-assisted
Problem:
`menu_mode_chars` includes the two-character `"tl"` value. The hardcoded
`1` is the `key_len` argument of `tv_dict_add_dict()`, which truncated
it to `"t"`, the designator for a tooltip. Scripts keying on the
documented mode designators therefore miss terminal mode entries and
misread them as tooltips.
Solution:
Pass the real length of the designator.
AI-assisted
Problem:
`REMAP_NONE` is -1 and `REMAP_SCRIPT` is -2. These are enumerated
values, not bit flags, so `&` is the wrong operator: `x & -1` is `x`,
and `-1 & -2` is -2, which is truthy. Both kinds of non-remapping menu
therefore reported `noremap` and `sid` as 1 and became
indistinguishable.
The sibling `menu_get_info()` already compares with `==`.
Solution:
Compare rather than mask.
The expectations in menu_spec.lua had the
conflated values baked in, so 22 entries from plain `*noremenu` commands
now report `sid = 0`, and the two from `nnoremenu <script> Export.Script`
now report `noremap = 0`.
AI-assisted
Problem:
`]C` jumps the primary onto another cursor, which dedupes at the next
edit. Every `]C` consumes a cursor.
Solution:
`]C` adds a cursor at the current position before jumping. This
effectively "rotates" the primary cursor.
Problem:
A non-edit mapping that moves the cursor by API (Lua,
`:call nvim_win_set_cursor()`, …) is not cascaded.
Solution:
Fallback to LHS-replay if the cursor moved and follow-mode is enabled.
Problem:
`scroll_cursor_halfway()` resets its `above` and `below` row counters on
every iteration of the `topline` scan, because they are declared inside
the loop. Each iteration then takes one line from each side regardless
of how many screen rows those lines occupy, so tall wrapped lines below
the cursor leave it near the top of the window instead of centered.
Affects the default `nosmoothscroll` path, reached from "zz" and from
`update_topline()`. Regression from 9b9ccac625 (vim-patch:9.0.1121),
which moved the declarations to their first use while porting; the
upstream patch did not touch them.
Solution:
Declare the counters at function scope, as Vim does, so they accumulate
across iterations.
Problem:
"v{motion}<Esc>" moves the primary cursor to the selection end, but the
other cursors snap back to their anchors unless follow-mode ("q=") is
on. The selection preview showed them at the selection end.
Solution:
Replay the abandoned selection at every cursor, not only in follow-mode.
An abandoned typed selection now always emits a "visual" CmdAtom.
Problem: Nvim has many Lua APIs that start callback-driven work: timers,
jobs, libuv handles, and other event-loop tasks. Callers that need to
sequence or cancel that work have to build their own coroutine wrappers,
task bookkeeping, and cleanup rules. This makes async control flow hard
to share, test, and document.
Solution: Add `vim.async`, a structured-concurrency module vendored from
async.nvim. It provides task handles, await/pawait helpers,
sleep/timeout helpers, completion-order iteration, and semaphores on top
of Nvim's event loop.
The API follows the same broad model as Trio: async work has an owner,
tasks are awaited explicitly, and cancellation is cooperative. Include
generated vimdoc with an introductory overview and examples, a news
entry, and functional tests for the new module.
AI-assisted
Problem:
'guicursor' is not respected when extra cursors exist.
Per-cursor selection toggles Visual off, and the sandbox restores it
only after restore_current_state(), whose ui_cursor_shape() caches the
Normal shape.
Solution:
Restore Visual before restore_current_state().
Problem: there should be exactly one cell of padding between sections,
and exactly one cell of minimum padding between the left and the right.
- Spaces between sections waste space when a section is empty.
According to a comment in #33036, this was the reason to avoid `%k`
and implement the keymap section with a vim expression, but other
sections still have this problem.
- The diagnostics section wastes space because it is not entirely empty
when there are diagnostics in another buffer.
- The terminal exit code section can touch the right side, e.g. the
ruler, even though it belongs to the left side.
Solution:
- Use auto-hiding item groups (`%(` without width fields) to get rid of
unneeded spaces when a section shows no information.
This simplifies the 'showcmd' and 'keymap' sections in particular.
- As a slight simplification, `term_exitcode` is moved into the flags
section since it is formatted with square brackets like a flag.
- Count the diagnostics for the current buffer specifically.
- Ensure at least one cell of padding between the left and the right
side by adding a space next to the separator `%=`.
Problem: some sections are implemented with `%{%`, even though
reevaluation of the expression result is not needed.
This leads to otherwise needless %-escaping in `progress_status`.
Solution: use `%{` instead.
Problem: `%{` and `%{%` (without items) replace spaces with fillchars.
This looks out-of-place inside the terminal exit code section, and in
contrast to all other sections, the 'busy' section is surrounded by
fillchars, which looks inconsistent, and with some terminal-font
combinations, ◐ overlaps the fillchar, e.g. Alacritty & JetBrains Mono.
Solution: use non-breaking spaces U+202F to avoid fillchar substitution.
Problem: sections that appear/disappear frequently can make otherwise
more stable sections jump around a lot.
Solution: sort the sections on the right roughly by volatility:
'showcmd' in first place, 'keymap' next to the ruler.
Problem:
`shada_read()` rebuilds `v:oldfiles` for any "forced" read, which first
*clears* the list. For an in-memory caller such as multicursor, the list
is never repopulated. So `v:oldfiles` is empty after `Q` + edit.
Solution:
Rebuild `v:oldfiles` only when `kShaDaGetOldfiles` is requested.
`shada_read_everything()` (`:rshada[!]`, startup) always requests it.
old bug from 411a06c8b6
Problem:
During 'autocomplete', a completion-ending `<BS>` restarts completion
immediately. Multicursor insert-cascade skips replay (`edit()` refuses
to nest if compl/pum is active), so the mcursors do not update until
ESC.
Solution:
Flush pending keys in `TRIGGER_AUTOCOMPLETE`, just before autocomplete
restarts again.
Problem:
Insert entered by a Visual-mode operator mapping (`xnoremap c c`) does
not live-mirror at cursors.
Solution:
Decide `typed` from the Visual session's own provenance (`vatom`, which
spans the selection and knows whether it was user input) instead of only
negating it.
Problem:
`parse_ssh_config()` compares tables against a freshly allocated empty
table.
- In `parse_multiple_values()`, the guard which avoids flushing an empty
accumulator never applies. Runs of separators and trailing whitespace
push empty strings into the results, and `is_valid()` does not filter
them. `Host alpha beta ` parses as `{ 'alpha', '', 'beta' }`.
- In `parse_value()`, the condition reduces to `chr == '"' and quoted`.
`quoted` starts false and only that branch sets it, so it can never
become true: quotes are never recognised and are inserted literally,
and the unterminated-quote check is unreachable.
Solution:
Compare `#val` instead. Add a test for repeated and trailing separators.
AI-assisted
Problem: Drawing 'statuscolumn' leads to a heap-buffer-overflow if a
sign/number column comes after many items.
Solution: Avoid curitem >= stl_items_len when writing to stl_items.
Problem:
`IterArray:take()` iterates up to `self._tail`, but `_tail` is
exclusive. When no element fails the predicate, the loop reads one index
past the last element and calls the predicate with `nil`.
Solution:
Stop at `self._tail - inc`, which is the last in-range index for both
iteration directions. Add tests using a predicate which dereferences its
argument and matches every element, forward and reversed.
AI-assisted
Problem: Cursor correction can move the cursor up at the end of the
buffer when 'scrolloffpad' is enabled (gx089).
Solution: Allow missing context below EOF in cursor_correct(). Add
regression tests for CTRL-D and CTRL-E (Seunghee Kim).
fixes: vim/vim#21096
closes: vim/vim#211886a3db67a52
Co-authored-by: SeungheeKim <ksh368@naver.com>
Problem:
With follow-mode ("q="), an 'operatorfunc' that only moves the cursor
does not cascade.
Solution:
Update the `effect` condition to include cursor-moves if follow-mode is
active.
Problem:
With mcursors and 'autocomplete', a non-literal key (`<BS>`) during an
active completion attempts to insert-cascade, which attempts a nested
`edit()`, which refuses with E565.
Solution:
Defer the flush while completion is active; the pending keys will be
handled later.
Problem:
An atom queued in buffer A cascades on B's cursors if the mapping ends
in B ("nnoremap X x:bnext<CR>").
Solution:
Check the atom's origin buffer (`CmdAtom.origin.buf`).
Note: This does not preclude mappings etc from doing work in temporary
throwaway buffers, as long as they return to the origin buffer.
Problem:
`vim.version.intersect()` returns tables with `VersionRange` (method
table) as their metatable. Although the calculated bounds are correct,
the results do not expose `VersionRange` methods.
Solution:
Construct intersection tables with `range_mt`, matching
`vim.version.range()`. Add assertions covering method access and
repeated intersection.
Problem:
When disabling, the loader list is traversed forwards while entries are
removed. As a result, some entries are skipped and remain active. Thus,
disabling `vim.loader` restores `_G.loadfile`, but does not fully
restore the original package loader chain.
Solution:
Traverse `package.loaders` in reverse when removing cached loaders.
Capture the original `package.loaders` list in the test, and assert that
the list is restored properly.
Problem:
If an edit cascades to cursor(s) that happen to be within a closed fold,
the edit acts on the fold itself (`:h fold-behavior`). E.g. "dd" deletes
the entire fold, not the cursor's line within the fold.
Solution:
Check `mc_replaying()` in `hasAnyFolding()`, so folds are ignored during
a multicursor cascade. This means multicursor always replays relative to
text *within* the fold.
Problem:
Coordinates-to-position conversion for the finish column in
`vim.hl.range()` checks the start column. This results in an incorrect
finish column when only one of the column coordinates is `vim.v.maxcol`.
Solution:
Check the finish column when converting the finish position. Update the
existing screen test so that a `vim.v.maxcol` finish highlights the
end-of-line marker, matching the existing `-1` behavior.
Problem: With 'wildmode' set to list:full the matches are listed but the
wildmenu is not shown, although it is "full" that starts
wildmenu mode (zeertzjq).
Solution: List the matches and show the menu, as the two behaviors in
the same phase ask for. The menu is left to the phases that
ask for it, so that "list" on its own still only lists
(Hirohito Higashi).
fixes: vim/vim#21196closes: vim/vim#21205fa1ddffcce
Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Problem: With 'wildmode' set to list:full, matches are listed but the
first match is not completed on the first Tab press (rendcrx).
Solution: Do not suppress selection when list and full are active in
the same completion phase. Add regression tests for file
completion, comma-separated phases and noselect precedence
(Seunghee Kim).
fixes: vim/vim#19532
related: vim/vim#18088
closes: vim/vim#2118154c988c6c5
Co-authored-by: SeungheeKim <ksh368@naver.com>