mirror of
https://github.com/neovim/neovim.git
synced 2026-08-17 18:04:46 -05:00
fix(cmdatom): <Cmd> mappings #41347
Problem:
`<cmd>` mappings do not emit `CmdAtom.text`.
`<cmd>` and Lua-callback mappings that edit the buffer apply only at the
primary cursor, not cascaded (multicursor).
Solution:
Capture the `<cmd>` command in getcmdkeycmd().
Add kKeyOpaque ("no capturable keys"); narrow kKeySynthetic ("not
a keystroke") to K_EVENT/K_IGNORE, so an opaque mapping's edit still
sets `map_edit` and cascades via LHS-replay.
This commit is contained in:
@@ -471,7 +471,7 @@ CmdAtom After a user action (an input "atom"): any
|
||||
|nvim_feedkeys()| (mode "n") to replay. Use
|
||||
|keytrans()| to key-notation. Empty for
|
||||
a mapping whose commands have no replayable
|
||||
keys (|<Cmd>|/Lua commands).
|
||||
keys (Lua callbacks).
|
||||
- lhs: LHS (user input). Raw bytes, like `keys`.
|
||||
- motionforce |forced-motion|: "v", "V", or
|
||||
"<C-V>" (|key-notation|).
|
||||
@@ -486,6 +486,7 @@ CmdAtom After a user action (an input "atom"): any
|
||||
Literal text, not key encoding. Examples:
|
||||
- typed "iab<Esc>" → text="ab"
|
||||
- typed ":cnext<CR>" → text="cnext"
|
||||
- typed "<Cmd>cnext<CR>" → text="cnext"
|
||||
- typed "iab<Left>c<Esc>" → text="c"
|
||||
- type: "command", "ex", "insert", "jump",
|
||||
"mapping", "motion", "mouse", "operator",
|
||||
|
||||
@@ -3430,6 +3430,9 @@ char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
|
||||
|
||||
if (aborted) {
|
||||
ga_clear(&line_ga);
|
||||
} else if (promptc == ':') {
|
||||
// Executing (do_cmdline()), not discarding (vim.on_key()): capture the payload.
|
||||
atom_cmdline_set(K_COMMAND, line_ga.ga_data, (size_t)line_ga.ga_len);
|
||||
}
|
||||
|
||||
return line_ga.ga_data;
|
||||
|
||||
@@ -190,7 +190,8 @@ static CmdAtom atom_from_spec(CmdAtomType type, CmdSpec spec)
|
||||
static CmdAtom atom_from_cmdline(CmdAtomType type, cmdarg_T *ca, const char *cmdline)
|
||||
{
|
||||
StringBuilder sb = KV_INITIAL_VALUE;
|
||||
if (type != kAEx && ca->count0 != 0) {
|
||||
if (ca->cmdchar != ':' && ca->count0 != 0) {
|
||||
// Not for ":", its count already prefilled (":.,.+1"). But "<Cmd>" needs count in the keys.
|
||||
kv_printf(sb, "%d", ca->count0);
|
||||
}
|
||||
sb_add_char(&sb, ca->cmdchar);
|
||||
@@ -525,9 +526,10 @@ unsigned atom_key_class(int cmd, int arg)
|
||||
switch (cmd) {
|
||||
case K_EVENT:
|
||||
case K_IGNORE:
|
||||
return kKeyOpaque | kKeySynthetic;
|
||||
case K_COMMAND:
|
||||
case K_LUA:
|
||||
return kKeySynthetic;
|
||||
return kKeyOpaque;
|
||||
case '/':
|
||||
case '?':
|
||||
case ':':
|
||||
@@ -591,12 +593,12 @@ unsigned atom_key_class(int cmd, int arg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures an accepted ":" cmdline payload.
|
||||
/// 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 != ':') {
|
||||
if (!atom_is_user_cmd() || (firstc != ':' && firstc != K_COMMAND)) {
|
||||
return;
|
||||
}
|
||||
xfree(curcmd.cmdline);
|
||||
@@ -977,11 +979,12 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
// so its operator can prep a redo: ":normal! vjd" is dot-repeatable.
|
||||
const bool user = atom_is_user_cmd();
|
||||
const unsigned keycls = atom_key_class(ca->cmdchar, ca->nchar);
|
||||
// Synthetic commands (timers, RPC, plugin callbacks) are not user-input: one that changes
|
||||
// nothing is invisible; one that changes the buffer/selection voids the pending visual atom.
|
||||
// Opaque cmd that changed nothing is invisible; one that changed the buffer/selection voids the
|
||||
// pending visual atom (see `kKeyOpaque`).
|
||||
//
|
||||
// XXX: This "state diff" is ad hoc: a synthetic change to unobserved state (e.g. only w_curswant)
|
||||
// counts as no-op. Extend this (or atom_key_class()) when such a case is reported...
|
||||
bool opaque = (keycls & kKeyOpaque) != 0;
|
||||
bool synthetic = (keycls & kKeySynthetic) != 0;
|
||||
bool unchanged = curbuf == old->buf
|
||||
&& equalpos(old->pos, curwin->w_cursor)
|
||||
@@ -990,7 +993,7 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
&& (!Visual.active
|
||||
|| (equalpos(old->visual.start, Visual.start)
|
||||
&& old->visual.mode == Visual.mode));
|
||||
if (synthetic && unchanged) {
|
||||
if (opaque && unchanged) {
|
||||
return;
|
||||
}
|
||||
bool ins_cascaded = user && curcmd.ins_cascaded;
|
||||
@@ -1074,8 +1077,8 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf);
|
||||
atom.changed = changed;
|
||||
atom_push(false, atom);
|
||||
} else if (curcmd.cmdline != NULL && ca->cmdchar == ':') {
|
||||
// Same for ":cnext<CR>".
|
||||
} else if (curcmd.cmdline != NULL && (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) {
|
||||
// Same for ":cnext<CR>" or "<Cmd>cnext<CR>".
|
||||
CmdAtom atom = atom_from_cmdline(kAEx, ca, curcmd.cmdline);
|
||||
atom.changed = changed;
|
||||
atom_push(false, atom);
|
||||
|
||||
@@ -66,17 +66,20 @@ struct CmdAtom {
|
||||
/// Key classes (atom_key_class()).
|
||||
/// Flags, bc same char can mean different things per mode (CTRL-T: tag-jump vs i_CTRL-T indent).
|
||||
enum {
|
||||
kKeySynthetic = 1 << 0, ///< Not a user keystroke (K_EVENT, K_IGNORE, K_COMMAND, K_LUA).
|
||||
kKeyPayload = 1 << 1, ///< Interactively-typed payload (/, ?, :, !).
|
||||
kKeyScrollMove = 1 << 2, ///< Scroll may move cursor (C-D/…): viewport-dependent, unreplayable.
|
||||
kKeyScrollView = 1 << 3, ///< Viewport-only scroll (C-Y,wheel): cursor stays, unless 'scrolloff'.
|
||||
kKeyJump = 1 << 4, ///< Moves to absolute pos from primary cursor's shared nav state
|
||||
kKeyOpaque = 1 << 0, ///< Uncapturable keys (<Cmd>, K_LUA, plus kKeySynthetic): its only
|
||||
///< trace is its effect.
|
||||
kKeySynthetic = 1 << 1, ///< Not a user keystroke (K_EVENT, K_IGNORE): unlike <Cmd>/K_LUA, never
|
||||
///< reaches us from a mapping.
|
||||
kKeyPayload = 1 << 2, ///< Interactively-typed payload (/, ?, :, !).
|
||||
kKeyScrollMove = 1 << 3, ///< Scroll may move cursor (C-D/…): viewport-dependent, unreplayable.
|
||||
kKeyScrollView = 1 << 4, ///< Viewport-only scroll (C-Y,wheel): cursor stays, unless 'scrolloff'.
|
||||
kKeyJump = 1 << 5, ///< Moves to absolute pos from primary cursor's shared nav state
|
||||
///< (jumplist C-O/I, CTRL-T, "g;"): not followable.
|
||||
kKeyMotion = 1 << 5, ///< Replayable special-key motion (arrows, <Home>, …).
|
||||
kKeyInsFlush = 1 << 6, ///< Insert-mode cmd a literal preview cannot represent:
|
||||
kKeyMotion = 1 << 6, ///< Replayable special-key motion (arrows, <Home>, …).
|
||||
kKeyInsFlush = 1 << 7, ///< Insert-mode cmd a literal preview cannot represent:
|
||||
///< - deletions/indent-shifts (<Del>, CTRL-W, …) may edit text
|
||||
///< outside the tracked region by per-cursor amounts;
|
||||
///< - cursor-moves (start_arrow()) move the insertion point itself.
|
||||
kKeyMouse = 1 << 7, ///< Mouse button press (<LeftMouse>, …). Drag/release/move are the
|
||||
kKeyMouse = 1 << 8, ///< Mouse button press (<LeftMouse>, …). Drag/release/move are the
|
||||
///< press's continuation: no class, invisible to capture.
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('CmdAtom', function()
|
||||
pick(atom_last(), 'type', 'lhs', 'keys', 'changed')
|
||||
)
|
||||
-- An empty-keys mapping that DOES edit still reports it: `changed` is
|
||||
-- the only informative payload of a <Cmd>/Lua-callback edit.
|
||||
-- the only informative payload of a Lua-callback edit.
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('n', ',e', function()
|
||||
vim.api.nvim_buf_set_lines(0, 0, 0, false, { 'NEW' })
|
||||
@@ -113,6 +113,32 @@ describe('CmdAtom', function()
|
||||
]])
|
||||
feed(',e')
|
||||
eq({ keys = '', changed = true }, pick(atom_last(), 'keys', 'changed'))
|
||||
|
||||
-- "<Cmd>" is opaque too, but unlike a Lua callback its command is text (like a ":" mapping).
|
||||
command('nnoremap ,c <Cmd>call setline(1, "N" . v:count)<CR>')
|
||||
feed('3,c')
|
||||
local cmdev = atom_last()
|
||||
eq({
|
||||
type = 'ex',
|
||||
lhs = ',c',
|
||||
keys = k('3<Cmd>call setline(1, "N" . v:count)<NL>'),
|
||||
text = 'call setline(1, "N" . v:count)',
|
||||
count = 3,
|
||||
changed = true,
|
||||
}, pick(cmdev, 'type', 'lhs', 'keys', 'text', 'count', 'changed'))
|
||||
-- Those keys replay: the count must survive, since "<Cmd>" reads v:count.
|
||||
eq('N3', fn.getline(1))
|
||||
fn.setline(1, 'reset')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(cmdev.keys))
|
||||
eq('N3', fn.getline(1))
|
||||
|
||||
-- Opaque key that changes nothing is invisible: mid-selection it must not void the pending
|
||||
-- visual atom, which would void its per-cursor extents.
|
||||
command('vnoremap ,n <Cmd>call execute("")<CR>')
|
||||
fn.setline(1, { 'aaa bbb' })
|
||||
feed('gg0viw,nd')
|
||||
eq(' bbb', fn.getline(1))
|
||||
eq({ type = 'visual', keys = 'viwd' }, pick(atom_last(), 'type', 'keys'))
|
||||
end)
|
||||
|
||||
it('motions, search, Ex emit without an edit', function()
|
||||
|
||||
Reference in New Issue
Block a user