mirror of
https://github.com/neovim/neovim.git
synced 2026-09-03 04:43:48 +00:00
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().
This commit is contained in:
@@ -573,9 +573,9 @@ nvim_ui_term_event({event}, {value}) *nvim_ui_term_event*
|
||||
Emitted by the TUI client to signal when a host-terminal event occurred.
|
||||
|
||||
Supports these events:
|
||||
• "termresponse": The host-terminal sent a DA1, OSC, DCS, or APC response
|
||||
sequence to Nvim. The payload is the received response. Sets
|
||||
|v:termresponse| and fires |TermResponse|.
|
||||
• "termresponse": The host-terminal sent an OSC, DCS, APC, or recognized
|
||||
CSI (DA1, kitty multiple-cursors) response to Nvim. The payload is the
|
||||
received response. Sets |v:termresponse| and fires |TermResponse|.
|
||||
|
||||
Attributes: ~
|
||||
|RPC| only
|
||||
@@ -1279,6 +1279,20 @@ nvim_load_context({dict}) *nvim_load_context()*
|
||||
Return: ~
|
||||
(`any`)
|
||||
|
||||
nvim_mcursor({buf}, {pos}) *nvim_mcursor()*
|
||||
Adds a multicursor in the given buffer.
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.13.0
|
||||
|
||||
Parameters: ~
|
||||
• {buf} (`integer`) Buffer handle, or 0 for current buffer
|
||||
• {pos} (`[integer, integer]`) (row, col) (1,0)-indexed cursor position
|
||||
(byte offset)
|
||||
|
||||
Return: ~
|
||||
(`integer`) Total number of extra cursors.
|
||||
|
||||
nvim_open_term({buf}, {opts}) *nvim_open_term()*
|
||||
Open a terminal instance in a buffer
|
||||
|
||||
|
||||
@@ -1323,7 +1323,8 @@ TermRequest When a |:terminal| child process emits an OSC,
|
||||
autocommand defined without |autocmd-nested|.
|
||||
|
||||
*TermResponse*
|
||||
TermResponse When Nvim receives a DA1, OSC, DCS, or APC response from
|
||||
TermResponse When Nvim receives an OSC, DCS, APC, or recognized
|
||||
CSI (DA1, kitty multiple-cursors) response from
|
||||
the host terminal. Sets |v:termresponse|.
|
||||
|
||||
The |event-data| has these keys (type: `vim.event.termresponse.data`):
|
||||
|
||||
@@ -562,6 +562,8 @@ CONCEPTS
|
||||
structured fields (`CmdSpec`). Every user action emits a |CmdAtom| event.
|
||||
- SPAN: Fragment of an Insert or Visual sequence, cascades but does not emit
|
||||
a CmdAtom.
|
||||
- CASCADE: Replay queued atoms at the "clock edge" at every cursor. Visual
|
||||
cascade "dry-runs" spans to display the selection.
|
||||
- COMPOSITE: An atom composed of subatoms (`CmdAtom.atoms`): a mapping/macro's
|
||||
commands, or a Visual sequence.
|
||||
- INSERT-SESSION: All changes from an insert-mode session, until <Esc>.
|
||||
@@ -569,13 +571,13 @@ CONCEPTS
|
||||
- INSERT-CASCADE: Replay an insert-session span-by-span as it is typed (each
|
||||
span is packaged as a quasi-session: `i` + keys + `<Esc>`), via nested
|
||||
`edit()`. Pending literal text is a PREVIEW until its span completes.
|
||||
- REPLAY: Execute keys (dot-repeat, or atom).
|
||||
- CASCADE: Replay queued atoms at the "clock edge" at every cursor. Visual
|
||||
cascade "dry-runs" spans to display the selection.
|
||||
- VOID: When a pending (Visual) atom's keys were tainted/poisoned (by: mouse,
|
||||
`gv`, a scroll that drags the cursor, …), thus not replayable.
|
||||
- LOSSY: When capture lost part of a composite (payload with no capturing
|
||||
atom, incomplete insert-session): `keys` cannot replay it, `lhs` can.
|
||||
- PAYLOAD: Keys a command reads interactively while executing: a search/Ex
|
||||
cmdline (`d/pat<CR>`), `f{char}`, a mapping's `getchar()`.
|
||||
- REPLAY: Execute keys (dot-repeat, or atom).
|
||||
- VOID: When a pending (Visual) atom's keys were tainted/poisoned (by: mouse,
|
||||
`gv`, a scroll that drags the cursor, …), thus not replayable.
|
||||
|
||||
IMPLEMENTATION
|
||||
|
||||
@@ -595,12 +597,11 @@ Capture happens at the `atom_xx()` hooks.
|
||||
|
||||
DOT-REPEAT
|
||||
|
||||
The redo buffer itself is the pending change atom (`RedoBuf`); the register,
|
||||
Visual flag and count are FIELDS, and the command body is bytes appended
|
||||
during execution by the `redo_append_xx()` family (Vim's `AppendToRedobuff*`).
|
||||
The old `["x][v][count]body` byte encoding no longer exists; `prep_redo()`
|
||||
stores the prefix structurally, and consumers recompose it at the boundaries
|
||||
(`redo_keys()`, `start_redo()`, `atom_from_redo()`).
|
||||
The redo buffer is the pending change as a `CmdSpec` (`RedoState`): the
|
||||
`["x][count]` prefix is structured fields set by `prep_redo()`, and the
|
||||
command body is bytes appended during execution by the `redo_append_xx()`
|
||||
family (Vim's `AppendToRedobuff*`). Consumers rebuild the keysequence at
|
||||
the boundaries (`redo_keys()`, `start_redo()`, `atom_from_redo()`).
|
||||
|
||||
THE CASCADE
|
||||
|
||||
@@ -646,7 +647,7 @@ draw "real" cursors, it is expected to:
|
||||
"nvim.multicursor.cursor" positions while any exist (in-progress Visual
|
||||
selection).
|
||||
- render selections from "nvim.multicursor.visual", or simply keep their
|
||||
"hl-MultiCursorVisual" grid highlights.
|
||||
|hl-MCursorVisual| grid highlights.
|
||||
|
||||
==============================================================================
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ Normal Mode: >
|
||||
cursor window
|
||||
---------------------------------------------------------------------------
|
||||
<LeftMouse> yes end yes
|
||||
<C-LeftMouse> yes end yes "CTRL-]" (2)
|
||||
<C-LeftMouse> no no change no toggle multicursor
|
||||
<S-LeftMouse> yes no change yes "*" (2)
|
||||
<LeftDrag> yes start or extend (1) no
|
||||
<LeftRelease> yes start or extend (1) no
|
||||
@@ -189,7 +189,7 @@ Insert or Replace Mode: >
|
||||
cursor window
|
||||
---------------------------------------------------------------------------
|
||||
<LeftMouse> yes (cannot be active) yes
|
||||
<C-LeftMouse> yes (cannot be active) yes "CTRL-O^]" (2)
|
||||
<C-LeftMouse> no (cannot be active) no no-op
|
||||
<S-LeftMouse> yes (cannot be active) yes "CTRL-O*" (2)
|
||||
<LeftDrag> yes start or extend (1) no like CTRL-O (1)
|
||||
<LeftRelease> yes start or extend (1) no like CTRL-O (1)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Nvim documentation
|
||||
|
||||
If you are new to Nvim, see |nvim-intro|. (To go to a help link, move the
|
||||
cursor to it and hit CTRL-], or ctrl-click with mouse. Try it here: |bars|.)
|
||||
cursor to it and hit CTRL-], or right-click with mouse. Try it here: |bars|.)
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
About Nvim *reference_toc* *Q_ct*
|
||||
|
||||
@@ -335,8 +335,8 @@ the help text is displayed as it was intended:
|
||||
|
||||
Jump to specific subjects by using tags. This can be done in two ways:
|
||||
- Use the "CTRL-]" command while standing on the name of a command or option.
|
||||
This only works when the tag is a keyword. "<C-Leftmouse>" and
|
||||
"g<LeftMouse>" work just like "CTRL-]".
|
||||
This only works when the tag is a keyword. "g<LeftMouse>" works just like
|
||||
"CTRL-]".
|
||||
- use the ":ta {subject}" command. This also works with non-keyword
|
||||
characters.
|
||||
|
||||
|
||||
@@ -402,7 +402,8 @@ Tag Char Note Normal-mode action ~
|
||||
|q| q{0-9a-zA-Z"} record typed characters into named register
|
||||
{0-9a-zA-Z"} (uppercase to append)
|
||||
|q| q (while recording) stops recording
|
||||
|Q| Q 2 replay last recorded register
|
||||
|Q| Q place/remove a multicursor; with [count]:
|
||||
place at each search match
|
||||
|q:| [count]q: edit : command-line in command-line window,
|
||||
with [count]: switch to |Ex-mode|
|
||||
|q/| q/ edit / command-line in command-line window
|
||||
@@ -430,7 +431,7 @@ Tag Char Note Normal-mode action ~
|
||||
|<C-End>| <C-End> 1 same as "G"
|
||||
|<C-Home>| <C-Home> 1 same as "gg"
|
||||
|<C-Left>| <C-Left> 1 same as "b"
|
||||
|<C-LeftMouse>| <C-LeftMouse> ":ta" to the keyword at the mouse click
|
||||
|<C-LeftMouse>| <C-LeftMouse> place a multicursor at mouse-click
|
||||
|<C-Right>| <C-Right> 1 same as "w"
|
||||
|<C-RightMouse>| <C-RightMouse> same as "CTRL-T"
|
||||
|<C-Tab>| <C-Tab> same as "g<Tab>"
|
||||
@@ -632,6 +633,7 @@ Tag Char Note Normal-mode action ~
|
||||
|[star| [* 1 same as "[/"
|
||||
|[`| [` 1 cursor to previous lowercase mark
|
||||
|[/| [/ 1 cursor to N previous start of a C comment
|
||||
|[C| [C 1 jump to N previous multicursor
|
||||
|[D| [D list all defines found in current and
|
||||
included files matching the word under the
|
||||
cursor, start searching at beginning of
|
||||
@@ -674,6 +676,7 @@ Tag Char Note Normal-mode action ~
|
||||
|]star| ]* 1 same as "]/"
|
||||
|]`| ]` 1 cursor to next lowercase mark
|
||||
|]/| ]/ 1 cursor to N next end of a C comment
|
||||
|]C| ]C 1 jump to N next multicursor
|
||||
|]D| ]D list all #defines found in current and
|
||||
included files matching the word under the
|
||||
cursor, start searching at cursor position
|
||||
@@ -706,6 +709,8 @@ Tag Char Note Normal-mode action ~
|
||||
|
||||
Tag Char Note Normal-mode action ~
|
||||
------------------------------------------------------------------------------ ~
|
||||
|g_CTRL-A| g CTRL-A insert an ascending number at each
|
||||
multicursor ("counter")
|
||||
|g_CTRL-G| g CTRL-G show information about current cursor
|
||||
position
|
||||
|g_CTRL-H| g CTRL-H start Select block mode
|
||||
@@ -796,7 +801,7 @@ Tag Char Note Normal-mode action ~
|
||||
|g<End>| g<End> 1 same as "g$" but go to the rightmost
|
||||
non-blank character instead
|
||||
|g<Home>| g<Home> 1 same as "g0"
|
||||
|g<LeftMouse>| g<LeftMouse> same as <C-LeftMouse>
|
||||
|g<LeftMouse>| g<LeftMouse> ":tag" on the keyword at mouse-click
|
||||
g<MiddleMouse> same as <C-MiddleMouse>
|
||||
|g<RightMouse>| g<RightMouse> same as <C-RightMouse>
|
||||
|g<Tab>| g<Tab> go to last accessed tabpage
|
||||
|
||||
@@ -386,7 +386,7 @@ a command that takes an argument, such as |f| or |m|, the timeout set with
|
||||
*CTRL-\_CTRL-G* *i_CTRL-\_CTRL-G* *c_CTRL-\_CTRL-G* *v_CTRL-\_CTRL-G*
|
||||
CTRL-\ CTRL-G works the same as |CTRL-\_CTRL-N| for backward compatibility.
|
||||
|
||||
*:exm* *:exmode* *gQ* *mode-Ex* *Ex-mode* *Ex* *EX* *E501*
|
||||
*:exm* *:exmode* *mode-Ex* *Ex-mode* *Ex* *EX* *E501*
|
||||
:exm[ode]
|
||||
[count]q: Switch to Ex mode. This is like the |cmdwin| except
|
||||
it stays open, in Insert-mode, so can you keep
|
||||
|
||||
@@ -219,7 +219,7 @@ Call |vim.lsp.enable()| to ENABLE a config: it will automatically ACTIVATE
|
||||
|
||||
- 'filetype' matches one of the config `filetypes`
|
||||
- 'buftype' is empty or "help"
|
||||
- `root_markers` or `root_dir()` resolved a workspace roort, or the config has
|
||||
- `root_markers` or `root_dir()` resolved a workspace root, or the config has
|
||||
`workspace_required=false`
|
||||
|
||||
Note: The initial call to |vim.lsp.enable()| (or |:lsp-enable|) also actively
|
||||
|
||||
@@ -217,8 +217,6 @@ The following new features were added.
|
||||
• Navigating the |jumplist| with CTRL+O, CTRL+I behaves more intuitively
|
||||
when deleting buffers, and avoids "invalid buffer" cases. #25461
|
||||
• |:fclose| command.
|
||||
• |v_Q-default| and |v_@-default| repeat a register for each line of a linewise
|
||||
visual selection.
|
||||
• Clicking on a tabpage in the tabline with the middle mouse button closes it.
|
||||
• |:checkhealth| buffer can be opened in a split window using modifiers like
|
||||
|:vertical|, |:horizontal| and |:botright|.
|
||||
|
||||
@@ -270,6 +270,11 @@ DIAGNOSTICS
|
||||
|
||||
EDITOR
|
||||
|
||||
• Multiple cursors (|multicursor|): edit in many places at once.
|
||||
• Toggle cursors with |Q|, |<C-LeftMouse>|, or |nvim_mcursor()|.
|
||||
• Toggle "follow mode" with |q=|.
|
||||
• Clear cursors with |CTRL-L|, restore them with |gQ|.
|
||||
• [count]Q places a cursor at every search match.
|
||||
• |:bcd| sets a buffer-local directory. |getcwd()| and |haslocaldir()| take
|
||||
`bufnr` as a third parameter.
|
||||
• |:lcd!| |:tcd!| |:bcd!| unsets the directory of that scope.
|
||||
|
||||
@@ -40,9 +40,8 @@ selection included. See |visual-repeat|.
|
||||
==============================================================================
|
||||
Semantic repeat *action-repeat* *cmdatom* *excalibur*
|
||||
|
||||
The |CmdAtom| event is published on every user action. This avoids the need
|
||||
for plugins to "announce" the repeatable unit, thus plugins like vim-repeat
|
||||
aren't needed.
|
||||
Every user action emits a |CmdAtom| event. This avoids the need for plugins to
|
||||
"announce" the repeatable unit, thus plugins like vim-repeat aren't needed.
|
||||
|
||||
`CmdAtom.lhs` is the high-level user input collected during an action,
|
||||
including getchar() input. This is signficant: it reflects the semantic
|
||||
@@ -113,6 +112,12 @@ vim-repeat or similar. >lua
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', '.', function()
|
||||
-- Multicursors: degrade to builtin "." (cascades).
|
||||
local mc = vim.api.nvim_create_namespace('nvim.multicursor')
|
||||
if #vim.api.nvim_buf_get_extmarks(0, mc, 0, -1, { limit = 1 }) > 0 then
|
||||
vim.api.nvim_feedkeys('.', 'n', false)
|
||||
return
|
||||
end
|
||||
-- CmdAtom is deferred; schedule the replay, in case "." follows an edit.
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
@@ -295,18 +300,6 @@ q Stops recording.
|
||||
{Visual}@@ register for each selected line.
|
||||
See |visual-repeat|, |default-mappings|.
|
||||
|
||||
*Q*
|
||||
Q Repeat the last recorded register [count] times.
|
||||
See |reg_recorded()|.
|
||||
|
||||
*v_Q-default*
|
||||
{Visual}Q In linewise Visual mode, repeat the last recorded
|
||||
register for each selected line.
|
||||
See |visual-repeat|, |default-mappings|.
|
||||
|
||||
*v_Q*
|
||||
{Visual}Q Place a cursor on each line of the Visual selection.
|
||||
|
||||
*:@*
|
||||
:[addr]@{0-9a-z".=*+} Execute the contents of register {0-9a-z".=*+} as an
|
||||
Ex command. First set cursor at line [addr] (default
|
||||
@@ -334,7 +327,106 @@ Q Repeat the last recorded register [count] times.
|
||||
==============================================================================
|
||||
Multiple cursors *mcursor* *multicursor*
|
||||
|
||||
todo
|
||||
You can place extra cursors ("multicursors") in a buffer, to repeat operations
|
||||
at each cursor, as-you-type. This is like a live |macro|, but unlike macros (a
|
||||
stream of keys without any context), actions (|CmdAtom|) replay "semantically".
|
||||
|
||||
- Mappings and Normal-mode operators are applied at each cursor.
|
||||
- Insert-mode edits appear at each cursor as you type.
|
||||
- Motions (|q=|) and Visual-mode sequences replay at each cursor: "viweex"
|
||||
re-executes the selection there, so the extents are per-cursor (each
|
||||
cursor's own word, block, …).
|
||||
- Macros (|@|) replay at each cursor.
|
||||
- Registers are cursor-local. Each cursor reads/writes its own registers.
|
||||
The yanks are joined (linewise) when the multicursor session ends.
|
||||
- Undo is atomic: |u| reverts the aggregate edits from all cursors at once.
|
||||
- CTRL-C interrupts the cascade.
|
||||
|
||||
Example: place a cursor on every matching pattern: >
|
||||
:g/pattern/normal! nQ
|
||||
<
|
||||
Example: place a cursor at each |quickfix| item, or a range: >
|
||||
:cdo normal! Q
|
||||
:2,4cdo normal! Q
|
||||
<
|
||||
Example: place a cursor at every match, using the API: >lua
|
||||
for _, m in ipairs(vim.fn.matchbufline('%', [[pattern]], 1, '$')) do
|
||||
vim.api.nvim_mcursor(0, { m.lnum, m.byteidx })
|
||||
end
|
||||
<
|
||||
Plugins can place cursors with |nvim_mcursor()| and inspect user actions via
|
||||
the |CmdAtom| event.
|
||||
|
||||
The cursor positions are tracked as |extmarks| in the "nvim.multicursor"
|
||||
namespace. Query them with |nvim_buf_get_extmarks()|; deleting an extmark
|
||||
deletes the cursor. You can also use |:marks| to peek at cursors: >
|
||||
:marks nvim.multicursor
|
||||
<
|
||||
Multicursors are presented as |hl-MCursor| highlights, unless your (terminal)
|
||||
UI supports the Kitty multiple-cursors protocol.
|
||||
|
||||
|
||||
COMMANDS
|
||||
*Q*
|
||||
Q Toggles a multicursor at the current cursor position.
|
||||
Disables follow-mode |q=|.
|
||||
|
||||
With a [count]: places a cursor at every match (|gn|)
|
||||
of the last search pattern. Example:
|
||||
1. Search for something ("*", "/pattern<CR>", …).
|
||||
2. Type "1Q".
|
||||
|
||||
*mcursor-mouse* *<C-LeftMouse>*
|
||||
<C-LeftMouse> Toggles a multicursor at the click position, without
|
||||
moving the primary cursor (or changing windows). Does
|
||||
NOT disable follow-mode |q=|. No-op in Insert-mode.
|
||||
|
||||
*v_Q*
|
||||
{Visual}Q Places a cursor on each line of the Visual selection.
|
||||
Enables follow-mode |q=|.
|
||||
|
||||
*q=*
|
||||
q= Toggles follow-mode: motions (not jumps/scrolls) are
|
||||
replayed per-cursor. Cursors arriving at the same
|
||||
position (e.g. "G") are merged (deduplicated).
|
||||
|
||||
Use [count] to force the mode instead of toggling:
|
||||
"1q=" on, "2q=" off.
|
||||
|
||||
*mcursor-clear*
|
||||
CTRL-L Clears multicursors in the current buffer.
|
||||
|CTRL-L-default|
|
||||
|
||||
*gQ*
|
||||
gQ Restores the previous multicursors.
|
||||
|
||||
*g_CTRL-A*
|
||||
g CTRL-A During a multicursor session, inserts an ascending
|
||||
number ("counter") at each cursor, so a column of
|
||||
cursors becomes 1, 2, 3, …. Use [count] to choose the
|
||||
initial number.
|
||||
|
||||
*]C*
|
||||
]C Jump to the [count]'th next cursor.
|
||||
|
||||
*[C*
|
||||
[C Jump to the [count]'th previous cursor.
|
||||
|
||||
|
||||
LIMITATIONS *mcursor-limitations*
|
||||
|
||||
- Extra cursors on the same line can be lost or misplaced by an edit that
|
||||
shifts columns (e.g. inserting text): the cursors shift each other.
|
||||
- Undo restores the buffer, not (per-cursor) registers.
|
||||
- Time-travel undo (|g-|, |g+|, |:earlier|) clears all cursors.
|
||||
- Reloading/unloading a buffer (|:edit!|, 'autoread') clears its cursors
|
||||
(extmarks limitation).
|
||||
- An operator replayed at a cursor inside a closed fold applies to the whole
|
||||
fold, like any operator (|fold-behavior|).
|
||||
- |Q| is not allowed while recording or executing a macro.
|
||||
- An 'operatorfunc' that caches its |getchar()| input instead of re-reading it
|
||||
misbehaves at the extra cursors: the unconsumed input runs as a normal-mode
|
||||
key.
|
||||
|
||||
|
||||
vim:tw=78:ts=8:noet:ft=help:norl:
|
||||
|
||||
@@ -5376,6 +5376,10 @@ CursorLineSign Like SignColumn when 'cursorline' is set for the cursor line.
|
||||
*hl-MatchParen*
|
||||
MatchParen Character under the cursor or just before it, if it
|
||||
is a paired bracket, and its match. |pi_paren.txt|
|
||||
*hl-MCursor*
|
||||
MCursor |multicursor| cursor.
|
||||
*hl-MCursorVisual*
|
||||
MCursorVisual |multicursor| Visual selection.
|
||||
*hl-ModeMsg*
|
||||
ModeMsg 'showmode' message (e.g., "-- INSERT --").
|
||||
*hl-MsgArea*
|
||||
|
||||
@@ -41,8 +41,7 @@ below.
|
||||
first one is jumped to. See |tag-matchlist| for
|
||||
jumping to other matching tags.
|
||||
|
||||
g<LeftMouse> *g<LeftMouse>*
|
||||
<C-LeftMouse> *<C-LeftMouse>* *CTRL-]*
|
||||
g<LeftMouse> *g<LeftMouse>* *CTRL-]*
|
||||
CTRL-] Jump to the definition of the keyword under the
|
||||
cursor. Same as ":tag {name}", where {name} is the
|
||||
keyword under or after cursor.
|
||||
|
||||
@@ -83,7 +83,7 @@ you want to remember fewer or more lines.
|
||||
|
||||
This defines a key mapping. More about that in the next section. This
|
||||
defines the "Q" command to do formatting with the "gq" operator. Otherwise the
|
||||
"Q" command repeats the last recorded register.
|
||||
"Q" command adds a |multicursor|.
|
||||
>
|
||||
vnoremap _g y:exe "grep /" .. escape(@", '\\/') .. "/ *.c *.h"<CR>
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ CTRL-L Clears and redraws the screen. The redraw may happen
|
||||
See also |nvim__redraw()|.
|
||||
*CTRL-L-default*
|
||||
By default, also clears search highlighting
|
||||
|:nohlsearch| and updates diffs |:diffupdate|.
|
||||
|default-mappings|
|
||||
|:nohlsearch|, removes |multicursor|s, and updates
|
||||
diffs |:diffupdate|. |default-mappings|
|
||||
|
||||
*:mod* *:mode* *E359*
|
||||
:mod[e] Clears and redraws the screen.
|
||||
|
||||
@@ -235,6 +235,7 @@ MAJOR COMPONENTS
|
||||
- Job control |job-control|
|
||||
- LSP framework |lsp|
|
||||
- Lua scripting |lua| |-l|
|
||||
- Multicursor |multicursor|
|
||||
- Parsing engine |treesitter|
|
||||
- Plugin manager |vim.pack|
|
||||
- Providers
|
||||
|
||||
@@ -393,9 +393,9 @@ operator: >lua
|
||||
end)
|
||||
<
|
||||
|
||||
Visual mode |default-mappings| "@" and "Q" repeat a register for all selected
|
||||
lines if the selection is linewise. See |v_@-default| and |v_Q-default| for
|
||||
details. For example, given the text:
|
||||
The Visual mode |default-mappings| "@" repeats a register for all selected
|
||||
lines if the selection is linewise. See |v_@-default| for details. For
|
||||
example, given the text:
|
||||
|
||||
123(hello)321
|
||||
456(world)654
|
||||
|
||||
@@ -728,10 +728,10 @@ v:termrequest
|
||||
|
||||
*v:termresponse* *termresponse-variable*
|
||||
v:termresponse
|
||||
The value of the most recent OSC or DCS control sequence
|
||||
received by Nvim from the terminal. This can be read in a
|
||||
|TermResponse| event handler after querying the terminal using
|
||||
another escape sequence.
|
||||
The most recent OSC, DCS, APC, or recognized CSI (DA1, kitty
|
||||
multiple-cursors) control sequence received by Nvim from the
|
||||
host-terminal. Can be read in a |TermResponse| event handler
|
||||
after querying the terminal.
|
||||
|
||||
*v:testing* *testing-variable*
|
||||
v:testing Must be set before using `test_garbagecollect_now()`.
|
||||
|
||||
@@ -112,9 +112,14 @@ do
|
||||
--- Use normal! <C-L> to prevent inserting raw <C-L> when using i_<C-O>. #17473
|
||||
---
|
||||
--- See |CTRL-L-default|
|
||||
vim.keymap.set('n', '<C-L>', '<Cmd>nohlsearch<Bar>diffupdate<Bar>normal! <C-L><CR>', {
|
||||
desc = ':help CTRL-L-default',
|
||||
})
|
||||
vim.keymap.set(
|
||||
'n',
|
||||
'<C-L>',
|
||||
'<Cmd>nohlsearch<Bar>diffupdate'
|
||||
.. '<Bar>call nvim_buf_clear_namespace(0, nvim_create_namespace("nvim.multicursor"), 0, -1)'
|
||||
.. '<Bar>normal! <C-L><CR>',
|
||||
{ desc = ':help CTRL-L-default' }
|
||||
)
|
||||
|
||||
--- Set undo points when deleting text in insert mode.
|
||||
---
|
||||
@@ -129,16 +134,10 @@ do
|
||||
--- See |&-default|
|
||||
vim.keymap.set('n', '&', ':&&<CR>', { desc = ':help &-default' })
|
||||
|
||||
--- Use Q in Visual mode to execute a macro on each line of the selection. #21422
|
||||
--- Use @ in Visual mode to execute a macro on each line of the selection. #21422
|
||||
--- This only make sense in linewise Visual mode. #28287
|
||||
---
|
||||
--- Applies to @x and includes @@ too.
|
||||
vim.keymap.set(
|
||||
'x',
|
||||
'Q',
|
||||
"mode() ==# 'V' ? ':normal! @<C-R>=reg_recorded()<CR><CR>' : 'Q'",
|
||||
{ silent = true, expr = true, desc = ':help v_Q-default' }
|
||||
)
|
||||
--- Includes @@ too.
|
||||
vim.keymap.set(
|
||||
'x',
|
||||
'@',
|
||||
@@ -991,10 +990,9 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
--- If the TUI (term_has_truecolor) was able to determine that the host
|
||||
--- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the
|
||||
--- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's
|
||||
--- response indicates that it does support truecolor enable 'termguicolors',
|
||||
--- If the TUI (term_has_truecolor) detected that the host terminal supports truecolor, enable
|
||||
--- 'termguicolors'. Otherwise, query the terminal (using both XTGETTCAP and SGR + DECRQSS). If
|
||||
--- the terminal's response indicates that it does support truecolor enable 'termguicolors',
|
||||
--- but only if the user has not already disabled it.
|
||||
---
|
||||
--- @param ui table<string,any> The attached TTY UI (see |nvim_list_uis()|).
|
||||
@@ -1088,6 +1086,7 @@ do
|
||||
end
|
||||
|
||||
detect_termguicolors(tty)
|
||||
require('vim._core.mcursor').detect(tty) -- Kitty multicursor protocol.
|
||||
|
||||
-- Show progress bars in supporting terminals
|
||||
nvim_on('Progress', vim.api.nvim_create_augroup('nvim.progress'), {
|
||||
@@ -1130,9 +1129,8 @@ do
|
||||
return
|
||||
end
|
||||
|
||||
-- 'termguicolors': enable when the attaching UI reports truecolor (or the
|
||||
-- terminal query confirms it), unless the user set it. Never disabled here.
|
||||
detect_termguicolors(ui)
|
||||
detect_termguicolors(ui) -- 'termguicolors'
|
||||
require('vim._core.mcursor').detect(ui) -- Kitty multicursor protocol.
|
||||
|
||||
-- 'background': (re)query OSC 11. The persistent handler also reacts to
|
||||
-- runtime theme changes (mode 2031 -> TUI re-queries -> |TermResponse|);
|
||||
|
||||
293
runtime/lua/vim/_core/mcursor.lua
Normal file
293
runtime/lua/vim/_core/mcursor.lua
Normal file
@@ -0,0 +1,293 @@
|
||||
--- Multicursor:
|
||||
--- - Display: kitty cursors if supported (`detect()`), else "highlight" cursors (hl-MCursor).
|
||||
--- https://github.com/kovidgoyal/kitty/blob/master/docs/multiple-cursors-protocol.rst
|
||||
--- - Commands: `[count]Q`, `{Visual}Q`, `gQ`, `]C`, `g CTRL-A`.
|
||||
--- - Script API: `active()`.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Extmarks tracking multicursor positions.
|
||||
local ns = vim.api.nvim_create_namespace('nvim.multicursor')
|
||||
--- Selection-end cursors, during a Visual selection.
|
||||
local vcur_ns = vim.api.nvim_create_namespace('nvim.multicursor.cursor')
|
||||
--- Kitty cursors protocol: host terminal supports the protocol.
|
||||
local tty_cursors = false
|
||||
local last_seq = '' ---@type string
|
||||
local pending = false
|
||||
|
||||
--- Gets the operative mcursors namespace, depending on the current state.
|
||||
--- @param buf integer
|
||||
local function display_ns(buf)
|
||||
if #vim.api.nvim_buf_get_extmarks(buf, vcur_ns, 0, -1, { limit = 1 }) > 0 then
|
||||
return vcur_ns
|
||||
end
|
||||
return ns
|
||||
end
|
||||
|
||||
--- Absolute (screen) coordinates ("2:row:col", 1-indexed) of mcursors in each visible window.
|
||||
--- Off-screen cursors are omitted.
|
||||
--- @return string[]
|
||||
local function coords()
|
||||
local r = {} ---@type string[]
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
-- Visible cursors only: constrain to the viewport, for performance.
|
||||
local top, bot = vim.fn.line('w0', win) - 1, vim.fn.line('w$', win) - 1
|
||||
local marks = vim.api.nvim_buf_get_extmarks(buf, display_ns(buf), { top, 0 }, { bot, -1 }, {})
|
||||
local last = vim.api.nvim_buf_line_count(buf)
|
||||
for _, m in ipairs(marks) do
|
||||
-- Marks can be stale by the time the (scheduled) refresh runs.
|
||||
if m[2] < last then
|
||||
local pos = vim.fn.screenpos(win, m[2] + 1, m[3] + 1)
|
||||
if pos.row > 0 and pos.col > 0 then
|
||||
r[#r + 1] = ('2:%d:%d'):format(pos.row, pos.col)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
--- Kitty cursors protocol: Sends a term sequence. Empty string ('') means clear.
|
||||
local function send(seq)
|
||||
if seq == last_seq then -- Skip redundant sequences.
|
||||
return
|
||||
end
|
||||
last_seq = seq
|
||||
vim.api.nvim_ui_send(seq == '' and '\027[>0;4 q' or seq)
|
||||
end
|
||||
|
||||
--- Kitty cursors protocol: Updates the cursors.
|
||||
local function refresh()
|
||||
local c = coords()
|
||||
-- Clear all extra cursors ("no cursor" over the full-screen rectangle), then set shape 29 (mimic
|
||||
-- primary) at each position.
|
||||
send(#c == 0 and '' or ('\027[>0;4 q\027[>29;%s q'):format(table.concat(c, ';')))
|
||||
end
|
||||
|
||||
--- Displays the mcursors. Invoked per-redraw while cursors exist.
|
||||
--- - Kitty cursors protocol: deferred to end of redraw cycle (display_end).
|
||||
--- - Else: highlight cells (the tracking extmarks only carry positions).
|
||||
---
|
||||
--- NOTE: The fake Visual selections ("nvim.multicursor.visual") are self-painting extmarks.
|
||||
--- TODO(justimk): could also do that for "nvim.multicursor" after #41576.
|
||||
local function display_win(_, _, bufnr, topline, botline)
|
||||
if tty_cursors then -- Terminal draws the cursors; emit once per redraw (on_end).
|
||||
pending = true
|
||||
return
|
||||
end
|
||||
local marks = vim.api.nvim_buf_get_extmarks(
|
||||
bufnr,
|
||||
display_ns(bufnr),
|
||||
{ topline, 0 },
|
||||
{ botline + 1, 0 },
|
||||
{}
|
||||
)
|
||||
local lastrow = vim.api.nvim_buf_line_count(bufnr)
|
||||
for _, m in ipairs(marks) do
|
||||
local row, col = m[2], m[3]
|
||||
-- Marks may be stale (undo/redo).
|
||||
if row < lastrow then
|
||||
-- TODO(justinmk): eliminate this text-vs-virtual handling. #41576
|
||||
local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, true)[1]
|
||||
if col >= #line then
|
||||
-- Past EOL (e.g. insert-mode "A"): overlay a virtual-space cell. #41576
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns, row, col, {
|
||||
ephemeral = true,
|
||||
virt_text = { { ' ', 'MCursor' } },
|
||||
virt_text_pos = 'overlay',
|
||||
priority = 4097, -- Cover the selection highlight.
|
||||
})
|
||||
else
|
||||
vim.api.nvim_buf_set_extmark(bufnr, ns, row, col, {
|
||||
ephemeral = true,
|
||||
end_col = col + 1,
|
||||
hl_group = 'MCursor',
|
||||
priority = 4097, -- Cover the selection highlight.
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Render cursors in one "frame" at redraw edge (on_end), when screen positions are final.
|
||||
local function display_end()
|
||||
if pending then
|
||||
pending = false
|
||||
refresh()
|
||||
end
|
||||
end
|
||||
|
||||
--- Enables/disables multicursor display handling.
|
||||
--- @param enable boolean
|
||||
function M.enable(enable)
|
||||
vim.api.nvim_set_decoration_provider(ns, enable and {
|
||||
on_win = display_win,
|
||||
on_end = display_end,
|
||||
} or {})
|
||||
if not enable then
|
||||
send('') -- Kitty cursors protocol: clear the UI-side cursors.
|
||||
end
|
||||
end
|
||||
|
||||
--- ]C/[C: Jumps to the [count]'th next/previous cursor.
|
||||
--- @param forward boolean
|
||||
--- @param count integer?
|
||||
--- @return boolean moved
|
||||
function M.jump(forward, count)
|
||||
local last = vim.api.nvim_buf_line_count(0)
|
||||
local positions = {} ---@type vim.Pos[]
|
||||
for _, m in ipairs(vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) do
|
||||
if m[2] < last then
|
||||
positions[#positions + 1] = vim.pos(0, m[2], m[3])
|
||||
end
|
||||
end
|
||||
local n = #positions
|
||||
if n == 0 then
|
||||
return false
|
||||
end
|
||||
table.sort(positions)
|
||||
local curpos = vim.pos.cursor(0)
|
||||
local steps = ((count or 1) - 1) % n
|
||||
local idx ---@type integer
|
||||
if forward then
|
||||
local i = 1 -- First cursor after current position. (n+1: none)
|
||||
while i <= n and positions[i] <= curpos do
|
||||
i = i + 1
|
||||
end
|
||||
idx = (i - 1 + steps) % n + 1
|
||||
else
|
||||
local i = n -- Last cursor before current position. (0: none)
|
||||
while i >= 1 and not (positions[i] < curpos) do
|
||||
i = i - 1
|
||||
end
|
||||
idx = (i - 1 - steps) % n + 1
|
||||
end
|
||||
vim.cmd [[normal! m']]
|
||||
vim.api.nvim_win_set_cursor(0, positions[idx]:to_cursor())
|
||||
return true
|
||||
end
|
||||
|
||||
--- Restores the previous multicursors.
|
||||
function M.restore()
|
||||
local last_ns = vim.api.nvim_create_namespace('nvim.multicursor.last')
|
||||
local lastrow = vim.api.nvim_buf_line_count(0)
|
||||
for _, m in ipairs(vim.api.nvim_buf_get_extmarks(0, last_ns, 0, -1, {})) do
|
||||
if m[2] < lastrow then
|
||||
vim.api.nvim_mcursor(0, { m[2] + 1, m[3] })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Places a cursor on each line of the Visual selection. Enables "follow mode" (q=).
|
||||
function M.visual()
|
||||
local vline = vim.fn.line('v') --[[@as integer]]
|
||||
local cline = vim.fn.line('.') --[[@as integer]]
|
||||
local first, last = math.min(vline, cline), math.max(vline, cline)
|
||||
local vcol = vim.fn.virtcol('.', true)[1] --[[@as integer]]
|
||||
vim.cmd.normal({ vim.keycode('<Esc>'), bang = true }) -- End Visual mode.
|
||||
--- Screen column (per-line: multibyte chars/tabs shift the byte<->screen mapping), or the
|
||||
--- past-EOL insertion point on shorter lines.
|
||||
--- @param lnum integer
|
||||
local function bytecol(lnum)
|
||||
if vim.fn.virtcol({ lnum, '$' }) <= vcol then
|
||||
return vim.fn.col({ lnum, '$' }) - 1
|
||||
end
|
||||
return vim.fn.virtcol2col(0, lnum, vcol) - 1
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(0, { first, bytecol(first) })
|
||||
for lnum = first, last do
|
||||
vim.api.nvim_mcursor(0, { lnum, bytecol(lnum) })
|
||||
end
|
||||
vim.cmd('norm! 1q=') -- "Follow" mode.
|
||||
end
|
||||
|
||||
--- "[count]Q": Places a multicursor at every match of the last search pattern.
|
||||
function M.matches()
|
||||
if vim.fn.getreg('/') == '' then
|
||||
require('vim._core.util').echo_err('E35: No previous regular expression')
|
||||
return
|
||||
end
|
||||
local view = vim.fn.winsaveview()
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
local pos = vim.fn.searchpos('', 'cW')
|
||||
while pos[1] ~= 0 do
|
||||
vim.api.nvim_mcursor(0, { pos[1], pos[2] - 1 })
|
||||
pos = vim.fn.searchpos('', 'W')
|
||||
end
|
||||
vim.fn.winrestview(view)
|
||||
end
|
||||
|
||||
--- Inserts an ascending number at each cursor (Emacs F3-counter): 1, 2, 3, ….
|
||||
--- Numbers ascend in cursor order (top-to-bottom, left-to-right); the primary counts too.
|
||||
--- @param start? integer First number (default 1)
|
||||
--- @param step? integer Increment (default 1)
|
||||
--- @param format? string %d-style format for each number (default "%d")
|
||||
function M.number(start, step, format)
|
||||
start = start or 1
|
||||
step = step or 1
|
||||
format = format or '%d'
|
||||
local pts = { vim.pos.cursor(0) } ---@type vim.Pos[]
|
||||
local lastrow = vim.api.nvim_buf_line_count(0)
|
||||
for _, m in ipairs(vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, {})) do
|
||||
if m[2] < lastrow then
|
||||
pts[#pts + 1] = vim.pos(0, m[2], m[3])
|
||||
end
|
||||
end
|
||||
table.sort(pts)
|
||||
-- Coincident cursors (e.g. primary sitting on a cursor) share a number slot.
|
||||
vim.list.unique(pts, function(p)
|
||||
return ('%d:%d'):format(p.row, p.col)
|
||||
end)
|
||||
-- Number in position order, but INSERT bottom-up in case of text shifting.
|
||||
for i = #pts, 1, -1 do
|
||||
local p = pts[i]
|
||||
vim.api.nvim_buf_set_text(
|
||||
0,
|
||||
p.row,
|
||||
p.col,
|
||||
p.row,
|
||||
p.col,
|
||||
{ format:format(start + (i - 1) * step) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
--- Kitty cursors protocol: enables terminal-driven cursor display.
|
||||
--- @param enable boolean
|
||||
function M.tty_cursors(enable)
|
||||
if tty_cursors == enable then
|
||||
return
|
||||
end
|
||||
tty_cursors = enable
|
||||
vim.api.nvim__redraw({ valid = false, flush = false })
|
||||
if not enable then
|
||||
send('') -- Clear the displayed terminal cursors.
|
||||
end
|
||||
end
|
||||
|
||||
--- Kitty cursors protocol: Queries the host terminal for protocol support, and enables it.
|
||||
--- @param ui table Id of the attached nvim_list_uis() TTY UI.
|
||||
function M.detect(ui)
|
||||
-- Query: "CSI > SP q"
|
||||
vim.tty.request('\027[> q', { chan = ui.chan }, function(resp)
|
||||
--- Response is a list of cursor shapes, e.g. "CSI > 1;2;3;29;30;40;100;101 SP q".
|
||||
local shapes = resp:match('^\027%[>([%d;]*) q$') ---@type string?
|
||||
if not shapes then
|
||||
return -- Not a reply to our query, keep listening.
|
||||
end
|
||||
-- Shape 29: cursors follow the primary cursor's shape.
|
||||
if vim.list_contains(vim.split(shapes, ';', { plain = true }), '29') then
|
||||
M.tty_cursors(true)
|
||||
end
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
--- True if multicursor is active in the current buffer.
|
||||
--- @return boolean
|
||||
function M.active()
|
||||
return #vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, { limit = 1 }) > 0
|
||||
end
|
||||
|
||||
return M
|
||||
13
runtime/lua/vim/_meta/api.gen.lua
generated
13
runtime/lua/vim/_meta/api.gen.lua
generated
@@ -119,6 +119,12 @@ function vim.api.nvim__inspect_cell(grid, row, col) end
|
||||
--- reach, so this function can be used to force a cache clear in a test.
|
||||
function vim.api.nvim__invalidate_glyph_cache() end
|
||||
|
||||
--- WARNING: This feature is experimental/unstable.
|
||||
---
|
||||
--- Returns true if a multicursor cascade is in-progress.
|
||||
--- @return boolean
|
||||
function vim.api.nvim__mcursor_cascading() end
|
||||
|
||||
--- WARNING: This feature is experimental/unstable.
|
||||
---
|
||||
--- Get the properties for namespace
|
||||
@@ -1674,6 +1680,13 @@ function vim.api.nvim_list_wins() end
|
||||
--- @return any
|
||||
function vim.api.nvim_load_context(dict) end
|
||||
|
||||
--- Adds a multicursor in the given buffer.
|
||||
---
|
||||
--- @param buf integer Buffer handle, or 0 for current buffer
|
||||
--- @param pos [integer, integer] (row, col) (1,0)-indexed cursor position (byte offset)
|
||||
--- @return integer # Total number of extra cursors.
|
||||
function vim.api.nvim_mcursor(buf, pos) end
|
||||
|
||||
--- @deprecated
|
||||
--- @param msg string
|
||||
--- @param log_level integer
|
||||
|
||||
8
runtime/lua/vim/_meta/vvars.gen.lua
generated
8
runtime/lua/vim/_meta/vvars.gen.lua
generated
@@ -773,10 +773,10 @@ vim.v.t_string = ...
|
||||
--- @type string
|
||||
vim.v.termrequest = ...
|
||||
|
||||
--- The value of the most recent OSC or DCS control sequence
|
||||
--- received by Nvim from the terminal. This can be read in a
|
||||
--- `TermResponse` event handler after querying the terminal using
|
||||
--- another escape sequence.
|
||||
--- The most recent OSC, DCS, APC, or recognized CSI (DA1, kitty
|
||||
--- multiple-cursors) control sequence received by Nvim from the
|
||||
--- host-terminal. Can be read in a `TermResponse` event handler
|
||||
--- after querying the terminal.
|
||||
--- @type string
|
||||
vim.v.termresponse = ...
|
||||
|
||||
|
||||
@@ -198,7 +198,9 @@ function M.hl_op(opts)
|
||||
local winid = api.nvim_get_current_win()
|
||||
|
||||
local state = hl_op_state[state_key]
|
||||
if state ~= nil and state.timer and not state.timer:is_closing() then
|
||||
-- Multicursor cascade: accumulate per-cursor, don't cancel the previous event's highlight.
|
||||
local cascading = api.nvim__mcursor_cascading()
|
||||
if state ~= nil and state.timer and not state.timer:is_closing() and not cascading then
|
||||
state.timer:close()
|
||||
assert(state.clear)
|
||||
state.clear()
|
||||
|
||||
@@ -48,9 +48,9 @@ void nvim_error_event(uint64_t channel_id, Integer type, String msg)
|
||||
///
|
||||
/// Supports these events:
|
||||
///
|
||||
/// - "termresponse": The host-terminal sent a DA1, OSC, DCS, or APC response sequence to Nvim.
|
||||
/// The payload is the received response. Sets |v:termresponse| and fires
|
||||
/// |TermResponse|.
|
||||
/// - "termresponse": The host-terminal sent an OSC, DCS, APC, or recognized CSI (DA1, kitty
|
||||
/// multiple-cursors) response to Nvim. The payload is the received response. Sets
|
||||
/// |v:termresponse| and fires |TermResponse|.
|
||||
///
|
||||
/// @param channel_id
|
||||
/// @param event Event name
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
#include "nvim/mark_defs.h"
|
||||
#include "nvim/math.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/memory_defs.h"
|
||||
@@ -1452,6 +1453,40 @@ void nvim_put(ArrayOf(String) lines, String type, Boolean after, Boolean follow,
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a multicursor in the given buffer.
|
||||
///
|
||||
/// @param buf Buffer handle, or 0 for current buffer
|
||||
/// @param pos (row, col) (1,0)-indexed cursor position (byte offset)
|
||||
/// @param[out] err Error details, if any
|
||||
/// @return Total number of extra cursors.
|
||||
Integer nvim_mcursor(Buffer buf, ArrayOf(Integer, 2) pos, Error *err)
|
||||
FUNC_API_SINCE(15)
|
||||
{
|
||||
buf_T *b = find_buffer_by_handle(buf, err);
|
||||
if (b == NULL) {
|
||||
return 0;
|
||||
}
|
||||
VALIDATE_EXP(!(pos.size != 2 || pos.items[0].type != kObjectTypeInteger
|
||||
|| pos.items[1].type != kObjectTypeInteger), "pos", "[row, col] array", NULL, {
|
||||
return 0;
|
||||
});
|
||||
|
||||
int64_t row = pos.items[0].data.integer;
|
||||
int64_t col = pos.items[1].data.integer;
|
||||
|
||||
VALIDATE_RANGE(!(row < 1 || row > b->b_ml.ml_line_count), "cursor line", {
|
||||
return 0;
|
||||
});
|
||||
VALIDATE_RANGE(!(col > MAXCOL || col < 0), "cursor column", {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Silently clamp to the EOL insertion point, like nvim_win_set_cursor().
|
||||
col = MIN(col, (int64_t)ml_get_buf_len(b, (linenr_T)row));
|
||||
mc_add(b, (pos_T){ .lnum = (linenr_T)row, .col = (colnr_T)col, .coladd = 0 });
|
||||
return (Integer)mc_count();
|
||||
}
|
||||
|
||||
/// Returns the 24-bit RGB value of a |nvim_get_color_map()| color name or
|
||||
/// "#rrggbb" hexadecimal string.
|
||||
///
|
||||
@@ -2082,6 +2117,14 @@ void nvim__invalidate_glyph_cache(void)
|
||||
must_redraw = UPD_CLEAR;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
/// Returns true if a multicursor cascade is in-progress.
|
||||
Boolean nvim__mcursor_cascading(void)
|
||||
FUNC_API_SINCE(15) FUNC_API_FAST
|
||||
{
|
||||
return mc_replaying();
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
Object nvim__unpack(String str, Arena *arena, Error *err)
|
||||
FUNC_API_FAST
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
#include "nvim/mark.h"
|
||||
#include "nvim/mark_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memfile_defs.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memline_defs.h"
|
||||
@@ -813,6 +814,7 @@ void buf_clear(void)
|
||||
{
|
||||
linenr_T line_count = curbuf->b_ml.ml_line_count;
|
||||
extmark_free_all(curbuf); // delete any extmarks
|
||||
mc_buf_free(curbuf); // Multicursors died with their extmarks.
|
||||
while (!(curbuf->b_ml.ml_flags & ML_EMPTY)) {
|
||||
ml_delete(1);
|
||||
}
|
||||
@@ -924,6 +926,7 @@ bool buf_freeall(buf_T *buf, int flags)
|
||||
linenr_T count = buf->b_ml.ml_line_count;
|
||||
ml_close(buf, true); // close and delete the memline/memfile
|
||||
buf->b_ml.ml_line_count = 0; // no lines in buffer
|
||||
mc_buf_clear(buf); // Tracked mc positions died with the text.
|
||||
|
||||
// Ensure marks are adjusted for cleared buffer in case buffer not on disk:
|
||||
// if it is reloaded the buffer will be empty.
|
||||
@@ -1020,6 +1023,7 @@ static void free_buffer_stuff(buf_T *buf, int free_flags)
|
||||
}
|
||||
uc_clear(&buf->b_ucmds); // clear local user commands
|
||||
extmark_free_all(buf); // delete any extmarks
|
||||
mc_buf_free(buf); // Multicursors died with their extmarks.
|
||||
map_clear_mode(buf, MAP_ALL_MODES, true, false); // clear local mappings
|
||||
map_clear_mode(buf, MAP_ALL_MODES, true, true); // clear local abbrevs
|
||||
XFREE_CLEAR(buf->b_start_fenc);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// Related:
|
||||
// - vim.with()
|
||||
// - switch_option_context(), restore_option_context()
|
||||
// - McSandbox: input-replay guard. Sibling axis to ctx_switch() and ctx_save().
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
@@ -22,7 +22,7 @@ typedef struct {
|
||||
} Context;
|
||||
typedef kvec_t(Context) ContextVec;
|
||||
|
||||
#define CONTEXT_INIT (Context) { \
|
||||
#define CONTEXT_INIT { \
|
||||
.pos = { 0 }, \
|
||||
.mark = 0, \
|
||||
.curswant = -1, \
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "nvim/globals.h"
|
||||
#include "nvim/map_defs.h"
|
||||
#include "nvim/marktree.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/pos_defs.h"
|
||||
@@ -251,6 +252,9 @@ bool extmark_clear(buf_T *buf, uint32_t ns_id, int l_row, colnr_T l_col, int u_r
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multicursor extmarks are about to die, let mc snapshot them.
|
||||
mc_ns_clearing(buf, ns_id);
|
||||
|
||||
bool all_ns = (ns_id == 0);
|
||||
uint32_t *ns = NULL;
|
||||
if (!all_ns) {
|
||||
@@ -295,6 +299,9 @@ bool extmark_clear(buf_T *buf, uint32_t ns_id, int l_row, colnr_T l_col, int u_r
|
||||
|
||||
if (marks_cleared_any) {
|
||||
decor_state_invalidate(buf);
|
||||
// Deleting the "nvim.multicursor" namespace deletes the cursors it tracked.
|
||||
// TODO(justinmk): ideally, clearing a ns could be handled in userspace, e.g. an event?
|
||||
mc_ns_cleared(buf, ns_id);
|
||||
}
|
||||
|
||||
return marks_cleared_any;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memfile.h"
|
||||
#include "nvim/memfile_defs.h"
|
||||
#include "nvim/memline.h"
|
||||
@@ -3168,6 +3169,8 @@ void buf_reload(buf_T *buf, int orig_mode, bool reload_options)
|
||||
// Set curwin/curbuf for "buf" and save some things.
|
||||
ctx_switch(&aco, NULL, NULL, buf, 0);
|
||||
|
||||
mc_buf_clear(buf); // Multicursor: file-reload invalidates the extmarks.
|
||||
|
||||
// Unless reload_options is set, we only want to read the text from the
|
||||
// file, not reset the syntax highlighting, clear marks, diff status, etc.
|
||||
// Force the fileformat and encoding to be the same.
|
||||
|
||||
@@ -183,6 +183,8 @@ static const char *highlight_init_both[] = {
|
||||
"default link ComplMatchIns NONE",
|
||||
"default link ComplHint NonText",
|
||||
"default link ComplHintMore MoreMsg",
|
||||
"default link MCursor Cursor",
|
||||
"default link MCursorVisual Visual",
|
||||
"default link Substitute Search",
|
||||
"default link StatusLineTerm StatusLine",
|
||||
"default link StatusLineTermNC StatusLineNC",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mapping.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/normal.h"
|
||||
#include "nvim/normal_defs.h"
|
||||
@@ -44,32 +45,12 @@
|
||||
|
||||
#include "input_cmdatom.c.generated.h"
|
||||
|
||||
static bool mc_replaying(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static void mc_vsel_refresh(void)
|
||||
{
|
||||
}
|
||||
|
||||
static void mc_vsel_clear(void)
|
||||
{
|
||||
}
|
||||
|
||||
static bool mc_following(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static void mc_clock_edge(bool map_edit)
|
||||
{
|
||||
}
|
||||
|
||||
CmdAtomVec g_atoms = KV_INITIAL_VALUE;
|
||||
/// Capture clock: ticks on any kind of capture (atom push, Visual subatom). Used to answer "was
|
||||
/// anything captured during this command (including its nested frames)?".
|
||||
static uint64_t atom_captures = 0;
|
||||
/// "Cursor-global" op (undo, "g CTRL-A"), possibly in a nested frame. Command must not cascade.
|
||||
static uint64_t global_ops = 0;
|
||||
/// Suppresses atom pushes.
|
||||
static bool atom_suppressed = false;
|
||||
/// Mapping edited the buffer, or its insert-session cascaded: cascades as one unit, incl. motions.
|
||||
@@ -120,7 +101,6 @@ static struct {
|
||||
char *cmdline; ///< The ":" payload captured at cmdline accept. NULL: none.
|
||||
///< Note: search payloads ("/pat<CR>") travel on `cmdarg.searchbuf`.
|
||||
bool ins_cascaded; ///< Did the command's insert-session already cascade?
|
||||
bool op_global; ///< Already applied to every cursor (undo, "g CTRL-A"): must not cascade.
|
||||
} curcmd;
|
||||
|
||||
/// Interactively typed keys of the executing command. Collected during a composite (its `lhs`
|
||||
@@ -238,6 +218,7 @@ pos_T atom_origin_pos(buf_T *buf)
|
||||
/// @return Allocated key sequence.
|
||||
static char *atom_redo_keys(CmdSpec spec)
|
||||
{
|
||||
assert(!mc_replaying()); // Captured material is never re-captured.
|
||||
char *keys = redo_keys(&spec).data;
|
||||
assert(keys != NULL); // A spec with no chars/count/reg composes to nothing.
|
||||
return keys;
|
||||
@@ -468,7 +449,7 @@ static void atom_push(bool cascade, CmdAtom *atom)
|
||||
atom_free(atom);
|
||||
return;
|
||||
}
|
||||
atom_push_raw(cascade, atom);
|
||||
atom_push_raw(cascade && mc_buf_has_cursors(curbuf), atom);
|
||||
}
|
||||
|
||||
/// Stages an atom built before its command executes (do_pending_operator() prep-exempt, Visual
|
||||
@@ -561,21 +542,15 @@ static void atom_composite_end(void)
|
||||
char *lhs = atom_composite_lhs();
|
||||
XFREE_CLEAR(composite.lhs);
|
||||
CmdAtom atom;
|
||||
if (kv_size(composite.atoms) == 0) {
|
||||
// The mapping's commands captured nothing (Ex/Lua commands, no-ops): but it is still a user
|
||||
// action, so emit it with empty keys (identified by `lhs`).
|
||||
atom = (CmdAtom){ .type = kAMapping, .keys = xstrdup(""), .lhs = lhs, .remap = remap,
|
||||
.origin = composite.origin,
|
||||
.changed = atom_origin_changed(composite.origin),
|
||||
.moved = atom_origin_moved(composite.origin),
|
||||
.undoseq = atom_origin_undoseq(composite.origin) };
|
||||
} else if (kv_size(composite.atoms) == 1) {
|
||||
if (kv_size(composite.atoms) == 1) {
|
||||
// Single subatom. "Unwrap" it so e.g. a motion mapping reports kAMotion, not kAMapping.
|
||||
atom = kv_pop(composite.atoms);
|
||||
xfree(atom.lhs);
|
||||
atom.lhs = lhs;
|
||||
atom.remap = remap;
|
||||
} else {
|
||||
// Zero subatoms (captured nothing (Ex/Lua, no-op); still a user action, identified by `lhs`),
|
||||
// or multiple subatoms.
|
||||
atom = (CmdAtom){ .type = kAMapping, .keys = atoms_concat_keys(composite.atoms).data,
|
||||
.lhs = lhs, .remap = remap, .origin = composite.origin,
|
||||
.changed = atom_origin_changed(composite.origin),
|
||||
@@ -612,7 +587,8 @@ void atom_term_enter(void)
|
||||
bool atom_is_user_cmd(void)
|
||||
{
|
||||
// reg_executing is already reset for a macro's LAST command (its trailing "x" stuffs "dl").
|
||||
return ((reg_executing == 0 && !pending_end_reg_executing) || composite.macro)
|
||||
return !mc_replaying()
|
||||
&& ((reg_executing == 0 && !pending_end_reg_executing) || composite.macro)
|
||||
&& ex_normal_busy == 0;
|
||||
}
|
||||
|
||||
@@ -646,13 +622,13 @@ static bool atom_capturable(bool consumers, bool keytyped)
|
||||
/// True if anything consumes atoms from `curbuf`. For performance: skip capture if no consumers.
|
||||
static bool atom_buf_has_consumers(void)
|
||||
{
|
||||
return has_event(EVENT_CMDATOM);
|
||||
return mc_buf_has_cursors(curbuf) || has_event(EVENT_CMDATOM);
|
||||
}
|
||||
|
||||
/// XXX: Checks consumers for ANY buffer: a mapping/macro may navigate into a buffer w/ cursors...
|
||||
static bool atom_has_consumers(void)
|
||||
{
|
||||
return has_event(EVENT_CMDATOM);
|
||||
return mc_count() > 0 || has_event(EVENT_CMDATOM);
|
||||
}
|
||||
|
||||
/// Classifies key/command `cmd` (`arg` is its argument char, for two-char commands like "g;").
|
||||
@@ -742,12 +718,11 @@ unsigned atom_key_class(int cmd, int arg)
|
||||
/// Captures an accepted ":" or "<Cmd>" cmdline payload.
|
||||
void atom_cmdline_set(int firstc, const char *line, size_t len)
|
||||
{
|
||||
// Not for nested cmdlines opened by a command's own execution (":normal", macros): they would
|
||||
// overwrite the user command's payload, e.g. `:exe "normal! :echo 1\r"`.
|
||||
if (!atom_is_user_cmd() || (firstc != ':' && firstc != K_COMMAND)) {
|
||||
// Not for nested cmdlines (":norm", macros), nor a second accept (":put ." => ":put _"
|
||||
// translation): the first accept is the payload.
|
||||
if (!atom_is_user_cmd() || (firstc != ':' && firstc != K_COMMAND) || curcmd.cmdline != NULL) {
|
||||
return;
|
||||
}
|
||||
xfree(curcmd.cmdline);
|
||||
curcmd.cmdline = xmemdupz(line, len);
|
||||
}
|
||||
|
||||
@@ -834,11 +809,12 @@ static void atom_redo_reset(void)
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks the running command as already applying to every cursor (see `curcmd.op_global`).
|
||||
/// Called by u_doit() and mc_counter().
|
||||
void atom_op_global_set(void)
|
||||
/// Flags the running command as already applying to every cursor. E.g.: undo, "g CTRL-A"
|
||||
void atom_did_global_op(void)
|
||||
{
|
||||
curcmd.op_global = true;
|
||||
if (!mc_replaying()) {
|
||||
global_ops++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares that the current frame prepped redo. Not for nested frames (":norm").
|
||||
@@ -885,7 +861,7 @@ void atom_stuff_start(const cmdarg_T *cap)
|
||||
/// @param peeked Resolved by a peek: the executing command did not consume the mapping's keys.
|
||||
void atom_map_start(const char *lhs, size_t len, bool peeked)
|
||||
{
|
||||
if (!atom_has_consumers()
|
||||
if (!atom_has_consumers() || mc_replaying()
|
||||
|| reg_executing != 0 || ex_normal_busy != 0 || !(State & MODE_NORMAL)
|
||||
|| Visual.active) {
|
||||
return;
|
||||
@@ -1030,6 +1006,9 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
/// @return True if redo was prepped.
|
||||
bool atom_visual_end(CmdSpec spec, bool redoable)
|
||||
{
|
||||
if (mc_replaying()) {
|
||||
return false;
|
||||
}
|
||||
return atom_visual_end_suffix(atom_redo_keys(spec), &spec, redoable);
|
||||
}
|
||||
|
||||
@@ -1176,10 +1155,10 @@ InsSession atom_ins_start(int cmd, long count, VisualIns vis, bool vblock)
|
||||
// The selection is consumed: already in the redo body. Also clears selection display.
|
||||
atom_visual_reset();
|
||||
}
|
||||
// bool repl = cmd == 'R' || cmd == 'V' || cmd == 'r' || cmd == 'v';
|
||||
// mc_ins_cascade_start(session.typed && count <= 1 && !repl
|
||||
// && (vis == kVInsNone || (vis == kVInsKeys && !vblock)),
|
||||
// session.origin.tick);
|
||||
bool repl = cmd == 'R' || cmd == 'V' || cmd == 'r' || cmd == 'v';
|
||||
mc_ins_cascade_start(session.typed && count <= 1 && !repl
|
||||
&& (vis == kVInsNone || (vis == kVInsKeys && !vblock)),
|
||||
session.origin.tick);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1193,6 +1172,17 @@ void atom_ins_end(const InsSession *session, bool busy)
|
||||
bool user_input = session->typed
|
||||
// A session is user input, if user input occurred during it. #41516
|
||||
|| maptick != session->origin.maptick;
|
||||
if (mc_replaying()) {
|
||||
return;
|
||||
}
|
||||
if (mc_ins_commit()) {
|
||||
curcmd.ins_cascaded = true;
|
||||
// Not during a mapping: there the spans are subatoms of its composite.
|
||||
if (has_event(EVENT_CMDATOM) && !atom_composite_active()) {
|
||||
atom_ins_push(session, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!user_input || busy || restart_edit != 0 || !atom_buf_has_consumers()
|
||||
|| (visual && session->vis != kVInsKeys)) {
|
||||
if (user_input && (busy || restart_edit != 0) && atom_composite_active()) {
|
||||
@@ -1201,7 +1191,7 @@ void atom_ins_end(const InsSession *session, bool busy)
|
||||
}
|
||||
return;
|
||||
}
|
||||
atom_ins_push(session, true);
|
||||
atom_ins_push(session, mc_buf_has_cursors(curbuf));
|
||||
}
|
||||
|
||||
/// Pushes the ended insert-session as one atom. Skips a session not ending in <Esc>, except
|
||||
@@ -1231,9 +1221,10 @@ void atom_cmd_start(CmdFrame *old)
|
||||
old->visual = Visual;
|
||||
old->keytyped = KeyTyped;
|
||||
old->captures = atom_captures;
|
||||
old->global_ops = global_ops;
|
||||
old->id = ++frame_id;
|
||||
// Sampled: "q=" toggled DURING a command must not apply to it retroactively.
|
||||
old->follow = false;
|
||||
old->follow = mc_following();
|
||||
old->consumers = atom_buf_has_consumers();
|
||||
// Diffed at command end: detects a register-write (yank).
|
||||
old->reg_ts = old->consumers ? reg_max_ts(true) : 0;
|
||||
@@ -1242,7 +1233,6 @@ void atom_cmd_start(CmdFrame *old)
|
||||
old->payload_end = 0;
|
||||
old->parent = cur_frame;
|
||||
cur_frame = old;
|
||||
curcmd.op_global = false;
|
||||
atom_redo_reset();
|
||||
}
|
||||
|
||||
@@ -1285,8 +1275,8 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
// Command from a mapping's RHS (typed keys have KeyTyped set).
|
||||
bool mapped = user && !old->keytyped && !synthetic;
|
||||
if (mapped
|
||||
// NOT if cursor-global op: cascading (e.g. vim-repeat "nmap u") would undo at every cursor.
|
||||
&& !curcmd.op_global
|
||||
// NOT if cursor-global op (cascading per-cursor would be nonsense).
|
||||
&& global_ops == old->global_ops
|
||||
// NOT if it ended in another buffer: a navigation mapping ("nnoremap <M-l> <C-w>l")
|
||||
// entering a buffer with cursors must not cascade.
|
||||
&& ((curbuf == old->origin.buf.br_buf && atom_origin_changed(old->origin)) || ins_cascaded)) {
|
||||
@@ -1346,11 +1336,10 @@ static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
bool scroll_cmd = (keycls & (kKeyScrollMove | kKeyScrollView)) != 0;
|
||||
bool mouse_cmd = (keycls & kKeyMouse) != 0;
|
||||
bool jump_cmd = (keycls & kKeyJump) != 0;
|
||||
// Replayable? Register prefix ('"x') is captured as part of the command it prefixes; "@x"/"Q"
|
||||
// are translations, their resolution is the atom stream.
|
||||
// Replayable? Register prefix ('"x') is captured as part of the command it prefixes; "@x" is
|
||||
// a translation, its resolution is the atom stream.
|
||||
bool replayable = (ca->cmdchar > 0 && ca->cmdchar < 0x100
|
||||
&& ca->cmdchar != '"' && ca->cmdchar != '@' && ca->cmdchar != 'Q'
|
||||
&& !scroll_cmd)
|
||||
&& ca->cmdchar != '"' && ca->cmdchar != '@' && !scroll_cmd)
|
||||
|| special_motion;
|
||||
bool changed = atom_origin_changed(old->origin);
|
||||
// Note: an operator's motion belongs to the operator (`finish_op`).
|
||||
|
||||
@@ -20,6 +20,7 @@ struct CmdFrame {
|
||||
VisualState visual; ///< Visual-mode state (active/start/mode are diffed).
|
||||
bool keytyped; ///< KeyTyped
|
||||
uint64_t captures; ///< Capture counter.
|
||||
uint64_t global_ops; ///< `global_ops` at entry.
|
||||
uint64_t id; ///< Identifies this frame (see `composite.frame`).
|
||||
bool follow; ///< mc_following() ("q=")
|
||||
bool consumers; ///< Capture is skipped if there are no consumers (for performance).
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
#include "nvim/input_defs.h"
|
||||
|
||||
// Concepts (see :help dev-cmdatom):
|
||||
// - atom, composite (atom with subatoms)
|
||||
// - insert-session
|
||||
// - INSERTION
|
||||
// - span
|
||||
// - atom, span, composite (atom with subatoms)
|
||||
// - insert-session, insertion
|
||||
// - payload
|
||||
// - replay, cascade, insert-cascade
|
||||
|
||||
typedef enum CmdAtomType {
|
||||
@@ -57,14 +56,14 @@ typedef struct {
|
||||
typedef struct CmdAtom CmdAtom;
|
||||
typedef kvec_t(CmdAtom) CmdAtomVec;
|
||||
|
||||
/// One repeatable operation. `keys` is the replay payload; `spec` is the structured form.
|
||||
/// One repeatable operation. `keys` is the replay bytes; `spec` is the structured form.
|
||||
struct CmdAtom {
|
||||
CmdSpec spec; ///< Structured fields.
|
||||
CmdAtomVec atoms; ///< Composite (multi-command mapping, Visual sequence): its subatoms,
|
||||
///< in order; their keys concatenate to `keys`. Empty for non-composite.
|
||||
char *keys; ///< Resolved keysequence (typeahead encoding), including `["x][count]` prefix
|
||||
///< (unlike `CmdSpec.body`, the raw unprefixed form).
|
||||
char *text; ///< Payload: insert-session text, or Ex/search cmdline.
|
||||
char *text; ///< Insert-session text, or Ex/search cmdline payload.
|
||||
char *lhs; ///< Unresolved user input: mapping LHS or macro register ("@q"), or Visual op.
|
||||
///< Label/hint, not replayed. NULL: untranslated, same as `keys`.
|
||||
CmdOrigin origin; ///< Pre-command state.
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "nvim/marktree_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memline_defs.h"
|
||||
#include "nvim/memory.h"
|
||||
@@ -381,6 +382,9 @@ static int insert_check(VimState *state)
|
||||
{
|
||||
InsertState *s = (InsertState *)state;
|
||||
|
||||
// Multicursor: insert-cascade, before entry ("A"/"o"/"cw"/…), and after every executed key.
|
||||
mc_ins_cascade();
|
||||
|
||||
if (!Ins.revins_legal) {
|
||||
Ins.revins_scol = -1; // reset on illegal motions
|
||||
} else {
|
||||
@@ -2240,6 +2244,7 @@ int stop_arrow(void)
|
||||
// The count is a spec field (not body bytes), so "[count]." replaces it ("3i…").
|
||||
prep_redo(false, false, (CmdSpec){ .count = 1, .cmd = 'i' });
|
||||
Ins.new_insert_skip = 2;
|
||||
mc_ins_cascade_restart();
|
||||
} else {
|
||||
// Cursor-move was captured (start_arrow()): the atom mc-cascade will replay it.
|
||||
// Only `last_insert` (the ". register, i_CTRL-A) restarts here, like Vim.
|
||||
@@ -2332,12 +2337,11 @@ static void stop_insert(pos_T *end_insert_pos, int esc, int nomove)
|
||||
// If a space was inserted for auto-formatting, remove it now.
|
||||
check_auto_format(true);
|
||||
|
||||
// If we just did an auto-indent, remove the white space from the end
|
||||
// of the line, and put the cursor back.
|
||||
// If we just did an auto-indent, remove the whitespace from EOL, and put the cursor back.
|
||||
// Do this when ESC was used or moving the cursor up/down.
|
||||
// Check for the old position still being valid, just in case the text
|
||||
// got changed unexpectedly.
|
||||
if (!nomove && Ins.did_ai
|
||||
// Check for the old position still being valid, just in case the text changed unexpectedly.
|
||||
// Not for span replay during a mc-session: its synthetic <Esc> ends the nested session early.
|
||||
if (!nomove && Ins.did_ai && !mc_ins_replaying()
|
||||
&& (esc || (vim_strchr(p_cpo, kCpoIndent) == NULL
|
||||
&& curwin->w_cursor.lnum != end_insert_pos->lnum))
|
||||
&& end_insert_pos->lnum <= curbuf->b_ml.ml_line_count) {
|
||||
@@ -3361,6 +3365,7 @@ static void ins_del(void)
|
||||
|| do_join(2, false, true, false, false) == FAIL) {
|
||||
vim_beep(kOptBoFlagBackspace);
|
||||
} else {
|
||||
mc_ins_join();
|
||||
curwin->w_cursor.col = temp;
|
||||
// Adjust orig_line_count in case more lines have been deleted than
|
||||
// have been added. That makes sure, that open_line() later
|
||||
@@ -3443,6 +3448,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
|
||||
|
||||
// Delete newline!
|
||||
if (curwin->w_cursor.col == 0) {
|
||||
mc_ins_join();
|
||||
linenr_T lnum = Ins.start.lnum;
|
||||
if (curwin->w_cursor.lnum == lnum || Ins.revins_on) {
|
||||
if (u_save((linenr_T)(curwin->w_cursor.lnum - 2),
|
||||
|
||||
@@ -13,8 +13,8 @@ typedef enum {
|
||||
kInsJump, ///< Non-replayable jump (mouse, <PageUp>, …): atom terminated (<Esc>).
|
||||
} InsArrow;
|
||||
|
||||
/// Insert-mode session state: the in-progress insert session, as one global "group" (Ins), so the
|
||||
/// insert session can be saved/restored as a whole around nested edit() sessions.
|
||||
/// Insert-mode session state: the in-progress insert session, as one global "group" (Ins), so it
|
||||
/// can be saved/restored around nested edit() sessions (mc_ins_save_state).
|
||||
typedef struct {
|
||||
pos_T start; ///< Start of current INSERTION: from here to cursor is the unit that
|
||||
///< undo, '[ and ". treat as one. Reanchored after cursor-move, C-G u.
|
||||
|
||||
1369
src/nvim/mcursor.c
Normal file
1369
src/nvim/mcursor.c
Normal file
File diff suppressed because it is too large
Load Diff
14
src/nvim/mcursor.h
Normal file
14
src/nvim/mcursor.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nvim/buffer_defs.h" // buf_T
|
||||
#include "nvim/context_defs.h" // IWYU pragma: keep (used by mcursor.h.generated.h)
|
||||
#include "nvim/eval/typval_defs.h" // varnumber_T
|
||||
#include "nvim/input_cmdatom_defs.h" // IWYU pragma: keep (used by mcursor.h.generated.h)
|
||||
#include "nvim/normal_defs.h" // IWYU pragma: keep (cmdarg_T, used by mcursor.h.generated.h)
|
||||
#include "nvim/pos_defs.h"
|
||||
#include "nvim/register_defs.h" // IWYU pragma: keep (yankreg_T, used by mcursor.h.generated.h)
|
||||
|
||||
#include "mcursor.h.generated.h"
|
||||
@@ -1880,6 +1880,27 @@ colnr_T ml_get_buf_len(buf_T *buf, linenr_T lnum)
|
||||
return buf->b_ml.ml_line_textlen - 1;
|
||||
}
|
||||
|
||||
/// Gets the charwise NL-joined text of range [start, end), 1-based lnum, 0-based col, end col
|
||||
/// exclusive, as an allocated String ("" if empty). Line range must be valid (start <= end); cols
|
||||
/// clamp to line-length.
|
||||
String ml_region_text(buf_T *buf, pos_T start, pos_T end)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
StringBuilder sb = KV_INITIAL_VALUE;
|
||||
for (linenr_T lnum = start.lnum; lnum <= end.lnum; lnum++) {
|
||||
char *line = ml_get_buf(buf, lnum);
|
||||
colnr_T len = ml_get_buf_len(buf, lnum);
|
||||
colnr_T from = lnum == start.lnum ? MIN(start.col, len) : 0;
|
||||
colnr_T to = lnum == end.lnum ? MIN(end.col, len) : len;
|
||||
kv_concat_len(sb, line + from, (size_t)(to - from));
|
||||
if (lnum < end.lnum) {
|
||||
kv_push(sb, NL);
|
||||
}
|
||||
}
|
||||
kv_push(sb, NUL);
|
||||
return cbuf_as_string(sb.items, kv_size(sb) - 1);
|
||||
}
|
||||
|
||||
/// @return codepoint at pos. pos must be either valid or have col set to MAXCOL!
|
||||
int gchar_pos(pos_T *pos)
|
||||
FUNC_ATTR_NONNULL_ARG(1)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include "nvim/buffer_defs.h"
|
||||
#include "nvim/buffer_updates.h"
|
||||
#include "nvim/channel.h"
|
||||
#include "nvim/context.h"
|
||||
#include "nvim/decoration_provider.h"
|
||||
#include "nvim/drawline.h"
|
||||
#include "nvim/errors.h"
|
||||
@@ -31,6 +30,7 @@
|
||||
#include "nvim/main.h"
|
||||
#include "nvim/map_defs.h"
|
||||
#include "nvim/mapping.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memfile.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/message.h"
|
||||
@@ -986,6 +986,7 @@ void free_all_mem(void)
|
||||
eval_clear();
|
||||
api_extmark_free_all_mem();
|
||||
atom_free_all();
|
||||
mc_free_all();
|
||||
|
||||
map_destroy(int, &buffer_handles);
|
||||
map_destroy(int, &window_handles);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "nvim/mark_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/menu.h"
|
||||
@@ -227,9 +228,10 @@ static void call_click_def_func(StlClickDefinition *click_defs, int col, int whi
|
||||
}
|
||||
|
||||
/// Translate window coordinates to buffer position without any side effects.
|
||||
/// Returns IN_BUFFER and sets "mpos->col" to the column when in buffer text.
|
||||
/// The column is one for the first column.
|
||||
static int get_fpos_of_mouse(pos_T *mpos)
|
||||
/// Returns IN_BUFFER and sets `mpos` (0-based column) when in buffer text.
|
||||
///
|
||||
/// @param wpp If not NULL: consider any window (not just `curwin`), and return it here.
|
||||
static int get_fpos_of_mouse(pos_T *mpos, win_T **wpp)
|
||||
{
|
||||
int grid = mouse_grid;
|
||||
int row = mouse_row;
|
||||
@@ -244,6 +246,9 @@ static int get_fpos_of_mouse(pos_T *mpos)
|
||||
if (wp == NULL) {
|
||||
return IN_UNKNOWN;
|
||||
}
|
||||
if (wpp != NULL) {
|
||||
*wpp = wp;
|
||||
}
|
||||
int winrow = row;
|
||||
int wincol = col;
|
||||
|
||||
@@ -276,7 +281,7 @@ static int get_fpos_of_mouse(pos_T *mpos)
|
||||
return IN_SEP_LINE;
|
||||
}
|
||||
|
||||
if (wp != curwin || below_buffer) {
|
||||
if ((wpp == NULL && wp != curwin) || below_buffer) {
|
||||
return IN_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -333,37 +338,7 @@ static int do_popup(int which_button, int m_pos_flag, pos_T m_pos)
|
||||
}
|
||||
|
||||
/// Do the appropriate action for the current mouse click in the current mode.
|
||||
/// Not used for Command-line mode.
|
||||
///
|
||||
/// Normal and Visual Mode:
|
||||
/// event modi- position visual change action
|
||||
/// fier cursor window
|
||||
/// left press - yes end yes
|
||||
/// left press C yes end yes "^]" (2)
|
||||
/// left press S yes end (popup: extend) yes "*" (2)
|
||||
/// left drag - yes start if moved no
|
||||
/// left relse - yes start if moved no
|
||||
/// middle press - yes if not active no put register
|
||||
/// middle press - yes if active no yank and put
|
||||
/// right press - yes start or extend yes
|
||||
/// right press S yes no change yes "#" (2)
|
||||
/// right drag - yes extend no
|
||||
/// right relse - yes extend no
|
||||
///
|
||||
/// Insert or Replace Mode:
|
||||
/// event modi- position visual change action
|
||||
/// fier cursor window
|
||||
/// left press - yes (cannot be active) yes
|
||||
/// left press C yes (cannot be active) yes "CTRL-O^]" (2)
|
||||
/// left press S yes (cannot be active) yes "CTRL-O*" (2)
|
||||
/// left drag - yes start or extend (1) no CTRL-O (1)
|
||||
/// left relse - yes start or extend (1) no CTRL-O (1)
|
||||
/// middle press - no (cannot be active) no put register
|
||||
/// right press - yes start or extend yes CTRL-O
|
||||
/// right press S yes (cannot be active) yes "CTRL-O#" (2)
|
||||
///
|
||||
/// (1) only if mouse pointer moved since press
|
||||
/// (2) only if click is in same buffer
|
||||
/// Not used for Command-line mode. Per-mode/button behavior: |mouse-mode-table|.
|
||||
///
|
||||
/// @param oap operator argument, can be NULL
|
||||
/// @param c K_LEFTMOUSE, etc
|
||||
@@ -438,6 +413,9 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
}
|
||||
stuffcharReadbuff(Ctrl_T);
|
||||
got_click = false; // ignore drag&release now
|
||||
if ((State & MODE_INSERT) == 0) {
|
||||
exec_stuffed(NULL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -489,10 +467,15 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
stuffcharReadbuff(Ctrl_G);
|
||||
stuffReadbuff("\"+p");
|
||||
} else {
|
||||
// Reg prefix must travel in the keys run by exec_stuffed. E.g. `"ay<MiddleMouse>`
|
||||
if (regname != 0) {
|
||||
stuffcharReadbuff('"');
|
||||
stuffcharReadbuff(regname);
|
||||
}
|
||||
stuffcharReadbuff('y');
|
||||
stuffcharReadbuff(K_MIDDLEMOUSE);
|
||||
}
|
||||
exec_stuffed(oap);
|
||||
exec_stuffed(NULL);
|
||||
return false;
|
||||
}
|
||||
// The rest is below jump_to_mouse()
|
||||
@@ -587,6 +570,21 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
}
|
||||
}
|
||||
|
||||
// Multicursor: CTRL-click toggles a cursor at click pos, without moving the primary cursor.
|
||||
// Not in quickfix: there CTRL-click jumps to the item. No-op during insert.
|
||||
if (is_click && (mod_mask & MOD_MASK_CTRL) && which_button == MOUSE_LEFT) {
|
||||
pos_T pos;
|
||||
win_T *wp = NULL;
|
||||
if (get_fpos_of_mouse(&pos, &wp) == IN_BUFFER && !bt_quickfix(wp->w_buffer)) {
|
||||
got_click = false; // ignore drag&release now
|
||||
if ((State & MODE_INSERT) == 0) {
|
||||
pos.coladd = 0;
|
||||
mc_toggle(wp->w_buffer, pos, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int m_pos_flag = 0;
|
||||
pos_T m_pos = { 0 };
|
||||
// When 'mousemodel' is "popup" or "popup_setpos", translate mouse events:
|
||||
@@ -594,7 +592,7 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
// shift-left button -> right button
|
||||
// alt-left button -> alt-right button
|
||||
if (mouse_model_popup()) {
|
||||
m_pos_flag = get_fpos_of_mouse(&m_pos);
|
||||
m_pos_flag = get_fpos_of_mouse(&m_pos, NULL);
|
||||
if (!(m_pos_flag & (IN_STATUS_LINE|MOUSE_WINBAR|MOUSE_STATUSCOL))
|
||||
&& which_button == MOUSE_RIGHT && !(mod_mask & (MOD_MASK_SHIFT|MOD_MASK_CTRL))) {
|
||||
if (!is_click) {
|
||||
@@ -885,15 +883,16 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
do_cmdline_cmd(".ll");
|
||||
}
|
||||
got_click = false; // ignore drag&release now
|
||||
} else if ((mod_mask & MOD_MASK_CTRL)
|
||||
|| (curbuf->b_help && (mod_mask & MOD_MASK_MULTI_CLICK) == MOD_MASK_2CLICK)) {
|
||||
// Ctrl-Mouse click (or double click in a help window) jumps to the tag
|
||||
// under the mouse pointer.
|
||||
} else if (curbuf->b_help && (mod_mask & MOD_MASK_MULTI_CLICK) == MOD_MASK_2CLICK) {
|
||||
// Double-click in a help window jumps to the clicked tag.
|
||||
if (State & MODE_INSERT) {
|
||||
stuffcharReadbuff(Ctrl_O);
|
||||
}
|
||||
stuffcharReadbuff(Ctrl_RSB);
|
||||
got_click = false; // ignore drag&release now
|
||||
if ((State & MODE_INSERT) == 0) {
|
||||
exec_stuffed(NULL);
|
||||
}
|
||||
} else if ((mod_mask & MOD_MASK_SHIFT)) {
|
||||
// Shift-Mouse click searches for the next occurrence of the word under
|
||||
// the mouse pointer
|
||||
@@ -905,6 +904,9 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
|
||||
} else { // MOUSE_RIGHT
|
||||
stuffcharReadbuff('#');
|
||||
}
|
||||
if ((State & MODE_INSERT) == 0) {
|
||||
exec_stuffed(NULL);
|
||||
}
|
||||
} else if (in_status_line || in_sep_line) {
|
||||
// Do nothing if on status line or vertical separator
|
||||
// Handle double clicks otherwise
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
#include "nvim/math.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memline_defs.h"
|
||||
#include "nvim/memory.h"
|
||||
@@ -266,7 +267,7 @@ static const struct nv_cmd {
|
||||
{ 'N', nv_next, NV_MOTION, SEARCH_REV },
|
||||
{ 'O', nv_open, 0, 0 },
|
||||
{ 'P', nv_put, 0, 0 },
|
||||
{ 'Q', nv_regreplay, 0, 0 },
|
||||
{ 'Q', nv_Q, 0, 0 },
|
||||
{ 'R', nv_Replace, 0, false },
|
||||
{ 'S', nv_subst, NV_KEEPREG, 0 },
|
||||
{ 'T', nv_csearch, NV_NCH_ALW|NV_LANG|NV_MOTION, BACKWARD },
|
||||
@@ -2041,13 +2042,20 @@ void display_showcmd(void)
|
||||
}
|
||||
// 'showcmdloc' is "last" or empty
|
||||
|
||||
// Multicursor count prefixes the pending command: "2× ciw".
|
||||
char mc_buf[16];
|
||||
size_t mc_len = mc_showcmd(mc_buf, sizeof(mc_buf));
|
||||
|
||||
if (ui_has(kUIMessages)) {
|
||||
MAXSIZE_TEMP_ARRAY(content, 1);
|
||||
MAXSIZE_TEMP_ARRAY(chunk, 3);
|
||||
if (!showcmd_is_clear) {
|
||||
char ext_buf[sizeof(mc_buf) + SHOWCMD_BUFLEN];
|
||||
STRCPY(ext_buf, mc_buf);
|
||||
xstrlcpy(ext_buf + mc_len, showcmd_is_clear ? "" : showcmd_buf, sizeof(ext_buf) - mc_len);
|
||||
if (*ext_buf != NUL) {
|
||||
// placeholder for future highlight support
|
||||
ADD_C(chunk, INTEGER_OBJ(0));
|
||||
ADD_C(chunk, CSTR_AS_OBJ(showcmd_buf));
|
||||
ADD_C(chunk, CSTR_AS_OBJ(ext_buf));
|
||||
ADD_C(chunk, INTEGER_OBJ(0));
|
||||
ADD_C(content, ARRAY_OBJ(chunk));
|
||||
}
|
||||
@@ -2062,7 +2070,7 @@ void display_showcmd(void)
|
||||
int showcmd_row = Rows - 1;
|
||||
grid_line_start(&msg_grid_adj, showcmd_row);
|
||||
|
||||
int len = 0;
|
||||
int len = mc_len > 0 ? grid_line_puts(sc_col, mc_buf, -1, 0) : 0;
|
||||
if (!showcmd_is_clear) {
|
||||
len += grid_line_puts(sc_col + len, showcmd_buf, -1, HL_ATTR(HLF_MSG));
|
||||
}
|
||||
@@ -3104,22 +3112,23 @@ static void nv_zet(cmdarg_T *cap)
|
||||
}
|
||||
}
|
||||
|
||||
/// "Q" command.
|
||||
static void nv_regreplay(cmdarg_T *cap)
|
||||
/// "Q" command: Toggles a multicursor at the cursor position.
|
||||
/// "[count]Q": Places a multicursor at every match of the last search pattern.
|
||||
/// "{visual}Q": Places a multicursor on each selected line.
|
||||
static void nv_Q(cmdarg_T *cap)
|
||||
{
|
||||
if (checkclearop(cap->oap)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reg_recorded != 0) {
|
||||
// The macro's commands are captured as one "@x"-labeled atom (see
|
||||
// atom_macro_start()).
|
||||
atom_macro_start(reg_recorded);
|
||||
}
|
||||
while (cap->count1-- && !got_int) {
|
||||
if (do_execreg(reg_recorded, false, false, false) == false) {
|
||||
clearopbeep(cap->oap);
|
||||
break;
|
||||
if (reg_recording != 0 || reg_executing != 0) {
|
||||
// Not allowed while recording/executing a macro. |mcursor-limitations|
|
||||
vim_beep(0);
|
||||
} else if (!checkclearop(cap->oap)) {
|
||||
if (Visual.active) {
|
||||
typval_T tv_args[] = { { .v_type = VAR_UNKNOWN } };
|
||||
nlua_call_typval("vim._core.mcursor", "visual", tv_args, NULL);
|
||||
} else if (cap->count0 > 0) {
|
||||
typval_T tv_args[] = { { .v_type = VAR_UNKNOWN } };
|
||||
nlua_call_typval("vim._core.mcursor", "matches", tv_args, NULL);
|
||||
} else {
|
||||
mc_toggle(curbuf, curwin->w_cursor, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4302,6 +4311,24 @@ static void nv_brackets(cmdarg_T *cap)
|
||||
cap->count1) == false) {
|
||||
clearopbeep(cap->oap);
|
||||
}
|
||||
} else if (cap->nchar == 'C') {
|
||||
// "[C" and "]C": jump to previous/next multicursor.
|
||||
if (cap->oap->op_type != OP_NOP) {
|
||||
// Not an operator motion: a cascaded "d]C" would consume its own targets.
|
||||
clearopbeep(cap->oap);
|
||||
} else {
|
||||
typval_T tv_args[] = {
|
||||
{ .v_type = VAR_BOOL, .vval.v_bool = cap->cmdchar == ']' ? kBoolVarTrue : kBoolVarFalse },
|
||||
{ .v_type = VAR_NUMBER, .vval.v_number = cap->count1 },
|
||||
{ .v_type = VAR_UNKNOWN },
|
||||
};
|
||||
typval_T rettv = TV_INITIAL_VALUE;
|
||||
nlua_call_typval("vim._core.mcursor", "jump", tv_args, &rettv);
|
||||
if (rettv.v_type != VAR_BOOL || rettv.vval.v_bool != kBoolVarTrue) {
|
||||
clearopbeep(cap->oap);
|
||||
}
|
||||
tv_clear(&rettv);
|
||||
}
|
||||
} else if (cap->nchar == 'r' || cap->nchar == 's' || cap->nchar == 'S') {
|
||||
// "[r", "[s", "[S", "]r", "]s" and "]S": move to next spell error.
|
||||
setpcmark();
|
||||
@@ -4557,14 +4584,11 @@ static void nv_replace(cmdarg_T *cap)
|
||||
// Other characters are done below to avoid problems with things like
|
||||
// CTRL-V 048 (for edit() this would be R CTRL-V 0 ESC).
|
||||
if (had_ctrl_v != Ctrl_V && cap->nchar == '\t' && (curbuf->b_p_et || p_sta)) {
|
||||
atom_stuff_start(cap);
|
||||
stuffnumReadbuff(cap->count1);
|
||||
stuffcharReadbuff('R');
|
||||
stuffcharReadbuff('\t');
|
||||
stuffcharReadbuff(ESC);
|
||||
if (exec_stuffed(cap->oap)) {
|
||||
cap->retval |= CA_COMMAND_BUSY;
|
||||
}
|
||||
exec_stuffed(cap);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4880,14 +4904,11 @@ static void nv_optrans(cmdarg_T *cap)
|
||||
static const char *str = "xXDCsSY&";
|
||||
|
||||
if (!checkclearopq(cap->oap)) {
|
||||
atom_stuff_start(cap);
|
||||
if (cap->count0) {
|
||||
stuffnumReadbuff(cap->count0);
|
||||
}
|
||||
stuffReadbuff(ar[strchr(str, (char)cap->cmdchar) - str]);
|
||||
if (exec_stuffed(cap->oap)) {
|
||||
cap->retval |= CA_COMMAND_BUSY;
|
||||
}
|
||||
exec_stuffed(cap);
|
||||
}
|
||||
cap->opcount = 0;
|
||||
}
|
||||
@@ -5416,6 +5437,8 @@ static void nv_g_cmd(cmdarg_T *cap)
|
||||
cap->cmdchar = cap->nchar;
|
||||
cap->nchar = NUL;
|
||||
nv_addsub(cap);
|
||||
} else if (cap->nchar == Ctrl_A && cap->oap->op_type == OP_NOP && mc_buf_has_cursors(curbuf)) {
|
||||
mc_counter(cap->count1);
|
||||
} else {
|
||||
clearopbeep(oap);
|
||||
}
|
||||
@@ -5445,6 +5468,17 @@ static void nv_g_cmd(cmdarg_T *cap)
|
||||
Visual.reselect = false;
|
||||
break;
|
||||
|
||||
// "gQ": restore the previous multicursors (analogous to "gv").
|
||||
case 'Q':
|
||||
if (reg_recording != 0 || reg_executing != 0) {
|
||||
// Not allowed while recording/executing a macro. |mcursor-limitations|
|
||||
vim_beep(0);
|
||||
} else {
|
||||
typval_T tv_args[] = { { .v_type = VAR_UNKNOWN } };
|
||||
nlua_call_typval("vim._core.mcursor", "restore", tv_args, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
// "gh": start Select mode.
|
||||
// "gH": start Select line mode.
|
||||
// "g^H": start Select block mode.
|
||||
@@ -5657,7 +5691,7 @@ static void nv_g_cmd(cmdarg_T *cap)
|
||||
case K_LEFTMOUSE:
|
||||
if (do_mouse(oap, cap->nchar, BACKWARD, cap->count1, 0)) {
|
||||
stuffcharReadbuff(Ctrl_RSB);
|
||||
exec_stuffed(oap);
|
||||
exec_stuffed(cap);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -5777,15 +5811,12 @@ static void nv_dot(cmdarg_T *cap)
|
||||
// If "restart_edit" is true, the last but one command is repeated
|
||||
// instead of the last command (inserting text). This is used for
|
||||
// CTRL-O <.> in insert mode.
|
||||
atom_stuff_start(cap);
|
||||
if (start_redo(cap->count0, restart_edit != 0 && Ins.moved == kInsNone) == false) {
|
||||
clearopbeep(cap->oap);
|
||||
return;
|
||||
}
|
||||
// Execute the redo keys here: the whole replay resolves within this "." command.
|
||||
if (exec_stuffed(cap->oap)) {
|
||||
cap->retval |= CA_COMMAND_BUSY;
|
||||
}
|
||||
exec_stuffed(cap);
|
||||
}
|
||||
|
||||
/// CTRL-R: undo undo or specify register in select mode
|
||||
@@ -6397,6 +6428,7 @@ static void nv_object(cmdarg_T *cap)
|
||||
/// "q" command: Start/stop macro recording.
|
||||
/// "q:", "q/", "q?": cmdwin.
|
||||
/// "[count]q:": interactive Ex-mode.
|
||||
/// "q=": Multicursor "follow".
|
||||
static void nv_q(cmdarg_T *cap)
|
||||
{
|
||||
if (cap->oap->op_type == OP_FORMAT) {
|
||||
@@ -6411,7 +6443,11 @@ static void nv_q(cmdarg_T *cap)
|
||||
return;
|
||||
}
|
||||
|
||||
if (cap->nchar == ':' || cap->nchar == '/' || cap->nchar == '?') {
|
||||
if (cap->nchar == '=') {
|
||||
if (!mc_follow_toggle(cap->count0)) {
|
||||
clearopbeep(cap->oap);
|
||||
}
|
||||
} else if (cap->nchar == ':' || cap->nchar == '/' || cap->nchar == '?') {
|
||||
if (cmdwin_buf != NULL) {
|
||||
emsg(_(e_cmdline_window_already_open));
|
||||
return;
|
||||
@@ -6729,30 +6765,32 @@ void normal_cmd(oparg_T *oap, bool toplevel)
|
||||
*oap = s.oa;
|
||||
}
|
||||
|
||||
/// Executes the pending readahead (see "Stuffing", input.c).
|
||||
/// Executes pending readahead now (see "Stuffing", input.c).
|
||||
///
|
||||
/// During "textlock" the stuffed keys are left for the main loop instead (for CmdAtom/multicursor
|
||||
/// purposes, that's fine: if stuff_empty()=false, the pending CmdAtom stays open and will collect
|
||||
/// the effects later).
|
||||
/// During "textlock" the stuffed keys are queued instead (main loop). For CmdAtom/multicursor
|
||||
/// purposes, that's fine: if stuff_empty()=false, pending CmdAtom stays open, will collect later.
|
||||
///
|
||||
/// @param oap The continuing operator state (see `normal_cmd`), or NULL.
|
||||
/// @return True if insert-session-resume is pending (i_CTRL-O): the caller reports
|
||||
/// CA_COMMAND_BUSY, so resume happens after next command instead.
|
||||
bool exec_stuffed(oparg_T *oap)
|
||||
/// @param cap The cmd whose translation was stuffed ("x" => "dl"), or NULL (internal stuff).
|
||||
void exec_stuffed(cmdarg_T *cap)
|
||||
{
|
||||
if (cap != NULL) {
|
||||
atom_stuff_start(cap); // Label as one atom.
|
||||
}
|
||||
if (text_locked() || curbuf_locked()) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
oparg_T oa;
|
||||
clear_oparg(&oa);
|
||||
if (oap == NULL) {
|
||||
oap = &oa;
|
||||
}
|
||||
oparg_T *oap = cap != NULL ? cap->oap : &oa;
|
||||
finish_op = false;
|
||||
while (!stuff_empty() && !got_int) {
|
||||
update_topline_cursor();
|
||||
// Continue the command's operator state.
|
||||
normal_cmd(oap, true);
|
||||
}
|
||||
finish_op = false;
|
||||
return restart_edit != 0;
|
||||
if (cap != NULL && restart_edit != 0) {
|
||||
// When insert-session-resume is pending (i_CTRL-O), resume happens after next command.
|
||||
cap->retval |= CA_COMMAND_BUSY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "nvim/math.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mbyte_defs.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memline_defs.h"
|
||||
#include "nvim/memory.h"
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mapping.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memfile.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
@@ -6836,6 +6837,11 @@ bool can_bs(int what)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Multicursor replays may join lines only if the primary's own span did.
|
||||
if (what == BS_EOL && !mc_ins_replay_can_join()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// support for number values was removed but we keep '2' since it is used in
|
||||
// legacy tests
|
||||
if (*p_bs == '2') {
|
||||
|
||||
@@ -686,10 +686,35 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
|
||||
uint8_t initial = (cmd >> 8) & 0xFF;
|
||||
uint8_t command = cmd & 0xFF;
|
||||
|
||||
// Currently unused
|
||||
(void)intermediate;
|
||||
|
||||
switch (command) {
|
||||
case 'q':
|
||||
if (initial == '>' && intermediate == ' ') {
|
||||
// Kitty multiple-cursors protocol query response:
|
||||
// CSI > shape;shape;… SP q
|
||||
MAXSIZE_TEMP_ARRAY(args, 2);
|
||||
ADD_C(args, STATIC_CSTR_AS_OBJ("termresponse"));
|
||||
|
||||
StringBuilder response = KV_INITIAL_VALUE;
|
||||
kv_concat(response, "\x1b[>");
|
||||
for (size_t i = 0; i < nparams; i++) {
|
||||
int arg;
|
||||
if (termkey_interpret_csi_param(params[i], &arg, NULL, NULL) != TERMKEY_RES_KEY) {
|
||||
kv_destroy(response);
|
||||
return;
|
||||
}
|
||||
kv_printf(response, "%d", arg);
|
||||
if (i < nparams - 1) {
|
||||
kv_push(response, ';');
|
||||
}
|
||||
}
|
||||
kv_concat(response, " q");
|
||||
ADD_C(args, STRING_OBJ(cbuf_as_string(response.items, response.size)));
|
||||
|
||||
// Forward to the client (TermResponse).
|
||||
rpc_send_event(ui_client_channel_id, "nvim_ui_term_event", args);
|
||||
kv_destroy(response);
|
||||
}
|
||||
break;
|
||||
case 'u':
|
||||
switch (initial) {
|
||||
case '?':
|
||||
|
||||
@@ -1687,10 +1687,16 @@ void tui_default_colors_set(TUIData *tui, Integer rgb_fg, Integer rgb_bg, Intege
|
||||
invalidate(tui, 0, tui->grid.height, 0, tui->grid.width);
|
||||
}
|
||||
|
||||
/// Writes directly to the TTY, bypassing the buffer.
|
||||
/// Writes to the TTY, or buffers (to avoid "tearing") if a frame is being assembled (or output is
|
||||
/// already buffered).
|
||||
void tui_ui_send(TUIData *tui, String content)
|
||||
FUNC_ATTR_NONNULL_ALL
|
||||
{
|
||||
if (kv_size(tui->invalid_regions) || tui->bufpos > 0) {
|
||||
// Append to buffer instead of writing directly.
|
||||
out(tui, content.data, content.size);
|
||||
return;
|
||||
}
|
||||
uv_write_t req;
|
||||
uv_buf_t buf = { .base = content.data, .len = UV_BUF_LEN(content.size) };
|
||||
int ret = uv_write(&req, (uv_stream_t *)&tui->output_handle, &buf, 1, NULL);
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
#include "nvim/mark.h"
|
||||
#include "nvim/mark_defs.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/mcursor.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memline_defs.h"
|
||||
#include "nvim/memory.h"
|
||||
@@ -481,6 +482,7 @@ int u_savecommon(buf_T *buf, linenr_T top, linenr_T bot, linenr_T newbot, bool r
|
||||
} else {
|
||||
uhp->uh_cursor_vcol = -1;
|
||||
}
|
||||
clearpos(&uhp->uh_cursor_after);
|
||||
|
||||
// save changed and buffer empty flag for undo
|
||||
uhp->uh_flags = (buf->b_changed ? UH_CHANGED : 0) +
|
||||
@@ -1902,7 +1904,7 @@ static void u_doit(int startcount, bool quiet, bool do_buf_event)
|
||||
if (!undo_allowed(curbuf)) {
|
||||
return;
|
||||
}
|
||||
atom_op_global_set(); // multicursor: undo/redo must not cascade (global, not per-cursor).
|
||||
atom_did_global_op(); // multicursor: undo/redo must not cascade (global, not per-cursor).
|
||||
|
||||
u_newcount = 0;
|
||||
u_oldcount = 0;
|
||||
@@ -1979,6 +1981,7 @@ void undo_time(int step, bool sec, bool file, bool absolute)
|
||||
text_locked_msg();
|
||||
return;
|
||||
}
|
||||
mc_undo_time(); // Time-travel crosses cascade boundaries, exit mc-session.
|
||||
|
||||
// First make sure the current undoable change is synced.
|
||||
if (!curbuf->b_u_synced) {
|
||||
@@ -2570,6 +2573,12 @@ static void u_undoredo(bool undo, bool do_buf_event)
|
||||
// Make sure the cursor is on an existing line and column.
|
||||
check_cursor(curwin);
|
||||
|
||||
if (!undo && curhead->uh_cursor_after.lnum > 0) {
|
||||
// Restore the post-change cursor pos, if available.
|
||||
curwin->w_cursor = curhead->uh_cursor_after;
|
||||
check_cursor(curwin);
|
||||
}
|
||||
|
||||
// Remember where we are for "g-" and ":earlier 10s".
|
||||
curbuf->b_u_seq_cur = curhead->uh_seq;
|
||||
if (undo) {
|
||||
|
||||
@@ -55,8 +55,9 @@ struct u_header {
|
||||
int uh_walk; ///< used by undo_time()
|
||||
u_entry_T *uh_entry; ///< pointer to first entry
|
||||
u_entry_T *uh_getbot_entry; ///< pointer to where ue_bot must be set
|
||||
pos_T uh_cursor; ///< cursor position before saving
|
||||
colnr_T uh_cursor_vcol;
|
||||
pos_T uh_cursor; ///< Pre-change cursor pos; restored on undo.
|
||||
pos_T uh_cursor_after; ///< Post-change pos; restored on redo. Not saved to undofile.
|
||||
colnr_T uh_cursor_vcol; ///< Virtual column of uh_cursor ('virtualedit'), or -1.
|
||||
int uh_flags; ///< see below
|
||||
fmark_T uh_namedm[NMARKS]; ///< marks before undo/after redo
|
||||
extmark_undo_vec_t uh_extmark; ///< info to move extmarks
|
||||
|
||||
@@ -886,10 +886,10 @@ M.vars = {
|
||||
termresponse = {
|
||||
type = 'string',
|
||||
desc = [=[
|
||||
The value of the most recent OSC or DCS control sequence
|
||||
received by Nvim from the terminal. This can be read in a
|
||||
|TermResponse| event handler after querying the terminal using
|
||||
another escape sequence.
|
||||
The most recent OSC, DCS, APC, or recognized CSI (DA1, kitty
|
||||
multiple-cursors) control sequence received by Nvim from the
|
||||
host-terminal. Can be read in a |TermResponse| event handler
|
||||
after querying the terminal.
|
||||
]=],
|
||||
},
|
||||
testing = {
|
||||
|
||||
@@ -133,9 +133,10 @@ describe('nvim_ui_send', function()
|
||||
|
||||
screen:expect_unchanged()
|
||||
|
||||
-- The TUI client queries OSC 11 on connect, so that precedes the payload.
|
||||
local bg_request = '\027]11;?\007'
|
||||
eq(bg_request .. 'Hello world', table.concat(read_data))
|
||||
-- On connect, these queries precede the payload.
|
||||
local mcursor_request = '\027[> q' -- kitty-multicursor (CSI > SP q) query.
|
||||
local bg_request = '\027]11;?\007' -- TUI client OSC 11 query.
|
||||
eq(mcursor_request .. bg_request .. 'Hello world', table.concat(read_data))
|
||||
end)
|
||||
|
||||
it('ignores ui_send event for UIs without stdout_tty', function()
|
||||
|
||||
@@ -2883,6 +2883,15 @@ describe('API', function()
|
||||
eq({ 'a', 'b', 'c' }, eval('[g:one, g:Two, g:THREE]'))
|
||||
api.nvim_load_context(ctx)
|
||||
eq({ 1, 2, 3 }, eval('[g:one, g:Two, g:THREE]'))
|
||||
|
||||
-- Context restores what it saved, irrespective of 'shada'.
|
||||
command('set shada=')
|
||||
command('autocmd OptionSet shada let g:optionset = 1')
|
||||
api.nvim_set_var('one', 'a')
|
||||
api.nvim_load_context(ctx)
|
||||
eq(1, eval('g:one'))
|
||||
eq('', eval('&shada'))
|
||||
eq(0, eval("get(g:, 'optionset', 0)"))
|
||||
end)
|
||||
|
||||
it('errors when context dict is invalid', function()
|
||||
|
||||
@@ -209,6 +209,7 @@ describe('vim._core', function()
|
||||
'vim._core.help',
|
||||
'vim._core.log',
|
||||
'vim._core.marks',
|
||||
'vim._core.mcursor',
|
||||
'vim._core.options',
|
||||
'vim._core.proc',
|
||||
'vim._core.server',
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('CmdAtom', function()
|
||||
|
||||
it('Lua-callback mapping (e.g. "]q" default)', function()
|
||||
-- Same shape as the "]q" default mapping: a Lua callback with no
|
||||
-- replayable keys. Still a user action: it publishes with an empty
|
||||
-- replayable keys. Still a user action: it emits with an empty
|
||||
-- replay payload.
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('n', ']q', function()
|
||||
@@ -199,7 +199,7 @@ describe('CmdAtom', function()
|
||||
end)
|
||||
|
||||
it('motions, search, Ex emit without an edit', function()
|
||||
-- Emission is not tied to editing: every user action publishes, so
|
||||
-- Emission is not tied to editing: every user action emits, so
|
||||
-- plugins can observe all activity.
|
||||
fn.setline(1, { 'alpha beta', 'gamma delta' })
|
||||
feed('gg0')
|
||||
@@ -409,6 +409,29 @@ describe('CmdAtom', function()
|
||||
feed('2G&3G2&')
|
||||
eq({ 'red a', 'red b', 'red c', 'red d' }, get_lines())
|
||||
eq({ type = 'excmd', lhs = '2&', keys = ':.,.+1s\n' }, pick(atom_last(), 'type', 'lhs', 'keys'))
|
||||
|
||||
-- Reg "." put re-inserts through edit(), and a mapping's composite labels it.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'one' })
|
||||
feed('ggifoo<Esc>')
|
||||
feed('$".p')
|
||||
eq('fooonefoo', fn.getline(1))
|
||||
eq(
|
||||
{ type = 'insert', lhs = k('1afoo<Esc>'), keys = k('1afoo<Esc>') },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys')
|
||||
)
|
||||
command('nnoremap ,p ".p')
|
||||
feed(',p')
|
||||
eq(
|
||||
{ type = 'insert', lhs = ',p', keys = k('1afoo<Esc>') },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys')
|
||||
)
|
||||
-- Typed ":put ." emits typed cmdline payload, not the internal ":put _" translation.
|
||||
feed(':put .<CR>')
|
||||
eq('foo', fn.getline(2))
|
||||
eq(
|
||||
{ type = 'excmd', lhs = ':put .\n', keys = ':put .\n', text = 'put .' },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys', 'text')
|
||||
)
|
||||
end)
|
||||
|
||||
it('mapping that enters :terminal mode', function()
|
||||
@@ -468,11 +491,6 @@ describe('CmdAtom', function()
|
||||
local evs = take()
|
||||
eq(1, #evs)
|
||||
eq('@q', evs[1].lhs)
|
||||
-- Same for "Q" (replay the last recorded register).
|
||||
feed('Q')
|
||||
evs = take()
|
||||
eq(1, #evs)
|
||||
eq({ lhs = '@q', keys = 'dl' }, pick(evs[1], 'lhs', 'keys'))
|
||||
|
||||
-- Programmatic / replayed input must NOT leak any atom.
|
||||
fresh()
|
||||
@@ -1444,6 +1462,12 @@ describe('CmdAtom', function()
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', '.', function()
|
||||
-- Multicursors: degrade to builtin ".", which repeats at each cursor.
|
||||
local mc = vim.api.nvim_create_namespace('nvim.multicursor')
|
||||
if #vim.api.nvim_buf_get_extmarks(0, mc, 0, -1, { limit = 1 }) > 0 then
|
||||
vim.api.nvim_feedkeys('.', 'n', false)
|
||||
return
|
||||
end
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
|
||||
@@ -1491,6 +1515,17 @@ describe('CmdAtom', function()
|
||||
retry(nil, 1000, function()
|
||||
eq('cd', fn.getline(1))
|
||||
end)
|
||||
|
||||
-- While multicursor is active, the "." mapping degrades to builtin "." so it cascades.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa', 'bbb', 'ccc' })
|
||||
feed('gg0QjQj')
|
||||
feed('x')
|
||||
n.poke_eventloop()
|
||||
eq({ 'aa', 'bb', 'cc' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
feed('.')
|
||||
retry(nil, 1000, function()
|
||||
eq({ 'a', 'b', 'c' }, api.nvim_buf_get_lines(0, 0, -1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('"," repeats the last motion atom', function()
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('macros with default mappings', function()
|
||||
eq('lxxx', eval('@i'))
|
||||
end)
|
||||
|
||||
it('can be replayed with Q', function()
|
||||
it('can be replayed with @@', function()
|
||||
insert [[
|
||||
hello
|
||||
hello
|
||||
@@ -87,39 +87,7 @@ helloFOO
|
||||
hello
|
||||
hello]]
|
||||
|
||||
feed [[Q]]
|
||||
expect [[
|
||||
helloFOOFOO
|
||||
hello
|
||||
hello]]
|
||||
|
||||
feed [[G3Q]]
|
||||
expect [[
|
||||
helloFOOFOO
|
||||
hello
|
||||
helloFOOFOOFOO]]
|
||||
|
||||
feed [[ggV3jQ]]
|
||||
expect [[
|
||||
helloFOOFOOFOO
|
||||
helloFOO
|
||||
helloFOOFOOFOOFOO]]
|
||||
end)
|
||||
|
||||
it('can be replayed with Q and @@', function()
|
||||
insert [[
|
||||
hello
|
||||
hello
|
||||
hello]]
|
||||
feed [[gg]]
|
||||
|
||||
feed [[qqAFOO<esc>q]]
|
||||
expect [[
|
||||
helloFOO
|
||||
hello
|
||||
hello]]
|
||||
|
||||
feed [[Q]]
|
||||
feed [[@q]]
|
||||
expect [[
|
||||
helloFOOFOO
|
||||
hello
|
||||
@@ -170,39 +138,6 @@ helloFOO123
|
||||
helloFOO]]
|
||||
end)
|
||||
|
||||
it('can be recorded and replayed in Visual mode', function()
|
||||
insert('foo BAR BAR foo BAR foo BAR BAR BAR foo BAR BAR')
|
||||
feed('0vqifofRq')
|
||||
eq({ 0, 1, 7, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('Q')
|
||||
eq({ 0, 1, 19, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('Q')
|
||||
eq({ 0, 1, 27, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('@i')
|
||||
eq({ 0, 1, 43, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
end)
|
||||
|
||||
it('can be recorded and replayed in Visual mode when ignorecase', function()
|
||||
command('set ignorecase')
|
||||
insert('foo BAR BAR foo BAR foo BAR BAR BAR foo BAR BAR')
|
||||
feed('0vqifofRq')
|
||||
eq({ 0, 1, 7, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('Q')
|
||||
eq({ 0, 1, 19, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('Q')
|
||||
eq({ 0, 1, 27, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
feed('@i')
|
||||
eq({ 0, 1, 43, 0 }, fn.getpos('.'))
|
||||
eq({ 0, 1, 1, 0 }, fn.getpos('v'))
|
||||
end)
|
||||
|
||||
it('can be replayed with @ in blockwise Visual mode', function()
|
||||
insert [[
|
||||
hello
|
||||
|
||||
2739
test/functional/editor/mcursor_spec.lua
Normal file
2739
test/functional/editor/mcursor_spec.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -452,7 +452,7 @@ describe('vim.ui_attach', function()
|
||||
end
|
||||
end)
|
||||
]])
|
||||
feed('Q')
|
||||
feed('@@')
|
||||
screen:expect({
|
||||
grid = [[
|
||||
|
|
||||
@@ -479,7 +479,7 @@ describe('vim.ui_attach', function()
|
||||
end
|
||||
end)
|
||||
]])
|
||||
feed('Q')
|
||||
feed('@@')
|
||||
screen:expect({
|
||||
grid = [[
|
||||
|
|
||||
|
||||
@@ -3216,6 +3216,22 @@ describe('TUI', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('TermResponse on kitty-multiple-cursors protocol query', function()
|
||||
child_exec_lua([[
|
||||
_G.termresponse = nil
|
||||
vim.api.nvim_create_autocmd('TermResponse', {
|
||||
once = true,
|
||||
callback = function(ev)
|
||||
_G.termresponse = ev.data.sequence
|
||||
end,
|
||||
})
|
||||
]])
|
||||
feed_data('\027[>1;2;3;29;30;40;100;101 q')
|
||||
retry(nil, nil, function()
|
||||
eq('\027[>1;2;3;29;30;40;100;101 q', child_exec_lua('return _G.termresponse'))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('TermResponse works with vim.wait() from another autocommand #32706', function()
|
||||
child_exec_lua([[
|
||||
_G.termresponse = nil
|
||||
|
||||
@@ -459,11 +459,11 @@ describe('messages2', function()
|
||||
{1:~ }|*12
|
||||
{19:W10: Warning: Changing a readonly file} |
|
||||
]])
|
||||
feed('<Esc>Qi')
|
||||
feed('<Esc>@@i')
|
||||
screen:expect([[
|
||||
^ |
|
||||
{1:~ }|*12
|
||||
{9:E354: Invalid register name: '^@'} |
|
||||
{9:E748: No previously used register} |
|
||||
]])
|
||||
end)
|
||||
|
||||
|
||||
@@ -1536,7 +1536,7 @@ stack traceback:
|
||||
{ content = { { 'baz' } }, kind = 'echo', append = true },
|
||||
},
|
||||
})
|
||||
feed('Q')
|
||||
feed('@@')
|
||||
screen:expect({
|
||||
grid = [[
|
||||
^ |
|
||||
@@ -1544,7 +1544,7 @@ stack traceback:
|
||||
]],
|
||||
messages = {
|
||||
{
|
||||
content = { { "E354: Invalid register name: '^@'", 9, 'ErrorMsg' } },
|
||||
content = { { 'E748: No previously used register', 9, 'ErrorMsg' } },
|
||||
history = true,
|
||||
kind = 'emsg',
|
||||
},
|
||||
@@ -1558,7 +1558,7 @@ stack traceback:
|
||||
]],
|
||||
msg_history = {
|
||||
prev_cmd = true,
|
||||
{ content = { { "E354: Invalid register name: '^@'", 9, 'ErrorMsg' } }, kind = 'emsg' },
|
||||
{ content = { { 'E748: No previously used register', 9, 'ErrorMsg' } }, kind = 'emsg' },
|
||||
},
|
||||
})
|
||||
end)
|
||||
@@ -1611,7 +1611,7 @@ stack traceback:
|
||||
end)
|
||||
|
||||
it('can capture execute("messages"))', function()
|
||||
feed('Q')
|
||||
feed('@@')
|
||||
screen:expect({
|
||||
grid = [[
|
||||
^ |
|
||||
@@ -1619,7 +1619,7 @@ stack traceback:
|
||||
]],
|
||||
messages = {
|
||||
{
|
||||
content = { { "E354: Invalid register name: '^@'", 9, 'ErrorMsg' } },
|
||||
content = { { 'E748: No previously used register', 9, 'ErrorMsg' } },
|
||||
history = true,
|
||||
kind = 'emsg',
|
||||
},
|
||||
@@ -1627,7 +1627,7 @@ stack traceback:
|
||||
})
|
||||
feed(':let msg = execute("messages")<CR>')
|
||||
screen:expect_unchanged()
|
||||
eq("E354: Invalid register name: '^@'", eval('msg'):gsub('\n', ''))
|
||||
eq('E748: No previously used register', eval('msg'):gsub('\n', ''))
|
||||
end)
|
||||
|
||||
it('single event for multi-expr :echo', function()
|
||||
@@ -3850,7 +3850,7 @@ describe('progress-message', function()
|
||||
eq('str-id', id7)
|
||||
|
||||
-- internal messages are also assigned an ID (and thus advance the next progress ID)
|
||||
feed('Q')
|
||||
feed('@@')
|
||||
local id8 = api.nvim_echo(
|
||||
{ { 'test-message 30' } },
|
||||
true,
|
||||
|
||||
@@ -54,6 +54,30 @@ describe('ui/mouse/input', function()
|
||||
})
|
||||
end)
|
||||
|
||||
it('middle click in Visual selection yanks, then puts', function()
|
||||
command('let g:loaded_clipboard_provider = 1') -- Avoid clipboard.
|
||||
feed('<LeftMouse><0,1>')
|
||||
eq({ 2, 0 }, api.nvim_win_get_cursor(0))
|
||||
feed('viw')
|
||||
eq('v', fn.mode())
|
||||
eq({ 2, 4 }, api.nvim_win_get_cursor(0))
|
||||
feed('"a') -- User typed register "a.
|
||||
feed('<MiddleMouse><3,0>')
|
||||
eq('mouse', fn.getreg('a'))
|
||||
eq('mouse', fn.getreg('"'))
|
||||
eq('tesmouseting', api.nvim_get_current_line())
|
||||
end)
|
||||
|
||||
it('shift-click search and CTRL-T pop execute within the click', function()
|
||||
fn.setline(1, { 'foo bar', 'x foo y' })
|
||||
feed('gg0')
|
||||
feed('<S-LeftMouse><0,0>') -- "*": search for the shift-clicked word.
|
||||
eq({ 2, 2 }, api.nvim_win_get_cursor(0))
|
||||
eq('', api.nvim_get_vvar('errmsg'))
|
||||
feed('<C-RightMouse><0,0>') -- "CTRL-T": empty tag stack raises E73.
|
||||
eq('E73: Tag stack empty', api.nvim_get_vvar('errmsg'))
|
||||
end)
|
||||
|
||||
it("in external ui works with unset 'mouse'", function()
|
||||
api.nvim_set_option_value('mouse', '', {})
|
||||
feed('<LeftMouse><2,1>')
|
||||
@@ -957,9 +981,19 @@ describe('ui/mouse/input', function()
|
||||
})
|
||||
end)
|
||||
|
||||
it('ctrl + left click will search for a tag', function()
|
||||
api.nvim_set_option_value('tags', './non-existent-tags-file', {})
|
||||
it('ctrl + left click places a multicursor', function()
|
||||
command('hi MCursor guifg=Black guibg=LightGrey')
|
||||
feed('<C-LeftMouse><0,0>')
|
||||
-- A cursor at the click; the primary cursor did not move.
|
||||
screen:expect({
|
||||
any = { '{17:t}esting', 'support and selectio^n' },
|
||||
})
|
||||
feed('q<BS>') -- remove all cursors
|
||||
end)
|
||||
|
||||
it('g + left click will search for a tag', function()
|
||||
api.nvim_set_option_value('tags', './non-existent-tags-file', {})
|
||||
feed('g<LeftMouse><0,0>')
|
||||
screen:expect({
|
||||
any = {
|
||||
'{9:E433: No tags file}',
|
||||
|
||||
@@ -2802,6 +2802,7 @@ func Test_normal33_g_cmd2()
|
||||
endfunc
|
||||
|
||||
func Test_normal_ex_substitute()
|
||||
throw 'Skipped: Nvim "gQ" restores multicursors (not Ex-mode)'
|
||||
" This was hanging on the substitute prompt.
|
||||
new
|
||||
call setline(1, 'a')
|
||||
|
||||
@@ -110,7 +110,7 @@ func Test_xterm_mouse_right_click_extends_visual()
|
||||
bwipe!
|
||||
endfunc
|
||||
|
||||
" Test that <C-LeftMouse> jumps to help tag and <C-RightMouse> jumps back.
|
||||
" Nvim: <C-LeftMouse> adds a multicursor. <C-RightMouse> still pops the tagstack.
|
||||
func Test_xterm_mouse_ctrl_click()
|
||||
let save_mouse = &mouse
|
||||
let save_term = &term
|
||||
@@ -129,14 +129,15 @@ func Test_xterm_mouse_ctrl_click()
|
||||
let col = 1
|
||||
call MouseCtrlLeftClick(row, col)
|
||||
call MouseLeftRelease(row, col)
|
||||
call assert_match('usr_02.txt$', bufname('%'), msg)
|
||||
call assert_equal('*usr_02.txt*', expand('<cWORD>'), msg)
|
||||
" No tag jump: a multicursor was added at the click position.
|
||||
"call assert_match('usr_02.txt$', bufname('%'), msg)
|
||||
"call assert_equal('*usr_02.txt*', expand('<cWORD>'), msg)
|
||||
|
||||
call MouseCtrlRightClick(row, col)
|
||||
call MouseRightRelease(row, col)
|
||||
" call assert_match('help.txt$', bufname('%'), msg)
|
||||
call assert_match('usr_toc.txt$', bufname('%'), msg)
|
||||
call assert_equal('|usr_02.txt|', expand('<cWORD>'), msg)
|
||||
"call assert_equal('|usr_02.txt|', expand('<cWORD>'), msg)
|
||||
|
||||
helpclose
|
||||
endfor
|
||||
|
||||
Reference in New Issue
Block a user