feat: integrate native Neovim multicursor - #16
Open
lettertwo wants to merge 11 commits into
Open
Conversation
lettertwo
force-pushed
the
feat/native-multicursor
branch
4 times, most recently
from
September 3, 2026 22:09
52aefd0 to
0789903
Compare
Neovim merged built-in multiple cursors (neovim/neovim#41587, `:h multicursor`, 0.13+). This makes it possible to hand occurrence marks off to that feature. New occurrence-mode action `cursors` (`Q`) converts all marks, or the marks in a visual selection, to native cursors. Motion-scoped operators `cursors_start` (`I`) and `cursors_end` (`A`) place cursors on the first or last character of marks within a motion, and `change_cursors` deletes the marks and enters insert. On 0.13+, `change_cursors` becomes the default `c` (in occurrence mode and via `coip`); the prompt-based change is still reachable with `operators = { c = "change" }`. Cursors start in follow-mode by default (`follow_cursors`), so motions replay at every cursor. The whole surface is gated on `vim.api.nvim_mcursor`, so the default keys are never bound on Neovim without the feature.
The new `cursors` action tested `mode():match("[vV]")`, which misses
blockwise Visual ("\22"), so `<C-v>` then `Q` took the non-visual branch
and converted every mark in the buffer. The three older sites carried a
literal 0x16 byte inside the character class, invisible in most editors,
which is how the new copy lost it. One `in_visual_mode()` helper now
serves all four.
The `cursors` action consumed the selection but never left Visual mode, so the scheduled `Cursor.move(primary)` extended the selection instead of placing the primary cursor, and the next keystroke acted on a selection the user never made. Exit to Normal mode once the scope is captured, the same way `Occurrence:of_selection` and the operator path do.
`opts.primary` is documented as an index into `locations`, but the list was deduped and clamped first and then indexed with the caller's original index. A duplicate ahead of the primary shifted it onto the wrong location, or past the end, where `Cursor.move(nil)` threw and no cursors were made. Resolve the index while building the deduped list instead.
`done(edited, edited)` handed the edited list to `on_done` as `result`, where only `false` means cancelled, and left `text_edited` to fall back to its default. It worked by accident. Pass `nil` explicitly and fix the docstring to match the real `on_done` signature.
`change_cursors` scheduled the cursor handoff and `nvim_input("i")` as
two independent closures. If the buffer changed before they ran, or
`mcursor.add` raised, the handoff was skipped but Insert mode was still
entered: text deleted at every occurrence, one cursor, and the error
logged only at debug level. Insert now rides on the handoff closure and
runs only after a successful add, and the failure is a warning.
The cursor operators made `toggle_dispose` skip any operator that sets its own `dispose_after_operator`, but the README still said the action flips the decision for whatever runs next. Say who the toggle applies to, regenerate the vimdoc, and pin the exemption with a test on a custom operator.
`change_cursors` from operator-pending mode and the `follow_cursors` option were only covered at config resolution and at `mcursor.add`. Drive both through the plugin.
The default keymap and operator tables were patched at module load based on `mcursor.is_supported()`, which baked the gate into module state and forced the stable-nvim test to unload and re-require `occurrence.Config` under a stubbed `vim.api`. `config.default()` now applies the gate per call, `config.new` builds on it, and the test just stubs the API.
The branch's docs commit was generated with the older vendored panvimdoc. Regenerate after rebasing onto main so the file matches the rendering CI's docs-check produces (two-line title, unindented code fences in list items, plain space after "e.g."). No README content changed.
lettertwo
force-pushed
the
feat/native-multicursor
branch
from
September 3, 2026 22:59
0789903 to
d1dd279
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Neovim merged built-in multiple cursors (neovim/neovim#41587,
:h multicursor, 0.13+). occurrence.nvim already documents a wiki integration with the third-party multicursor.nvim plugin. This PR ships a first-class native equivalent: mark occurrences, then hand them to Neovim as real cursors.What this adds
cursorsaction (Qin occurrence mode): convert all marks, or in visual mode the marks in the selection, to native cursors. With no marks yet, it marks the word under the cursor first, likenext.cursors_start/cursors_end(I/A): motion-scoped operators that place a cursor on the first or last character of each mark within a motion, e.g.Iip,Aip. AfterAip,aappends after every occurrence.change_cursors: delete the marks in a motion, place cursors at the insertion points, and enter insert. On 0.13+ this becomes the defaultc, in occurrence mode (cip) and operator-pending mode (coip). The prompt-basedchangestays available withoperators = { c = "change" }.follow_cursorsoption (defaulttrue): cursors start in follow-mode (:h q=) so motions replay at every cursor. Toggle at runtime with nativeq=.These new bindings are gated on the presence of the new cursor API, so compataibility with nvim < 0.13 is maintained.