aboutsummaryrefslogtreecommitdiff
path: root/test/functional/editor/mcursor_spec.lua
Commit message (Collapse)AuthorAge
* feat(multicursor): ]C [C use previous cursors if mc inactive #42267IOCRYPOSTO7 days
| | | | | | | | | | | | Problem: After clearing cursors, "]C" "[C" do nothing, although the previous cursor positions ("gQ") may still be useful as marks. Solution: If multicursor is not active, "]C" "[C" iterate the "nvim.multicursor.last" extmarks. They also work as an operator motion ("d]C"), since there is no cascade. AI-assisted
* fix(multicursor): Visual-mode mapping drops getchar() payload #42324Justin M. Keyes8 days
| | | | | | | | | | 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).
* fix(multicursor): v:cascading during on_key, insert-preview #42319Justin M. Keyes8 days
| | | | | | | | | | | 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()`.
* fix(multicursor): "zqn" places no cursors after "/\%V" search #42313Justin M. Keyes8 days
| | | | | | | | 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.
* multicursor: v:cascading #42289Evgeni Chasnovski8 days
| | | | | Problem: No way to check if cascade is in action. Solution: Add `v:cascading` variable.
* fix(multicursor): insert-mode Lua/API edit dropped on ESC #42312Justin M. Keyes8 days
| | | | | | | | | | | | | | | | | | | | | | | | 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.
* test: simplify functional test expectationsLewis Russell2026-10-05
| | | | | | | | | | | | Problem: Repeated default fields and field-by-field assertions obscure the values that each functional test is meant to check. Solution: Add eq_partial() to check selected record fields while preserving list length and order. Use local helpers to fill fixed expected defaults where tests need full equality. Keep explicit absence checks and test-specific values at the call sites. AI-assisted
* fix(multicursor): session-end clobbers '< '>; zqgj skips rows #42221Justin M. Keyes2026-10-03
| | | | | | | | | | | | Problem: - At session end, `'< '>` revert to the session-start snapshot, which drops a selection made during the session. And the snapshot may be from another buffer. - `zqgj` skips screen rows: `zq` tries the motion before the steps, and those trial runs advance `w_curswant`. Solution: - Keep the primary's `'< '>` at session-end. - Restore `w_curswant` after the trial runs and after each run of steps.
* fix(multicursor): zq failed motions, "t", first stepJustin M. Keyes2026-09-30
| | | | | | | | | | | | | | | | Problem: - "Vjzqfx" (on a line without "x") places a cursor at the selection start. - 'nowrapscan': `zqN` places a cursor on a non-match; `zq#` mid-word places it mid-word. - `zqfa` places at primary even if not on an "a". - `zqti` stops after one step (bc "ti" before an "i" does not move). Solution: - `did_beep_silent`: counts beeps silenced by `emsg_silent`, so `zq` steps can detect failed motions. - If a `zq` step does not move the cursor, retry it from the next char. - Decide the first `zq` step the same way as `{Visual}zq` already does: the primary is considered a step only if the motion lands on it.
* fix(multicursor): zq edge cases; cascade perf #42165Justin M. Keyes2026-09-30
| | | | | | | | | | | - `{Visual}Q` / `{Visual}zqj`: for linewise motion ("j"), every line gets a cursor. - `zq`: handle 3-key motions (`g'a`), don't scroll the view, preserve the search direction, update existing cursors `gv` target. - Disallow `zq` in macros. - Abort `zq` on non-motion ("zqx"). - Classify `g'x` classify like `'x`. - Classify `'(` `'{` etc. as `type=motion` (they are cursor-relative). - Perf: skip save/restore of marks when cascading motions ('follow').
* refactor(multicursor)!: drop nvim_mcursor #42156Justin M. Keyes2026-09-30
| | | | | | | | | | | | | | Problem: `nvim_mcursor` is not fully baked, and not strongly motivated, currently. The main programmatic interface for cursors is the "nvim.multicursor" extmarks namespace. Solution: Drop `nvim_mcursor`. Add an internal hook so that multicursor can react when extmarks are added to the "nvim.multicursor" namespace. TODO: If extmarks/namespaces are clumsy to work with, we should _fix that_ rather than adding wrappers.
* feat(multicursor): zq operator; cursor-local marks #42148Justin M. Keyes2026-09-29
| | | | | | | | | | | | | | | | | | Problem: - `[count]Q` and `{Visual}Q` are implemented as ad hoc utils, because `Q` is not an "operator". - `Q` always "toggles", it is not "idempotent". - Various *cursor-relative* marks (`'.`, `'[`) are not saved/restored per-cursor, so they don't work during cascade. - Insert cascade clobbers the primary's `'^` and changelist. Solution: - Introduce the `zq{motion}` operator, which places cursors at every reachable `{motion}`. - `{Visual}Q` is now an alias to `{Visual}zqj`. - Introduce `1Q` and `2Q` for idempotent cursor placement. - Drop `[count]Q`, it's redundant with `zqgn`. - Classify cursor-relative marks as `type=motion` instead of `type=jump`. Save/restore per-cursor during cascade.
* feat(multicursor): 'follow' option #42095Justin M. Keyes2026-09-27
| | | | | | | | Problem: - gQ does not restore follow-mode (q=). - Users/plugins have no way to check if follow-mode is enabled. Solution: Introduce buffer-local 'follow' option.
* vim-patch:9.2.1114: Visual selection is not highlighted at the cursor ↵Takumi Katase2026-09-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | position #42105 vim-patch:9.2.1114: Visual selection is not highlighted at the cursor position Problem: In a terminal the character under the cursor is normally not shown as part of the visual selection. Solution: Don't set "noinvcur" in win_line() (Shane Harper). The problem did not occur in the GUI, only in a terminal. The name "noinvcur" ("don't invert the cursor") was misleading: the variable did not affect the cursor, but whether the character under the cursor could be displayed as part of the visual selection. The "noinvcur" handling may have been meant to help during a visual selection when Visual is shown in reverse video and the terminal draws the cursor by inverting the character under it, as otherwise the character under the cursor would not look highlighted like the rest of the selection. But Vim cannot know in general how a terminal draws the cursor, and when either the cursor is not drawn by inverting the character under it or Visual is not shown in reverse video, the handling just left a character of the visual selection unhighlighted. closes: vim/vim#21317 https://github.com/vim/vim/commit/d10aab4ba6602f8f2f2576d2d62ceb63869db6fb Co-authored-by: Shane Harper <shane@shaneharper.net> AI-assisted
* fix(multicursor): "gv" + Insert-entering cmd #42110Justin M. Keyes2026-09-27
| | | | | | | | | | | | | | | | | | | | | Problem: - Multicursor does not cascade a `gv`-entered insert-session (`gvc`). - CmdAtom of a `gv`-entered insert-session (e.g. `gvcX<Esc>`) has `keys="1vcX<Esc>"` (redo's fallback), not the typed keys. And is not emitted at all if multicursor is inactive. Analysis: Redo can't contain "gv", so it does the "1v" fallback, and the insert-cascade replays redo. Solution: Replay "gv" + op at each cursor: store the typed selection in `InsSession.vsel`; it supplants redo's "1v" in the emitted atom and in the insert-cascade replay. A cursor without a Visual area skips a "gv"-based replay (else its keys would run as Normal-mode: "gvrx" => "rx"). Redo behavior is unchanged ("1v" fallback): "gv" is absolute, "." acts at the cursor.
* fix(multicursor): flicker with ui2 #42090Justin M. Keyes2026-09-25
| | | | | | | | | | | | | Problem: UI clients (such as the builtin "ui2") redraw on 'showcmd' updates. When multicursor is active this flushes a frame per-cursor during cascade, which causes: - flicker - slow performance - renders half-applied edits, displaced cursors, Visual selections without highlights Solution: Skip 'showcmd' updates during cascade.
* fix(multicursor): unsynced primary-overlapping cursor #42089Justin M. Keyes2026-09-25
| | | | | | | | | | | | | | | | | | Problem: - The primary-overlapping cursor is synced to primary only after cascade, so it may display at a shifted position (e.g. after "rx") during cascade. - LHS-replay edits apply twice if there is a primary-overlapping cursor. Solution: - Sync the primary-overlapping cursor _before_ replay. This ensures the mark is correct _during_ the cascade. - Drop the post-cascade sync; this is safe because the overlapping mark and the primary-tracking mark are both right-gravity, so they get shifted the same way. - Set `origin` on LHS-replay atoms, so the cascade can compare it. Bonus: a buffer-switching mapping no longer LHS-replays if it ends up in a different buffer.
* fix(multicursor): insert-mode <c-r>x per-cursor registers #42076Justin M. Keyes2026-09-24
| | | | | | | | | | | | Problem: Insert-mode `<c-r>x` stuffs the literal text, so multicursor cascade puts the same (primary) text at each cursor. Note: `<c-r>-` happens to work because `insert_reg()` special-cases it. Solution: Update `insert_reg` so that it preps redo with `<c-r>x` instead of the literal text. Update `atom_key_class` so that CTRL-R flushes the insert-span.
* feat(multicursor): gQ restores primary-cursor position #42071Justin M. Keyes2026-09-24
| | | | | | | | | | | | | | | | | | | | | Problem: Unlike "gv", "gQ" does not restore the primary-cursor position. This is inconvenient for workflows where you temporarily clear the cursors, navigate somewhere, then want to return to the multicursor session. Solution: Store the primary-cursor position at extmark-id 1 of the `'nvim.multicursor.last'` namespace. Note: users that *don't* want to restore the primary-cursor position can delete extmark-id 1 from the `'nvim.multicursor.last'` namespace just before `gQ`. (Intentionally undocumented because this interface is not final, and should not block Nvim 0.13 release.) vim.keymap.set('n', 'gQ', function() local ns = vim.api.nvim_create_namespace('nvim.multicursor.last') pcall(vim.api.nvim_buf_del_extmark, 0, ns, 1) return 'gQ' end, { expr = true })
* fix(multicursor): skip gQ during an active session #42068Justin M. Keyes2026-09-24
| | | | | | | Problem: gQ is nonsense during an active multicursor session. Solution: Make it a no-op.
* fix(multicursor): mapped jump merges cursors in follow-mode #42063Justin M. Keyes2026-09-24
| | | | | | | | | | | | Problem: Jumps (intentionally) don't cascade, thus they don't queue atoms. But in follow-mode, a mapping that moves the cursor but queues no atoms, LHS-cascades (this is a general fallback for the case where `keys` cannot be resolved). Thus `nnoremap <Down> ]C` replays `]C` at each cursor, which causes overlapping cursors, which are then merged/deduped. Solution: If a composite contains a "jump", set `composite.queued` to disable LHS-replay.
* fix(multicursor): don't dedupe primary-overlapping cursor #42060Justin M. Keyes2026-09-24
| | | | | | | | | | | | | | Problem: A primary-overlapping cursor is deduped before an edit. But if a Visual/Insert action moves the primary first, the cursor is not skipped (e.g. `vipQva)c` replays at the primary-overlapping cursor). Solution: Skip _replay_ of a primary-overlapping cursor, but don't _dedupe_ it. Keep its position synced with the primary. Sample the primary-overlapping cursor (`CmdOrigin.mcursor`) at the action start (first root frame, or Visual session-start), instead of current frame start.
* feat(multicursor): cgn, failed insert-entry #42054Justin M. Keyes2026-09-23
| | | | | | | | | | | | | | | | | | Problem: - A failed motion in a replay (`vt;` where there is no `;`) flushes the rest of the replay, so `vt;cX` inserts `X` without changing the selection. - If the Insert-entering cmd fails at a cursor (`ct;`, `cgn` with no match), the text is still inserted at the failed cursor. Solution: - `beep_flush()`: Don't flush typeahead (i.e. keys following the failed cmd) during mc-replay. - Aligns with interactively-typed behavior. - Remove a cursor if its Insert-entering command aborts (`beep && !changedtick`). - This is particular to Insert-entering commands (`c{motion}`), because they are tricky. We don't usually remove cursors if a replayed cmd happens to fail at some cursors.
* feat(multicursor): jump vs motion #42052Justin M. Keyes2026-09-23
| | | | | | | | | | | | | | | | | | | | Problem: Incoherent classification of motions vs jumps. Solution: Define "motion" as cursor-relative, and "jump" as "absolute". - Classify G/gg/H/L/M/go/N% as `CmdAtom.type=jump`. - Classify `*`/`#` as motions. Behavior changes: - `*` in follow-mode now moves each cursor to the next occurrence of its own word. - Note: the "last search pattern" is not stored per-cursor, so `n` still searches for the primary's search pattern. - G/gg/H/M/L in follow-mode do not cascade (would converge/dedupe all cursors). - If you want to force cursors to converge for some reason, use `9999j`. - Also affects mappings that mix an absolute move with an edit. E.g. mapping `nnoremap X ggdd` only replays `dd` per-cursor now. This is how marks already behaved.
* fix(multicursor): undo/redo mapping in follow-mode #42051Justin M. Keyes2026-09-23
| | | | | | | | | | Problem: In follow-mode, a mapping that moves the cursor but doesn't queue an atom, cascades LHS. Undo moves the cursor and (intentionally) does not queue an atom, so `nnoremap u u` replays undo per-cursor. Solution: - Set `composite.queued`, to skip LHS-cascade. - Also treat "U" (line-undo) as a "global op".
* fix(multicursor): edit applied twice at cursor overlapping the primary #42050Justin M. Keyes2026-09-23
| | | | | | | | | | | Problem: A cursor overlapping the primary is deduped by comparing positions at the clock edge, i.e. after the primary edit executed. A "replace" at the cursor cell ("rx", "~", "g~iw") shifts the overlapping extmark, so it no longer matches and gets its own replay. E.g. "vipQg~~" applies twice to the 2nd line, which inverts the effect. Solution: Decide "overlaps the primary" at cmd start, before the edit.
* fix(multicursor): revert "]C"/"[C" cursor-plopping behavior #42034Justin M. Keyes2026-09-22
| | | | | | | | | | | | | Problem: Jumping to a cursor adds one at the old position, so navigation has a side-effect. "]CQ" no longer deletes a cursor, and returning with "[C" after wandering off adds a cursor wherever you were. This is useful in some cases, but as a default it is surprising. The default behavior should be simple and composable. Solution: Revert the "]C"/"[C" cursor-plopping behavior. Document a mapping for the "rotate" behavior.
* fix(multicursor): cascade mapping-edit during Visual selection #42027Justin M. Keyes2026-09-21
| | | | | | | | | | | | | | | | Problem: - A mapping that edits the buffer during a Visual selection applies only at the primary cursor. The edit "voids" the pending Visual-session, because `mc_vsel_refresh` only previews the selection per-cursor and must not edit. - A cursor overlapping the primary applies an edit twice, if the command moves the primary before cascade ("yiwp"). Solution: Cascade a Visual session's edits span-by-span, like an insert-session. - Each span is prefixed with "gv" so per-cursor reselect works. - Void only if no subatom was captured. - Dedupe cursors against the primary's anchor at cmd start, not its position after.
* fix(cmdatom): capture <Cmd>, not its subatoms #42020Justin M. Keyes2026-09-21
| | | | | | | | | | | | | | | | | | | Problem: A command's `:norm` children are captured as Visual subatoms: `v<Cmd>normal! e<CR>d` is "decomposed" to `ved`. That is a cosmetic, nice-to-have feature, which greatly complicates the cmdatom logic: it must detect "lossy" cases (API moves, "gv" on marks the command set, a session ended and restarted). Solution: Drop the nice-to-have. - A command is captured as itself, never its `:norm` children. - While a child frame runs, a pending Visual session is kept open until the enclosing frame resolves. - `:`/`<Cmd>` no longer decline to be a Visual subatom. - Stuffed keys ("." replay, "x" => "dl") and K_EVENT children (RPC, timer), are still captured as their translation. - `mc_add()` discards any pending Visual session, so `{Visual}Q` does not cascade recursively to the cursors it just created.
* fix(multicursor): replay of Lua mapping-id ending in "0" #42011Justin M. Keyes2026-09-20
| | | | | | | | | | | | | | | | Problem: - A captured Lua mapping (`K_LUA`+id) whose id ends in "0" replays the wrong mapping: `atom_from_cmdline()` escapes the id like a typed cmdline (trailing "0" => CTRL-V "048"), but the id is read raw. E.g. treesitter `an` (id 60) replays `gc` (id 6), so `van` does not preview/cascade at mcursors. - Same for `<Cmd>…` ending in "0" or "^" (`<Cmd>normal! 0<CR>`). - `c` + Lua/":" textobj (`canX`) does not insert-cascade. Solution: - Only CTRL-V-escape ":", "/" cmdline; append `<Cmd>` and Lua mapping-ids "raw" (`sb_add_spec()`). - `kVInsMotion`: an insert entered by Ex/Lua motion re-executes the motion, unless it read input.
* fix(multicursor): selection by API/Lua from mapping #41999Justin M. Keyes2026-09-19
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: A Visual selection produced by a Lua mapping is captured as the keys it fed (`:norm!`), so non-primary cursors get the wrong selection. Examples: - API cursor-moves around a fed "o" are lost. - `setpos("'<")` + "gv" reselects the primary's marks (treesitter `an`). - A mapping that feeds an absolute selection (`:norm! <Esc>`, then `norm! 3GV6G`). Solution: Classify fed keys as `lossy` if their produced selection differs from the selection at mapping-end, or fed-key-start: the mapping moved it by other means (API). Same idea as `composite.lossy` LHS-replay. Now the captured Visual session contains the mapping's own cmd (`<Cmd>…`, `:…`, or a Lua mapping-id) as one subatom, instead of the keys it fed. E.g. `v…<K_LUA id>` instead of `v…V…G`. Also affects dot-repeat ("."). A `:norm!` that ends a typed selection keeps the Visual session (`vatom`) open (instead of the usual `atom_visual_reset()` plus emit/stage), so the enclosing user frame (the mapping's `<Cmd>`/Lua, or a typed `:norm!`) can complete or replace it. - A child frame re-entering Visual does not restart it, so a later `norm! V` continues that session instead of a new `kVatomFed` one. - At end of the user (toplevel) frame, the fed subatoms are dropped in favor of the mapping's own atom: as a subatom if Visual is active, else as the session's closing suffix.
* fix(multicursor): visual "gv", change-marks '[,'] #41988Justin M. Keyes2026-09-18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Problem: - `gv` reselects only primary's region, it doesn't have per-cursor behavior. '< '> are buffer-global and the sandbox restores the primary's marks. The Visual atom containing `gv` was voided anyway, so it never cascaded. - Same for change-marks (`'[ '] '.`): they are stale after edits from other cursors, not managed per-cursor. - `McSandbox` saves "editor state", which should be owned by the `Context` module. Solution: Lift more state from `McSandbox` into `Context`. `kCtxCursor` (position, curswant) and `kCtxMarks` (Visual area, change-marks), are owned by `Context` now, as an "in-memory" special-case (TODO: support it for the "serialized" case?). - Classify a `gv` selection as replayable. Manage its state per-cursor. Dot-repeat "." after `gv` behaves like Vim (the "1v" fallback). - Save/restore change-marks per-cursor. The primary's change-marks are shifted by edits from other cursors during the cascade. `McSandbox` still contains (for now) the view and replay isolation (typeahead/mode, redobuff, search patterns, the active selection). Bonus: - `visualmode()` is per cursor. - A cursor's `curswant` follows its column when other cursors' edits shift it, instead of the column captured after its own replay.
* refactor(undo): redundant "forget" logic #41980Justin M. Keyes2026-09-18
| | | | | | | | | | | | | | | Problem: `u_undo_and_forget()` and `u_forget_unchanged()` do similar undo-tree surgery: unlink a header, promote the alt branch it displaced. Solution: Extract common logic to `u_forget_header()`. Bonus: - The `:undo!` copy now fixes `b_u_oldhead`, so after forgetting a root header, its alt branch is still listed by `undotree()`. - Freeing can no longer unlink the promoted branch: `uh_alt_prev` is cleared first. The old code relied on the forgotten header being the first sibling.
* fix(multicursor): empty insert-session adds a no-op undo-state #41979Justin M. Keyes2026-09-18
| | | | | | | | | Problem: An insert-session (`mc_ins_cascade_start`) saves a preliminary "covering" undo-entry, to cover all cursors. If the insert-session exits without changes, that no-op entry remains, requiring an extra "u". Solution: Drop the entry at insert-session end if there were no changes.
* feat(multicursor): color extra Kitty cursors #41935phanium2026-09-18
| | | | | | | | | | | Problem: Extra cursors use shape 29 (follow primary), so they render identically to the primary instead of using hl-MCursor colors. Solution: Send MCursor fg/bg as sRGB via shapes 30/40 before placing cursors. Terminals without 30/40 support ignore the color sequences. AI-assisted
* fix(multicursor): "q=" before an API cursor-move is ignored #41899Justin M. Keyes2026-09-13
| | | | | | | | | | | | | Problem: A mapping that toggles follow-mode before/after an API cursor-move cascades the move if follow-mode was on from the start. Why: Follow-mode is sampled only at command start, and the API move has no CmdFrame of its own. Solution: Record follow-mode at the time of the move, not the start of the frame. Update `CmdFrame.follow` on all "undecided" parent frames. A frame that already moved keeps its old value (not retroactive).
* fix(cmdatom): mapping tail after a mode-change is not captured #41897Justin M. Keyes2026-09-13
| | | | | | | | | | | | | | | | Problem: `inoremap <Esc> <Esc>l` starts in insert-mode and ends in normal-mode ("l"). Its normal-mode "l" is not captured as a CmdAtom (and thus doesn't cascade to multicursors in follow-mode). Analysis: CmdFrames only delineate in Normal/Visual/Op-pending (i.e. the `MODE_NORMAL` modes), so a mapping resolved in Insert, Cmdline or Terminal mode does not start a `composite`, then the mapping "tail" is lost. Solution: Start a "deferred" `composite` if state is not `MODE_NORMAL`, instead of skipping it entirely.
* feat(multicursor): {Visual}[count]Q matches in Visual selection #41863Justin M. Keyes2026-09-11
| | | | | | | | | | | | | | Problem: `[count]Q` cannot be limited to a Visual selection. Solution: `{Visual}[count]Q` places cursors on matches within a Visual selection (linewise). TODO: This works linewise, even if the visual selection is charwise/block. If we really want to limit by charwise/block selection, then we should add that feature to `seachpos()` or similar. Every caller of `seachpos()` should not have to cobble together bespoke support thereof.
* fix(multicursor): {Visual}Q should not move primary cursor #41862Justin M. Keyes2026-09-11
| | | | | | | | | Problem: `{Visual}Q` moves the primary to the top of the selection, for no particular reason. And it's inconsistent with `[count]Q`. Solution: Keep the primary on the line where the selection ended, aligned to the shared screen column so it still coincides with that line's cursor.
* refactor(cmdatom): merge `curcmd` into root CmdFrameJustin M. Keyes2026-09-11
| | | | | | | | | | | | | | | | Problem: - `curcmd` has the lifetime of the root CmdFrame, but lives in its own container with a reset (atom_redo_reset) that must mirror "toplevel user command". - `curcmd.follow` is also the wrong scope: the clock-edge is deferred across a mapping's keys, each a toplevel command, so `nnoremap <F1> 1q=j2q=` runs "j" in follow-mode, yet the edge reads the trailing "2q=" and does not cascade the queued motion. Solution: - Eliminate `curcmd`. Use the root `CmdFrame` as the source of truth. - Record follow-at-first-motion on `composite`, which spans exactly the frames the deferred clock-edge closes.
* fix(multicursor): q= (toggle follow) is not synchronousJustin M. Keyes2026-09-11
| | | | | | | | | | | | | | | | | Problem: The clock-edge that decides whether queued motions cascade, reads the global `mc_follow_motion` at the end, which may differ since the command started/executed. Examples: - A mapping like `2q=gg1q=` (off, move, on) cascades the "gg" and merges the cursors, because follow=on when the clock-edge resolves stuff. - `{Visual}Q` after a mapping enables follow at the end, and the clock-edge mistakenly cascades the mapping's keys. Solution: Save `CmdFrame.follow` in `curcmd`, and pass it to `mc_clock_edge()`. Innermost-frame wins, so `q=` performed by a subatom/subcmd wins.
* fix(mul