mirror of
https://github.com/neovim/neovim.git
synced 2026-08-26 00:51:53 +00:00
feat(cmdatom)!: eliminate the need for vim-repeat #41414
Problem:
- `lhs` is not fully realized. E.g. for a "payload" mapping
`lhs` omits the `getchar()` payload during a mapping (vim-surround
`ds'` reports `lhs="ds"`). This means plugins like vim-repeat are
still needed...
- The "delta" fields of a CmdAtom are calculated too late.
- `<abuf>` and `changed` check whatever (wrong) buffer a command
(":bnext") might land in.
- CTRL-W_w between two windows on the same buffer reports type="motion".
Solution:
- `CmdOrigin` samples (buf/win/cursor/changedtick) at each "scope" entry
(CmdFrame, composite, Visual session, insert session).
- `dd<C-w>l` reports `changed=true` for the buffer it edited, regardless
of where the cursor ends up.
- New fields:
- `pos`: cursor position at command start.
- `moved`: indicates whether the cursor moved (in same buffer).
- `undoseq`: undo state at settlement.
- lhs now includes the payload: "ds)" reports lhs="ds)" instead of "ds".
- Easy for users to "replay" any atom.
- Rename: type "command" => "normal", "ex" => "excmd"; `arg` => `cmdarg`
- Drop `cascade` field (no reason to expose it)
This commit is contained in:
@@ -421,83 +421,93 @@ ChanOpen Just after a channel was opened.
|
||||
|
||||
*CmdAtom*
|
||||
CmdAtom After a user action (an "atom" of input): any
|
||||
motion, operator, insert session, Visual-mode
|
||||
keysequence, Ex cmdline (":cnext<CR>"),
|
||||
mapping (and its subatoms), or scroll/mouse.
|
||||
normal command, motion, operator, insert
|
||||
session, Visual sequence, Ex cmdline
|
||||
(":cnext<CR>"), mapping (and subatoms), macro,
|
||||
or scroll/mouse.
|
||||
|
||||
Event is |deferred|.
|
||||
<amatch> is the atom "type" (see below).
|
||||
Event is |deferred|.
|
||||
|
||||
Only for user input, not programmatic input:
|
||||
INPUT ATOM ~
|
||||
typed keys yes
|
||||
Typed keys yes
|
||||
Mapping, macro "@q" yes
|
||||
|nvim_input()| yes
|
||||
|nvim_feedkeys()| with "t" (see note)
|
||||
dot-repeat |.| yes
|
||||
mapping, typed "@q" yes
|
||||
Hydrogen yes
|
||||
|:normal| no
|
||||
API requests no, lol
|
||||
API requests no
|
||||
"@q" fed by a script no
|
||||
|multicursor| cascade no
|
||||
aborted operation no
|
||||
terminal-mode keys no
|
||||
mouse drag/release no
|
||||
Hydrogen yes
|
||||
Aborted operation no
|
||||
Terminal-mode keys no
|
||||
Mouse drag/release no (TODO?)
|
||||
|
||||
Note: all keys fed during a mapping execution
|
||||
(e.g. Lua callback) are part of the mapping's
|
||||
expansion.
|
||||
are part of the mapping's resolved `keys`.
|
||||
|
||||
The |event-data| has these fields (type: `vim.event.cmdatom.data`):
|
||||
|
||||
- arg: Typed operand of `cmd`: the "x" of "fx",
|
||||
the replacement char of |r|.
|
||||
|key-notation|, like `cmd`.
|
||||
- atoms: The constituent atoms of a composite
|
||||
(multi-command mapping, Visual sequence),
|
||||
in-order. A consumer of e.g. "motion" atoms
|
||||
may want to inspect these "children".
|
||||
- cascade: Queued for "multicursor" replay.
|
||||
- changed: Buffer was changed.
|
||||
- cmd: Command/motion/object name: "w", "fx" is
|
||||
"f", "iw", "gJ", "<Home>", …. |key-notation|
|
||||
(not raw bytes).
|
||||
- count: Effective |count|; omitted if none.
|
||||
- keys: Resolved (post-mapping) keysequence,
|
||||
raw internal bytes (not |key-notation|):
|
||||
feed directly to |feedkeys()| or
|
||||
|nvim_feedkeys()| (mode "n") to replay. Use
|
||||
|keytrans()| to key-notation. Empty if
|
||||
not replayable: a Lua mapping, or
|
||||
unreplayable Visual operation (mouse, |gv|,
|
||||
Select mode).
|
||||
- atoms: Ordered "subatoms" of a composite
|
||||
atom (multi-command mapping, Visual sequence).
|
||||
- changed: Buffer was changed by the atom.
|
||||
- cmd: Command/motion/object name: "w", "f",
|
||||
"iw", "gJ", "<Home>", …. |key-notation|
|
||||
- cmdarg: Operand of `cmd`: the "x" of "fx",
|
||||
the replacement char of |r|. |key-notation|
|
||||
- count: Effective |count|, or nil.
|
||||
- keys: Resolved (post-mapping) keysequence.
|
||||
Raw internal bytes (use |keytrans()| to get
|
||||
key-notation): feed to |nvim_feedkeys()|
|
||||
(mode "n") to replay. Empty: unreplayable.
|
||||
- lhs: User input, before it is
|
||||
resolved/translated. Mapping LHS, macro
|
||||
register ("@q"), or the keys of a Visual op.
|
||||
Raw bytes, like `keys`.
|
||||
resolved/translated. Mapping LHS plus any
|
||||
payload it read (|getchar()|), macro register
|
||||
("@q"), or Visual sequence. Raw bytes, like
|
||||
`keys`.
|
||||
- motionforce |forced-motion|: "v", "V", or
|
||||
"<C-V>" (|key-notation|).
|
||||
- moved: Cursor moved. To detect a "motion":
|
||||
`moved and not changed and not operator`
|
||||
- operator: Operator: "d", "g@", "zf", ….
|
||||
|key-notation|, like `cmd`.
|
||||
- pending: What is awaiting input after
|
||||
a mapping: "operator", "visual".
|
||||
- pending: The next atom completes what
|
||||
this mapping started: "mapping",
|
||||
"operator", "visual".
|
||||
- pos: Start position: [row, col], 1-indexed
|
||||
row, 0-indexed col.
|
||||
- reg: Register name.
|
||||
- remap: `keys` cannot replay this atom: the
|
||||
mapping reads its own arguments, or resolved
|
||||
to no keys. Re-run `lhs` instead. Empty
|
||||
`keys` WITHOUT `remap` is unreplayable (a
|
||||
Visual op with a tainted selection).
|
||||
- text: Payload text: inserted text of an
|
||||
insert session (after its last cursor-move,
|
||||
like |quote.|), Ex cmdline, or search.
|
||||
Literal text, not key encoding. Examples:
|
||||
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",
|
||||
"scroll", "visual".
|
||||
- "command": neither edits nor moves the
|
||||
cursor (undo, folds, CTRL-W commands, …).
|
||||
- "jump": moves the cursor via
|
||||
- type:
|
||||
- "excmd"
|
||||
- "insert"
|
||||
- "jump": Moves the cursor via
|
||||
absolute/shared navigation state
|
||||
(|jumplist|, marks, |star|).
|
||||
- "mapping"
|
||||
- "motion"
|
||||
- "mouse"
|
||||
- "normal": Non-motion/jump command ("u",
|
||||
CTRL-R, "za", CTRL-W, …).
|
||||
- "operator"
|
||||
- "scroll"
|
||||
- "visual"
|
||||
- undoseq: Undo state after the action
|
||||
(|undotree()| `seq_cur`). Decreases on undo.
|
||||
|
||||
*CmdlineChanged*
|
||||
CmdlineChanged After EVERY change inside command line. Also
|
||||
|
||||
@@ -11,7 +11,7 @@ Chapter 26 of the user manual introduces repeating |usr_26.txt|.
|
||||
Type |gO| to see the table of contents.
|
||||
|
||||
==============================================================================
|
||||
Single repeats *single-repeat*
|
||||
Single repeat *single-repeat*
|
||||
|
||||
*.*
|
||||
. Repeat last change, with count replaced with [count].
|
||||
@@ -37,30 +37,100 @@ jumps (mouse, |i_<PageUp>|, …) split an insert session.
|
||||
Repeating a Visual-mode command re-executes the captured keysequence,
|
||||
selection included. See |visual-repeat|.
|
||||
|
||||
==============================================================================
|
||||
Semantic repeat *cmdatom* *action-repeat*
|
||||
|
||||
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.
|
||||
|
||||
*excalibur*
|
||||
`CmdAtom.lhs` is the high-level user input collected during an action,
|
||||
including getchar() input. This is signficant: it reflects the semantic
|
||||
intent. `CmdAtom.keys` reveals the low-level internal commands that were
|
||||
produced.
|
||||
|
||||
INPUT (`lhs`) RESOLUTION (`keys`) ~
|
||||
dw "dw" Not translated.
|
||||
zfa{ "zfa{" Not an edit (`changed=false`).
|
||||
x "dl" Translated builtin
|
||||
,d "dl" Mapping `:nnoremap ,d x`
|
||||
@q "dl" Macro `@q = "x"`
|
||||
<F6> "dlw" Composite `:nnoremap <F6> xw`:
|
||||
`type='mapping'`, subatoms "dl" and "w"
|
||||
concat to the composite keys.
|
||||
ds) ":call …<CR>" "Payload" mapping (reads |getchar()|
|
||||
e.g. vim-surround "ds'"), `remap=true`
|
||||
]q "" Lua mapping, `remap=true`
|
||||
,D "" Incomplete mapping (ended mid-
|
||||
operation) `:nnoremap ,D d`,
|
||||
`pending='operator'`, the NEXT atom
|
||||
("dw") completes it
|
||||
V<C-E>d "" Unreplayable: viewport-dependent
|
||||
Visual sequence; `lhs` is only
|
||||
a hint/label.
|
||||
|
||||
To repeat an atom, feed `keys` (mode "n"), or `lhs` (mode "m"). Empty `keys`
|
||||
without `remap` means the atom is unreplayable. >lua
|
||||
|
||||
local function replay(a)
|
||||
if not a.remap and a.keys == '' then
|
||||
return -- Unreplayable Visual op.
|
||||
end
|
||||
vim.api.nvim_feedkeys(a.remap and a.lhs or a.keys, a.remap and 'm' or 'n', false)
|
||||
end
|
||||
<
|
||||
*motion-repeat*
|
||||
Example: a "," mapping that repeats the last motion, like |;| but for any
|
||||
motion ("3w", "fx", "/pat<CR>"): remember the last "motion" |CmdAtom| and
|
||||
replay its keys: >lua
|
||||
local last ---@type string?
|
||||
Example: This "," mapping repeats the last "motion" ("zj", "3w", "fx",
|
||||
"/pat<CR>", "]c") by checking `(moved and not changed)`. Drop the
|
||||
`not ev.data.moved` condition to repeat any non-edit ("zz", "zfa{", CTRL-W_w): >lua
|
||||
|
||||
local last ---@type vim.event.cmdatom.data?
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
pattern = 'motion',
|
||||
callback = function(ev)
|
||||
last = ev.data.keys
|
||||
if not ev.data.moved or ev.data.changed or ev.data.lhs == ',' then
|
||||
return -- Skip non-motions, edits, and this mapping itself.
|
||||
end
|
||||
last = ev.data
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', ',', function()
|
||||
-- CmdAtom is deferred: schedule the replay after any pending event,
|
||||
-- so `last` is fresh when "," follows a motion.
|
||||
-- CmdAtom is deferred: schedule the replay, in case "," follows a motion.
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
vim.api.nvim_feedkeys(last, 'n', false) -- "n": already resolved
|
||||
replay(last)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
<
|
||||
*restore-undo-cursor*
|
||||
Example: Restore cursor position after undo. Works for |u|, "3u", |CTRL-R|,
|
||||
":undo N", |g-| and any mapping: >lua
|
||||
|
||||
local seen = {} ---@type table<integer, table>
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
callback = function(ev)
|
||||
local seq = ev.data.undoseq
|
||||
if not seq then
|
||||
return
|
||||
end
|
||||
local s = seen[ev.buf] or {}
|
||||
seen[ev.buf] = s
|
||||
-- Note: g- :earlier may cross undo-tree branches, "best effort" in that case.
|
||||
if s.prev and seq < s.prev and s[seq + 1] then
|
||||
vim.api.nvim_win_set_cursor(0, s[seq + 1]) -- Undo: first abandoned state.
|
||||
elseif s.prev and seq > s.prev and s[seq] then
|
||||
vim.api.nvim_win_set_cursor(0, s[seq]) -- Redo: revisiting a known seq.
|
||||
elseif ev.data.changed and not s[seq] then
|
||||
s[seq] = ev.data.pos -- New edit.
|
||||
end
|
||||
s.prev = seq
|
||||
end,
|
||||
})
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
Multiple repeats *multi-repeat*
|
||||
Multiple repeat *multi-repeat*
|
||||
|
||||
*:g* *:global* *E148*
|
||||
:[range]g[lobal]/{pattern}/[cmd]
|
||||
@@ -128,7 +198,7 @@ This replaces all occurrences of "pat" with "PAT". The same can be done with: >
|
||||
Which is two characters shorter!
|
||||
|
||||
==============================================================================
|
||||
Complex repeats *complex-repeat*
|
||||
Complex repeat *complex-repeat*
|
||||
|
||||
*q* *recording* *macro*
|
||||
q{0-9a-zA-Z"} Record typed characters into register {0-9a-zA-Z"}
|
||||
|
||||
@@ -6,20 +6,23 @@ error('Cannot require a meta file')
|
||||
|
||||
--- Data for the CmdAtom event.
|
||||
--- @class vim.event.cmdatom.data
|
||||
--- @field arg? string Typed operand of `cmd` ("fx" => "x").
|
||||
--- @field atoms? vim.event.cmdatom.data[] Subatoms of a composite (mapping, Visual sequence).
|
||||
--- @field cascade boolean Queued for multicursor replay.
|
||||
--- @field changed boolean Changed the buffer.
|
||||
--- @field cmd? string Command/motion/object name ("w", "f", "iw", "gJ").
|
||||
--- @field cmdarg? string Operand of `cmd` ("fx" => "x").
|
||||
--- @field count? integer Effective count.
|
||||
--- @field keys string Resolved keysequence, raw bytes: feed to nvim_feedkeys() to replay.
|
||||
--- @field lhs? string Mapping LHS or macro register ("gj", "@q"). Raw bytes, like `keys`.
|
||||
--- @field lhs? string High-level user input: mapping LHS + any payload it read, or macro register ("gj", "ds'", "@q"). Raw bytes.
|
||||
--- @field motionforce? 'v'|'V'|'<C-V>' forced-motion type.
|
||||
--- @field moved? boolean Moved the cursor.
|
||||
--- @field operator? string Operator name ("d", "g~", "g@"). key-notation.
|
||||
--- @field pending? 'operator'|'visual' Mapping ended mid-operation.
|
||||
--- @field pending? 'mapping'|'operator'|'visual' The next atom completes what this started.
|
||||
--- @field pos? [integer,integer] Cursor before the action: 1-indexed row, 0-indexed column.
|
||||
--- @field reg? string Register name.
|
||||
--- @field remap? true `keys` cannot replay this (the mapping reads its own args): feed `lhs` with remapping.
|
||||
--- @field text? string Inserted text, or the Ex/search cmdline.
|
||||
--- @field type 'command'|'ex'|'insert'|'jump'|'mapping'|'motion'|'mouse'|'operator'|'scroll'|'visual'
|
||||
--- @field type 'excmd'|'insert'|'jump'|'mapping'|'motion'|'mouse'|'normal'|'operator'|'scroll'|'visual'
|
||||
--- @field undoseq? integer Undo state after the action (`undotree().seq_cur`). Decreases on undo.
|
||||
|
||||
--- @class vim.event.lspattach.data
|
||||
--- @field client_id integer
|
||||
|
||||
@@ -260,8 +260,8 @@ static void redo_chars(const CmdSpec *spec, StringBuilder *buf, bool arg_meta)
|
||||
if (spec->cmd2 != NUL) {
|
||||
sb_add_char(buf, spec->cmd2);
|
||||
}
|
||||
if (spec->arg != NUL && !arg_meta) {
|
||||
sb_add_char(buf, spec->arg);
|
||||
if (spec->cmdarg != NUL && !arg_meta) {
|
||||
sb_add_char(buf, spec->cmdarg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "nvim/state_defs.h"
|
||||
#include "nvim/strings.h"
|
||||
#include "nvim/vim_defs.h"
|
||||
#include "nvim/window.h"
|
||||
|
||||
#include "input_cmdatom.c.generated.h"
|
||||
|
||||
@@ -69,7 +70,7 @@ static struct {
|
||||
char *lhs; ///< Label: mapping LHS or macro "@x" (NULL: not collecting).
|
||||
bool queued; ///< A cascadable atom was queued (g_atoms) while collecting.
|
||||
bool macro; ///< Macro execution: captured as an "@x"-labeled atom.
|
||||
varnumber_T tick; ///< b:changedtick at start.
|
||||
CmdOrigin origin; ///< State at start.
|
||||
} composite;
|
||||
|
||||
/// The executing command's frame; its `parent` chain spans nested `normal_execute()`.
|
||||
@@ -90,6 +91,7 @@ typedef enum {
|
||||
static struct {
|
||||
CmdAtomVec atoms; ///< Accumulated subatoms. A void session collects them as the `lhs` label.
|
||||
VatomState state;
|
||||
CmdOrigin origin; ///< State at session start (before the "v").
|
||||
} vatom;
|
||||
|
||||
/// Per-command capture scratch.
|
||||
@@ -111,14 +113,14 @@ static struct {
|
||||
} typed;
|
||||
|
||||
static const char *const type_names[] = {
|
||||
[kACommand] = "command",
|
||||
[kAEx] = "ex",
|
||||
[kAInsert] = "insert",
|
||||
[kAExcmd] = "excmd",
|
||||
[kAInsertSpan] = "insert", // spans display as "insert" (as a composite's `atoms`)
|
||||
[kAInsert] = "insert",
|
||||
[kAJump] = "jump",
|
||||
[kAMapping] = "mapping",
|
||||
[kAMotion] = "motion",
|
||||
[kAMouse] = "mouse",
|
||||
[kANormal] = "normal",
|
||||
[kAOperator] = "operator",
|
||||
[kAScroll] = "scroll",
|
||||
[kAVisual] = "visual",
|
||||
@@ -170,10 +172,39 @@ CmdSpec atom_cmd_spec(const cmdarg_T *cap)
|
||||
.count = cap->count0,
|
||||
.cmd = cap->cmdchar,
|
||||
.cmd2 = operand ? NUL : cap->nchar,
|
||||
.arg = operand ? cap->nchar : NUL,
|
||||
.cmdarg = operand ? cap->nchar : NUL,
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the current buffer/window/cursor state.
|
||||
static CmdOrigin atom_origin(void)
|
||||
{
|
||||
CmdOrigin origin = { .win = curwin, .pos = curwin->w_cursor,
|
||||
.tick = buf_get_changedtick(curbuf) };
|
||||
set_bufref(&origin.buf, curbuf);
|
||||
return origin;
|
||||
}
|
||||
|
||||
/// True if the buffer was edited. False if the buf disappeared.
|
||||
static bool atom_origin_changed(CmdOrigin origin)
|
||||
{
|
||||
return bufref_valid(&origin.buf) && buf_get_changedtick(origin.buf.br_buf) != origin.tick;
|
||||
}
|
||||
|
||||
/// True if the cursor moved (in original buf). False if the win or buf disappeared.
|
||||
static bool atom_origin_moved(CmdOrigin origin)
|
||||
{
|
||||
return bufref_valid(&origin.buf) && win_valid(origin.win)
|
||||
&& origin.win->w_buffer == origin.buf.br_buf
|
||||
&& !equalpos(origin.pos, origin.win->w_cursor);
|
||||
}
|
||||
|
||||
/// Undo state of the buffer now, or 0 if the buffer disappeared.
|
||||
static int atom_origin_undoseq(CmdOrigin origin)
|
||||
{
|
||||
return bufref_valid(&origin.buf) ? origin.buf.br_buf->b_u_seq_cur : 0;
|
||||
}
|
||||
|
||||
/// Composes a CmdSpec into `redo_keys` format.
|
||||
/// @return Allocated key sequence.
|
||||
static char *atom_redo_keys(CmdSpec spec)
|
||||
@@ -197,7 +228,7 @@ static CmdAtom atom_from_spec(CmdAtomType type, CmdSpec spec)
|
||||
}
|
||||
|
||||
/// Gets a typed cmdline as a CmdAtom.
|
||||
/// ":cnext<CR>" => CmdAtom{ kAEx, keys=":cnext<NL>", text="cnext" }
|
||||
/// ":cnext<CR>" => CmdAtom{ kAExcmd, keys=":cnext<NL>", text="cnext" }
|
||||
static CmdAtom atom_from_cmdline(CmdAtomType type, cmdarg_T *ca, const char *cmdline)
|
||||
{
|
||||
StringBuilder sb = KV_INITIAL_VALUE;
|
||||
@@ -275,11 +306,11 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
}
|
||||
// Inapplicable fields are OMITTED, not defaulted.
|
||||
Dict d = ARRAY_DICT_INIT;
|
||||
char *arg = atom_key_name(spec->arg);
|
||||
if (arg != NULL && *arg != NUL) {
|
||||
PUT(d, "arg", CSTR_AS_OBJ(arg));
|
||||
char *cmdarg = atom_key_name(spec->cmdarg);
|
||||
if (cmdarg != NULL && *cmdarg != NUL) {
|
||||
PUT(d, "cmdarg", CSTR_AS_OBJ(cmdarg));
|
||||
} else {
|
||||
xfree(arg);
|
||||
xfree(cmdarg);
|
||||
}
|
||||
PUT(d, "changed", BOOLEAN_OBJ(atom->changed));
|
||||
if (*cmd != NUL) {
|
||||
@@ -290,6 +321,15 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
if (spec->count > 0) {
|
||||
PUT(d, "count", INTEGER_OBJ(spec->count));
|
||||
}
|
||||
// Note: origin is undefined for an insert-session span, its cursor effect is undefined.
|
||||
if (atom->origin.pos.lnum > 0) {
|
||||
PUT(d, "moved", BOOLEAN_OBJ(atom->moved));
|
||||
Array pos = ARRAY_DICT_INIT;
|
||||
ADD(pos, INTEGER_OBJ(atom->origin.pos.lnum));
|
||||
ADD(pos, INTEGER_OBJ(atom->origin.pos.col));
|
||||
PUT(d, "pos", ARRAY_OBJ(pos));
|
||||
PUT(d, "undoseq", INTEGER_OBJ(atom->undoseq));
|
||||
}
|
||||
// keys/lhs are RAW bytes (typeahead encoding).
|
||||
const char *keys = atom->keys != NULL ? atom->keys : "";
|
||||
PUT(d, "keys", CSTR_TO_OBJ(keys));
|
||||
@@ -305,6 +345,9 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
if (spec->regname != 0) {
|
||||
PUT(d, "reg", CSTR_TO_OBJ(regname));
|
||||
}
|
||||
if (atom->remap) {
|
||||
PUT(d, "remap", BOOLEAN_OBJ(true));
|
||||
}
|
||||
if (atom->text != NULL && *atom->text != NUL) {
|
||||
PUT(d, "text", CSTR_TO_OBJ(atom->text));
|
||||
}
|
||||
@@ -313,13 +356,12 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
}
|
||||
|
||||
/// Schedules a CmdAtom event.
|
||||
static void atom_emit(const CmdAtom *atom, const char *pending, bool cascade)
|
||||
static void atom_emit(const CmdAtom *atom, const char *pending)
|
||||
{
|
||||
if (!has_event(EVENT_CMDATOM)) {
|
||||
return;
|
||||
}
|
||||
Dict data = atom_dict(atom);
|
||||
PUT(data, "cascade", BOOLEAN_OBJ(cascade));
|
||||
if (kv_size(atom->atoms) > 0) {
|
||||
Array atoms = ARRAY_DICT_INIT;
|
||||
for (size_t i = 0; i < kv_size(atom->atoms); i++) {
|
||||
@@ -330,7 +372,8 @@ static void atom_emit(const CmdAtom *atom, const char *pending, bool cascade)
|
||||
if (*pending != NUL) {
|
||||
PUT(data, "pending", CSTR_TO_OBJ(pending));
|
||||
}
|
||||
aucmd_defer(EVENT_CMDATOM, (char *)type_names[atom->type], NULL, AUGROUP_ALL, curbuf, NULL,
|
||||
buf_T *buf = atom->origin.buf.br_buf != NULL ? atom->origin.buf.br_buf : curbuf;
|
||||
aucmd_defer(EVENT_CMDATOM, (char *)type_names[atom->type], NULL, AUGROUP_ALL, buf, NULL,
|
||||
&DICT_OBJ(data));
|
||||
api_free_dict(data);
|
||||
}
|
||||
@@ -342,6 +385,12 @@ static void atom_emit(const CmdAtom *atom, const char *pending, bool cascade)
|
||||
void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
{
|
||||
assert(atom.keys != NULL);
|
||||
if (atom.origin.buf.br_buf != NULL) {
|
||||
// Calculated here, from the atom's own baseline.
|
||||
atom.changed = atom_origin_changed(atom.origin);
|
||||
atom.moved = atom_origin_moved(atom.origin);
|
||||
atom.undoseq = atom_origin_undoseq(atom.origin);
|
||||
}
|
||||
if (atom_visual_pending()) {
|
||||
// Collecting the Visual composite: subatom of the pending visual atom.
|
||||
if (vatom.state & kVatomTyped) {
|
||||
@@ -367,7 +416,7 @@ void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
} else {
|
||||
if (atom.type != kAInsertSpan) {
|
||||
// Spans are cascade-internal; only emit the whole session (kAInsert).
|
||||
atom_emit(&atom, "", cascade);
|
||||
atom_emit(&atom, "");
|
||||
}
|
||||
atom_free(&atom);
|
||||
}
|
||||
@@ -412,23 +461,29 @@ static void atom_stage_flush(CmdFrame *frame)
|
||||
if (frame->staged.keys == NULL) {
|
||||
return;
|
||||
}
|
||||
frame->staged.changed = buf_get_changedtick(curbuf) != frame->tick;
|
||||
// Staged commands are edits, thus cascade. Except with no keys (poisoned Visual selection).
|
||||
bool cascade = *frame->staged.keys != NUL;
|
||||
atom_push(cascade, frame->staged);
|
||||
frame->staged = (CmdAtom){ 0 };
|
||||
}
|
||||
|
||||
/// Queues an LHS-replay atom: a mapping that edited invisibly (:normal/:call, "ds'") re-runs
|
||||
/// per cursor from its LHS + payload keys. Cascade only.
|
||||
void atom_lhs_replay_queue(void)
|
||||
/// The composite's trigger: its LHS plus the keys typed while it ran, i.e. the user "intention".
|
||||
/// @return Allocated.
|
||||
static char *atom_composite_lhs(void)
|
||||
{
|
||||
StringBuilder keys = KV_INITIAL_VALUE;
|
||||
kv_concat(keys, composite.lhs);
|
||||
kv_concat_len(keys, (char *)typed.keys.items + typed.map_start,
|
||||
kv_size(typed.keys) - typed.map_start);
|
||||
kv_push(keys, NUL);
|
||||
kv_push(g_atoms, ((CmdAtom){ .type = kAMapping, .keys = keys.items, .remap = true }));
|
||||
return keys.items;
|
||||
}
|
||||
|
||||
/// Queues an LHS-replay atom: a mapping that edited invisibly (:normal/:call, "ds'") re-runs
|
||||
/// per cursor from its LHS + payload keys. Cascade only.
|
||||
void atom_lhs_replay_queue(void)
|
||||
{
|
||||
kv_push(g_atoms, ((CmdAtom){ .type = kAMapping, .keys = atom_composite_lhs(), .remap = true }));
|
||||
}
|
||||
|
||||
/// True if the executing mapping queued a subatom: its edit was captured, no LHS-replay needed.
|
||||
@@ -449,7 +504,7 @@ static void atom_composite_start(const char *lhs, size_t len)
|
||||
xfree(composite.lhs);
|
||||
composite.lhs = xmemdupz(lhs, len);
|
||||
composite.queued = false;
|
||||
composite.tick = buf_get_changedtick(curbuf);
|
||||
composite.origin = atom_origin();
|
||||
}
|
||||
|
||||
/// Emits the composite atom with its collected subatoms (`CmdAtom.atoms`).
|
||||
@@ -462,26 +517,36 @@ static void atom_composite_end(const char *pending)
|
||||
if (composite.lhs == NULL) {
|
||||
return;
|
||||
}
|
||||
char *lhs = composite.lhs;
|
||||
composite.lhs = NULL;
|
||||
// The mapping read a payload via getchar() ("ds'"); replaying `keys` would prompt again.
|
||||
// Consumer should rerun `lhs` (which carries the payload) in "remap" mode.
|
||||
const bool remap = kv_size(typed.keys) > typed.map_start || kv_size(composite.atoms) == 0;
|
||||
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,
|
||||
.changed = buf_get_changedtick(curbuf) != composite.tick };
|
||||
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) {
|
||||
// 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 {
|
||||
atom = (CmdAtom){ .type = kAMapping, .keys = atoms_concat_keys(composite.atoms).data,
|
||||
.lhs = lhs, .changed = buf_get_changedtick(curbuf) != composite.tick };
|
||||
.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) };
|
||||
atom.atoms = composite.atoms; // Subatoms.
|
||||
composite.atoms = (CmdAtomVec)KV_INITIAL_VALUE; // Reset.
|
||||
}
|
||||
atom_emit(&atom, pending, composite.queued);
|
||||
atom_emit(&atom, pending);
|
||||
atom_free(&atom);
|
||||
}
|
||||
|
||||
@@ -729,6 +794,12 @@ void atom_map_start(const char *lhs, size_t len)
|
||||
|| Visual.active) {
|
||||
return;
|
||||
}
|
||||
if (atom_composite_active()) {
|
||||
// Mapping resolved from another's trailing prefix ("nmap x j," + "nnoremap ,w w"): end the
|
||||
// pending composite, so each `lhs` owns only the keys it produced.
|
||||
typed.map_start = kv_size(typed.keys);
|
||||
atom_composite_end("mapping");
|
||||
}
|
||||
atom_composite_start(lhs, len);
|
||||
typed.map_start = kv_size(typed.keys);
|
||||
}
|
||||
@@ -774,6 +845,7 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
xfree(suffix);
|
||||
return false;
|
||||
}
|
||||
const CmdOrigin origin = vatom.origin; // atom_visual_reset() clears the session.
|
||||
const bool prep = redoable && spec != NULL;
|
||||
if (suffix == NULL || !atom_visual_replayable()) {
|
||||
bool prepped = prep && spec->op != NUL && suffix != NULL;
|
||||
@@ -795,7 +867,7 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
atom_visual_reset(); // End the session before staging.
|
||||
if (emit) {
|
||||
atom_stage_set((CmdAtom){ .type = kAVisual, .spec = *spec, .keys = xstrdup(""),
|
||||
.lhs = label });
|
||||
.lhs = label, .origin = origin });
|
||||
}
|
||||
return prepped;
|
||||
}
|
||||
@@ -823,9 +895,11 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
// embedded in `keys` (and decomposed in `atoms`).
|
||||
.spec = spec != NULL ? *spec : (CmdSpec){ 0 },
|
||||
.keys = keys,
|
||||
.origin = origin,
|
||||
};
|
||||
if (spec != NULL) {
|
||||
kv_push(vatom.atoms, ((CmdAtom){ .type = kAOperator, .spec = *spec, .keys = suffix }));
|
||||
kv_push(vatom.atoms, ((CmdAtom){ .type = kAOperator, .spec = *spec, .keys = suffix,
|
||||
.origin = cur_frame->origin }));
|
||||
} else {
|
||||
xfree(suffix);
|
||||
}
|
||||
@@ -874,19 +948,21 @@ void atom_capture_op(oparg_T *oap, cmdarg_T *cap, bool redo_yank)
|
||||
spec.motion_force = oap->motion_force;
|
||||
spec.cmd = cap->cmdchar;
|
||||
spec.cmd2 = operand ? NUL : cap->nchar;
|
||||
spec.arg = operand ? cap->nchar : NUL;
|
||||
atom_stage_set(atom_from_spec(kAOperator, spec));
|
||||
spec.cmdarg = operand ? cap->nchar : NUL;
|
||||
CmdAtom op_atom = atom_from_spec(kAOperator, spec);
|
||||
op_atom.origin = cur_frame->origin;
|
||||
atom_stage_set(op_atom);
|
||||
}
|
||||
} else if (!Visual.active || oap->motion_force) {
|
||||
// Prepped: atom_cmd_end() derives the atom from redobuff.
|
||||
} else if (oap->op_type == OP_REPLACE && cap->nchar <= 0) {
|
||||
// Visual "r<C-V><CR>": `spec.arg` cannot represent the sentinel nchar (REPLACE_CR_NCHAR), so
|
||||
// Visual "r<C-V><CR>": `spec.cmdarg` cannot represent the sentinel (REPLACE_CR_NCHAR), so
|
||||
// hand-compose the literal suffix keys.
|
||||
char suffix[4] = { 'r', Ctrl_V, cap->nchar == REPLACE_CR_NCHAR ? CAR : NL, NUL };
|
||||
atom_visual_end_suffix(xstrdup(suffix), &spec, redoable);
|
||||
} else {
|
||||
// Visual-mode op: complete the accumulated visual atom with the operator keys.
|
||||
spec.arg = oap->op_type == OP_REPLACE ? cap->nchar : NUL;
|
||||
spec.cmdarg = oap->op_type == OP_REPLACE ? cap->nchar : NUL;
|
||||
if ((oap->op_type == OP_NR_ADD || oap->op_type == OP_NR_SUB) && cap->arg) {
|
||||
// g<C-A>: the "g" variant is distinguished by cap->arg, not the op char: compose it back.
|
||||
spec.op_extra = spec.op;
|
||||
@@ -971,7 +1047,10 @@ InsSession atom_ins_start(int cmd, long count, VisualIns vis, bool vblock)
|
||||
InsSession session = {
|
||||
.typed = atom_is_user_input(), // Sampled before the session.
|
||||
.vis = vis,
|
||||
.tick = buf_get_changedtick(curbuf),
|
||||
// A consumed selection opens the redo body, so the atom starts where the selection did.
|
||||
// Else the CmdFrame origin, from before the entry moved the cursor (a/A/…).
|
||||
.origin = vis == kVInsKeys ? vatom.origin
|
||||
: cur_frame != NULL ? cur_frame->origin : atom_origin(),
|
||||
};
|
||||
if (vis != kVInsNone && !mc_replaying()) {
|
||||
if (vis == kVInsKeys && !(atom_visual_replayable() && (vatom.state & kVatomTyped))) {
|
||||
@@ -981,6 +1060,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);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1010,7 +1093,7 @@ static void atom_ins_push(const InsSession *session, bool cascade)
|
||||
return;
|
||||
}
|
||||
atom.text = get_last_insert_save();
|
||||
atom.changed = buf_get_changedtick(curbuf) != session->tick;
|
||||
atom.origin = session->origin;
|
||||
atom_push_raw(cascade, atom);
|
||||
}
|
||||
|
||||
@@ -1018,9 +1101,7 @@ static void atom_ins_push(const InsSession *session, bool cascade)
|
||||
/// classify the command (motion, Visual-mode transition, edit). Pushes the frame (`cur_frame`).
|
||||
void atom_cmd_start(CmdFrame *old)
|
||||
{
|
||||
old->pos = curwin->w_cursor;
|
||||
old->buf = curbuf;
|
||||
old->tick = buf_get_changedtick(curbuf);
|
||||
old->origin = atom_origin();
|
||||
old->visual = Visual;
|
||||
old->keytyped = KeyTyped;
|
||||
old->captures = atom_captures;
|
||||
@@ -1059,9 +1140,9 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
// 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)
|
||||
&& buf_get_changedtick(curbuf) == old->tick
|
||||
bool unchanged = curbuf == old->origin.buf.br_buf && curwin == old->origin.win
|
||||
&& !atom_origin_moved(old->origin)
|
||||
&& !atom_origin_changed(old->origin)
|
||||
&& Visual.active == old->visual.active
|
||||
&& (!Visual.active
|
||||
|| (equalpos(old->visual.start, Visual.start)
|
||||
@@ -1075,11 +1156,9 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
if (mapped
|
||||
// NOT if cursor-global op: cascading (e.g. vim-repeat "nmap u") would undo at every cursor.
|
||||
&& !curcmd.op_global
|
||||
&& ((
|
||||
// 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->buf
|
||||
&& buf_get_changedtick(curbuf) != old->tick) || ins_cascaded)) {
|
||||
// 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)) {
|
||||
map_edit = true;
|
||||
}
|
||||
|
||||
@@ -1092,6 +1171,7 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
atom_visual_reset();
|
||||
// Decided once, at session start.
|
||||
vatom.state = (old->keytyped || atom_composite_active()) ? kVatomTyped : kVatomFed;
|
||||
vatom.origin = old->origin;
|
||||
}
|
||||
// Decided by the session (not atom_capturable()), so fed selections (":normal! vjd") still
|
||||
// accumulate for redo-prep. Recording/replay commands are meta (not part of the edit).
|
||||
@@ -1140,9 +1220,9 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
&& ca->cmdchar != '"' && ca->cmdchar != '@' && ca->cmdchar != 'Q'
|
||||
&& !scroll_cmd)
|
||||
|| special_motion;
|
||||
bool changed = buf_get_changedtick(curbuf) != old->tick;
|
||||
bool motion = curbuf == old->buf
|
||||
&& !equalpos(old->pos, curwin->w_cursor)
|
||||
bool changed = atom_origin_changed(old->origin);
|
||||
bool moved = atom_origin_moved(old->origin);
|
||||
bool motion = moved
|
||||
&& !changed
|
||||
&& !finish_op && !jump_cmd
|
||||
&& ((ca->cmdchar > 0 && ca->cmdchar < 0x100
|
||||
@@ -1170,7 +1250,7 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
// did neither is a no-op (vim-surround "ysa[" whose surround char was <Esc>).
|
||||
bool effect = changed || reg_max_ts(true) > old->reg_ts;
|
||||
if (atom.keys != NULL && *atom.keys != NUL) {
|
||||
atom.changed = changed;
|
||||
atom.origin = old->origin;
|
||||
atom_push(effect, atom);
|
||||
} else {
|
||||
atom_free(&atom);
|
||||
@@ -1179,13 +1259,13 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
&& !(vis && unchanged)) {
|
||||
// Payload typed in the cmdline ("/pat<CR>"). Emit-only. Not if pattern was not found.
|
||||
CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf);
|
||||
atom.changed = changed;
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, atom);
|
||||
} else if (!vis && curcmd.cmdline != NULL
|
||||
&& (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) {
|
||||
// Same for ":cnext<CR>" or "<Cmd>cnext<CR>". Never a Visual subatom.
|
||||
CmdAtom atom = atom_from_cmdline(kAEx, ca, curcmd.cmdline);
|
||||
atom.changed = changed;
|
||||
CmdAtom atom = atom_from_cmdline(kAExcmd, ca, curcmd.cmdline);
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, atom);
|
||||
} else if (replayable && (!vis || (keycls & kKeyPayload) == 0)) {
|
||||
// Non-redoable command (u, zz, q=): never cascaded as an edit.
|
||||
@@ -1194,8 +1274,8 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
// Omit `regname`: would prefix '"x' to every command collected; op-end re-adds it later.
|
||||
spec.regname = 0;
|
||||
}
|
||||
CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kACommand, spec);
|
||||
atom.changed = changed;
|
||||
CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kANormal, spec);
|
||||
atom.origin = old->origin;
|
||||
atom_push(follow, atom);
|
||||
} else if ((scroll_cmd || mouse_cmd) && !atom_composite_active()) {
|
||||
// Emit-only (viewport-dependent).
|
||||
@@ -1205,7 +1285,7 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
spec.count = 0;
|
||||
}
|
||||
CmdAtom atom = atom_from_spec(scroll_cmd ? kAScroll : kAMouse, spec);
|
||||
atom.changed = changed;
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, atom);
|
||||
}
|
||||
if (vis && kv_size(vatom.atoms) == collected && !unchanged) {
|
||||
@@ -1213,7 +1293,7 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
}
|
||||
if (vis && (curbuf != old->buf || buf_get_changedtick(curbuf) != old->tick)) {
|
||||
if (vis && (curbuf != old->origin.buf.br_buf || atom_origin_changed(old->origin))) {
|
||||
// Not replayable: edited buffer during selection, so the keys do not describe the change.
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ extern CmdAtomVec g_atoms;
|
||||
/// Pre-command state sampled at entry + storage for its "staged" atom. atom_cmd_end() finalizes it.
|
||||
typedef struct CmdFrame CmdFrame;
|
||||
struct CmdFrame {
|
||||
pos_T pos; ///< Cursor position.
|
||||
const buf_T *buf; ///< Current buffer.
|
||||
varnumber_T tick; ///< b:changedtick
|
||||
CmdOrigin origin; ///< State at entry.
|
||||
VisualState visual; ///< Visual-mode state (active/start/mode are diffed).
|
||||
bool keytyped; ///< KeyTyped
|
||||
uint64_t captures; ///< Capture counter.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "klib/kvec.h"
|
||||
#include "nvim/buffer_defs.h"
|
||||
#include "nvim/eval/typval_defs.h"
|
||||
#include "nvim/input_defs.h"
|
||||
|
||||
@@ -14,9 +15,7 @@
|
||||
// - replay, cascade, insert-cascade
|
||||
|
||||
typedef enum CmdAtomType {
|
||||
kACommand, ///< Not an (operator) edit, nor a cursor-move (u zz <c-w>l …). Never cascades,
|
||||
///< except as part of a mapping's composite.
|
||||
kAEx, ///< Ex command (":cnext<CR>"): the typed cmdline is the payload.
|
||||
kAExcmd, ///< Ex command (":cnext<CR>"): the typed cmdline is the payload.
|
||||
kAInsert, ///< Insert session: entry command + text + <Esc>.
|
||||
kAInsertSpan, ///< Span (chunk) of an ongoing insert-session, cascaded mid-session.
|
||||
kAJump, ///< Cursor movement by absolute/shared navigation (jumplist, marks, `*`):
|
||||
@@ -24,11 +23,21 @@ typedef enum CmdAtomType {
|
||||
kAMapping, ///< Subcommands of a mapping/macro, collapsed into one atom (`lhs`).
|
||||
kAMotion, ///< Motion (cascades in "q=" follow-motion mode).
|
||||
kAMouse, ///< Mouse action: emit-only (not replayable).
|
||||
kANormal, ///< Normal-mode command that is not a motion or jump ("u", CTRL-R, "za", …).
|
||||
///< Never cascades, except as part of a mapping's composite.
|
||||
kAOperator, ///< Operator+motion, or a self-contained edit command.
|
||||
kAScroll, ///< Scroll (CTRL-Y/D/…, wheel): emit-only, like kAMouse.
|
||||
kAVisual, ///< Visual-mode sequence ("viwee" + operator).
|
||||
} CmdAtomType;
|
||||
|
||||
/// State gathered at start of a command, composite, or insert. For calculating the "delta" at end.
|
||||
typedef struct {
|
||||
bufref_T buf; ///< Buffer.
|
||||
const win_T *win; ///< Window.
|
||||
pos_T pos; ///< Cursor position. Stored here bc the window might be closed.
|
||||
varnumber_T tick; ///< b:changedtick.
|
||||
} CmdOrigin;
|
||||
|
||||
/// How an insert-session was entered from Visual mode.
|
||||
typedef enum {
|
||||
kVInsNone, ///< Not entered from Visual mode.
|
||||
@@ -41,7 +50,7 @@ typedef enum {
|
||||
typedef struct {
|
||||
bool typed; ///< Session is user input (typed, or via mapping/macro).
|
||||
VisualIns vis; ///< Session was entered from Visual mode.
|
||||
varnumber_T tick; ///< b:changedtick at start (the session atom's `changed` baseline).
|
||||
CmdOrigin origin; ///< State at start.
|
||||
} InsSession;
|
||||
|
||||
typedef struct CmdAtom CmdAtom;
|
||||
@@ -57,8 +66,11 @@ struct CmdAtom {
|
||||
char *text; ///< Payload: insert-session text, or Ex/search cmdline.
|
||||
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.
|
||||
CmdAtomType type;
|
||||
int undoseq; ///< Undo state at settlement. Not monotonic (decreases on undo).
|
||||
bool changed; ///< The command changed the buffer.
|
||||
bool moved; ///< The command moved the cursor.
|
||||
bool remap; ///< Replay `keys` w/ remap. For replay of a payload mapping (vim-surround "ds'"),
|
||||
///< which edits invisibly (:norm/Ex) and must rerun LHS instead of resolved keys.
|
||||
};
|
||||
|
||||
@@ -30,8 +30,8 @@ typedef struct {
|
||||
int op_extra; ///< Second operator char ("g~" => '~'; 0 = none)
|
||||
int motion_force; ///< Forced motion type ('v'/'V'/CTRL-V; 0 = none)
|
||||
int cmd; ///< Command/motion char ('J', 'p', 'f', K_LEFT, …; 0 = none)
|
||||
int cmd2; ///< Second char of a two-char command name ("gJ" => 'J'; 0 = none)
|
||||
int arg; ///< Operand ("fx" => 'x', "ma" => 'a'; 0 = none)
|
||||
int cmd2; ///< Command char 2 (for two-char cmds: "gJ" => 'J'; 0 = none)
|
||||
int cmdarg; ///< Command operand ("fx" => 'x', "ma" => 'a'; 0 = none)
|
||||
StringBuilder body; ///< Captured cmd "body": keys without the `["x][count]` "prefix", updated
|
||||
///< as the cmd executes (redo_append_xx); redo_keys() prepends the prefix.
|
||||
///< Empty in spec-only contexts (i.e. not a "redo" buf).
|
||||
|
||||
@@ -4585,7 +4585,7 @@ static void nv_replace(cmdarg_T *cap)
|
||||
invoke_edit(cap, true, 'r', false);
|
||||
} else {
|
||||
prep_redo(true, true, (CmdSpec){ .regname = cap->oap->regname, .count = cap->count1,
|
||||
.cmd = 'r', .arg = cap->nchar });
|
||||
.cmd = 'r', .cmdarg = cap->nchar });
|
||||
if (had_ctrl_v != NUL) {
|
||||
redo_append_char(had_ctrl_v);
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('CmdAtom', function()
|
||||
feed('3,c')
|
||||
local cmdev = atom_last()
|
||||
eq({
|
||||
type = 'ex',
|
||||
type = 'excmd',
|
||||
lhs = ',c',
|
||||
keys = k('3<Cmd>call setline(1, "N" . v:count)<NL>'),
|
||||
text = 'call setline(1, "N" . v:count)',
|
||||
@@ -161,7 +161,7 @@ describe('CmdAtom', function()
|
||||
{ type = 'motion', keys = 'w' },
|
||||
{ type = 'motion', keys = '3l' },
|
||||
{ type = 'motion', keys = k('/gamma<NL>') },
|
||||
{ type = 'ex', keys = k(':nohlsearch<NL>') },
|
||||
{ type = 'excmd', keys = k(':nohlsearch<NL>') },
|
||||
}, atoms_tail(4, 'type', 'keys'))
|
||||
-- ":" embeds its count as the range prefill, never as composed digits;
|
||||
-- the count field carries it.
|
||||
@@ -169,7 +169,7 @@ describe('CmdAtom', function()
|
||||
feed('2:<CR>')
|
||||
eq(2, fn.line('.'))
|
||||
eq(
|
||||
{ type = 'ex', keys = k(':.,.+1<NL>'), count = 2, cmd = ':' },
|
||||
{ type = 'excmd', keys = k(':.,.+1<NL>'), count = 2, cmd = ':' },
|
||||
pick(atom_last(), 'type', 'keys', 'count', 'cmd')
|
||||
)
|
||||
eq({ 'alpha beta', 'gamma delta' }, get_lines()) -- nothing was edited
|
||||
@@ -180,11 +180,11 @@ describe('CmdAtom', function()
|
||||
api.nvim_input_mouse('wheel', 'up', '', 0, 0, 0)
|
||||
api.nvim_input_mouse('left', 'press', '', 0, 1, 2)
|
||||
eq({
|
||||
{ type = 'scroll', keys = k('<C-E>'), cascade = false },
|
||||
{ type = 'scroll', keys = k('3<C-Y>'), cascade = false },
|
||||
{ type = 'scroll', keys = k('<ScrollWheelUp>'), cascade = false },
|
||||
{ type = 'mouse', keys = k('<LeftMouse>'), cascade = false },
|
||||
}, atoms_tail(4, 'type', 'keys', 'cascade'))
|
||||
{ type = 'scroll', keys = k('<C-E>') },
|
||||
{ type = 'scroll', keys = k('3<C-Y>') },
|
||||
{ type = 'scroll', keys = k('<ScrollWheelUp>') },
|
||||
{ type = 'mouse', keys = k('<LeftMouse>') },
|
||||
}, atoms_tail(4, 'type', 'keys'))
|
||||
eq(2, fn.line('.')) -- the click moved the cursor
|
||||
-- <amatch>/match is the atom type, never path-expanded.
|
||||
n.exec_lua([[
|
||||
@@ -216,7 +216,7 @@ describe('CmdAtom', function()
|
||||
local op = atoms()[#atoms() - 1] -- "3dl"
|
||||
eq(
|
||||
{ operator = 'd', cmd = 'l', changed = true },
|
||||
pick(op, 'operator', 'cmd', 'arg', 'motionforce', 'changed')
|
||||
pick(op, 'operator', 'cmd', 'cmdarg', 'motionforce', 'changed')
|
||||
)
|
||||
-- A visual atom carries the completing operator's fields, and decomposes
|
||||
-- into its commands ("v", "f," and the operator).
|
||||
@@ -225,11 +225,11 @@ describe('CmdAtom', function()
|
||||
eq(
|
||||
{
|
||||
{ keys = 'v', cmd = 'v', changed = false },
|
||||
{ keys = 'f,', cmd = 'f', arg = ',', changed = false },
|
||||
{ keys = 'f,', cmd = 'f', cmdarg = ',', changed = false },
|
||||
{ keys = 'd', changed = true }, -- the completing operator did the edit
|
||||
},
|
||||
vim.tbl_map(function(c)
|
||||
return pick(c, 'keys', 'cmd', 'arg', 'changed')
|
||||
return pick(c, 'keys', 'cmd', 'cmdarg', 'changed')
|
||||
end, vis.atoms)
|
||||
)
|
||||
eq('d', vis.atoms[3].operator)
|
||||
@@ -249,12 +249,12 @@ describe('CmdAtom', function()
|
||||
fn.setline(1, { 'one', 'two' })
|
||||
feed('gg0magJ')
|
||||
eq({ 'onetwo' }, get_lines()) -- "gJ": join without inserting a space
|
||||
eq({ cmd = 'm', arg = 'a' }, pick(atoms()[#atoms() - 1], 'cmd', 'arg'))
|
||||
eq({ cmd = 'gJ' }, pick(atom_last(), 'cmd', 'arg'))
|
||||
eq({ cmd = 'm', cmdarg = 'a' }, pick(atoms()[#atoms() - 1], 'cmd', 'cmdarg'))
|
||||
eq({ cmd = 'gJ' }, pick(atom_last(), 'cmd', 'cmdarg'))
|
||||
local nrec = #atoms()
|
||||
feed('qax') -- the recording register is an operand, not part of the name
|
||||
feed('q')
|
||||
eq({ cmd = 'q', arg = 'a' }, pick(atoms()[nrec + 1], 'cmd', 'arg'))
|
||||
eq({ cmd = 'q', cmdarg = 'a' }, pick(atoms()[nrec + 1], 'cmd', 'cmdarg'))
|
||||
-- Forced motion type ("dvj") is a field.
|
||||
fn.setline(1, { 'one', 'two' })
|
||||
feed('gg0dvj')
|
||||
@@ -578,7 +578,7 @@ describe('CmdAtom', function()
|
||||
atoms_start()
|
||||
-- "." with nothing to repeat stuffs nothing.
|
||||
feed('.')
|
||||
eq({ { type = 'command', keys = '.', lhs = '.' } }, atoms_tail(1, 'type', 'keys', 'lhs'))
|
||||
eq({ { type = 'normal', keys = '.', lhs = '.' } }, atoms_tail(1, 'type', 'keys', 'lhs'))
|
||||
-- Operators: the atom is the redobuff (count/register included).
|
||||
-- "x" is normalized ("translated") to the elemental command "dl".
|
||||
atom('x', 'dl', 'x')
|
||||
@@ -640,14 +640,14 @@ describe('CmdAtom', function()
|
||||
eq('motion', atom_last().type)
|
||||
-- Jumps: absolute/shared-state navigation, their own kind.
|
||||
atom('ma', 'ma')
|
||||
eq('command', atom_last().type) -- "m" sets state; it does not jump
|
||||
eq('normal', atom_last().type) -- "m" sets state; it does not jump
|
||||
atom('`a', '`a')
|
||||
eq('jump', atom_last().type)
|
||||
atom('<C-o>', '<C-O>')
|
||||
eq('jump', atom_last().type)
|
||||
-- Non-redoable commands: still emitted, as type "command".
|
||||
atom('zz', 'zz')
|
||||
eq('command', atom_last().type)
|
||||
eq('normal', atom_last().type)
|
||||
atom('u', 'u')
|
||||
atom('<C-r>', '<C-R>')
|
||||
-- "." emits its resolution (like "x" => "dl").
|
||||
@@ -670,7 +670,7 @@ describe('CmdAtom', function()
|
||||
atom('2/beta<CR>', '2/beta<NL>')
|
||||
-- Ex commands: their own atom kind, the cmdline is the "text" payload.
|
||||
atom(':set tw=42<CR>', ':set<Space>tw=42<NL>')
|
||||
eq({ type = 'ex', text = 'set tw=42' }, pick(atom_last(), 'type', 'text'))
|
||||
eq({ type = 'excmd', text = 'set tw=42' }, pick(atom_last(), 'type', 'text'))
|
||||
-- A nested cmdline opened by the command's own execution (":normal")
|
||||
-- does not hijack the payload.
|
||||
atom(':exe "normal! :echo 1\\r"<CR>', ':exe<Space>"normal!<Space>:echo<Space>1<Bslash>r"<NL>')
|
||||
@@ -691,6 +691,22 @@ describe('CmdAtom', function()
|
||||
feed(',V')
|
||||
eq({ lhs = ',V', pending = 'visual' }, pick(atom_last(), 'lhs', 'pending'))
|
||||
feed('<Esc>')
|
||||
-- A mapping whose trailing prefix is completed by TYPED keys ("," + "w")
|
||||
-- ends where its own keys stop: each `lhs` owns only what it produced.
|
||||
command('set notimeout')
|
||||
command('nmap ,x j,')
|
||||
command('nnoremap ,w w')
|
||||
feed('gg0')
|
||||
before = #atoms()
|
||||
feed(',xw') -- no poke between: nvim awaits the pending "," under 'notimeout'
|
||||
eq(before + 2, #atoms())
|
||||
eq({
|
||||
{ type = 'motion', lhs = ',x', keys = 'j', pending = 'mapping' },
|
||||
{ type = 'motion', lhs = ',w', keys = 'w' },
|
||||
}, atoms_tail(2, 'type', 'lhs', 'keys', 'pending'))
|
||||
command('set notimeout&')
|
||||
command('nunmap ,x')
|
||||
command('nunmap ,w')
|
||||
-- An ABORTED mapping emits nothing: an error discards its remaining
|
||||
-- keys, and the composite with them.
|
||||
command('nnoremap ,E :NoSuchCmd<CR>x')
|
||||
@@ -739,7 +755,7 @@ describe('CmdAtom', function()
|
||||
feed('za')
|
||||
eq({ 'za' }, atoms_tail(1))
|
||||
-- Neither an operator nor a motion: its own kind.
|
||||
eq('command', atom_last().type)
|
||||
eq('normal', atom_last().type)
|
||||
eq(-1, fn.foldclosed(1))
|
||||
end)
|
||||
|
||||
@@ -762,14 +778,18 @@ describe('CmdAtom', function()
|
||||
operator = 'd',
|
||||
cmd = 'w',
|
||||
changed = true,
|
||||
cascade = true, -- the edit is cascadable
|
||||
lhs = ',d',
|
||||
moved = false, -- "dw" deletes at the cursor: it does not move
|
||||
pos = { 1, 0 }, -- cursor BEFORE the action, like nvim_win_get_cursor()
|
||||
undoseq = 2, -- undo state AFTER it; decreases on undo (see |restore-undo-cursor|)
|
||||
}, evs[#evs])
|
||||
-- Count and register are captured; one event per occurrence.
|
||||
feed('"z2dw')
|
||||
feed('"z2dw')
|
||||
evs = atoms()
|
||||
eq({ keys = '"z2dw', count = 2, reg = 'z' }, pick(evs[#evs], 'keys', 'count', 'reg'))
|
||||
-- Identical occurrences, except `undoseq`: each edit is a new undo state.
|
||||
evs[#evs].undoseq, evs[#evs - 1].undoseq = nil, nil
|
||||
eq(evs[#evs - 1], evs[#evs])
|
||||
end)
|
||||
|
||||
@@ -874,14 +894,196 @@ describe('CmdAtom', function()
|
||||
it('a ":call" payload mapping publishes its resolved RHS', function()
|
||||
-- The atom published to CmdAtom carries the RESOLVED RHS (the ":call" line).
|
||||
n.exec(t_atom.delsurround_vim)
|
||||
fn.setline(1, { 'a (one)' })
|
||||
fn.setline(1, { 'a (one)', 'b (two)' })
|
||||
feed('gg0f(')
|
||||
atoms_start()
|
||||
feed('ds)') -- ")" is the getchar()'d payload
|
||||
eq({ 'a one' }, get_lines())
|
||||
eq({ 'a one', 'b (two)' }, get_lines())
|
||||
local ev = atoms()[#atoms()]
|
||||
eq('ds', ev.lhs)
|
||||
t.matches(':call DelSurround%(%)', ev.keys)
|
||||
-- Replaying those keys would prompt for the payload again, so the atom asks
|
||||
-- for LHS-replay instead: `lhs` carries the payload, `remap` says to remap.
|
||||
eq({ lhs = 'ds)', remap = true }, pick(ev, 'lhs', 'remap'))
|
||||
feed('2G0f(')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'm', false)]]):format(ev.lhs))
|
||||
n.poke_eventloop()
|
||||
eq({ 'a one', 'b two' }, get_lines())
|
||||
end)
|
||||
|
||||
it('|restore-undo-cursor|: `pos` + `undoseq` restore across every undo form', function()
|
||||
-- Keep in sync with the example in runtime/doc/repeat.txt.
|
||||
n.exec_lua([[
|
||||
local seen = {} -- buf -> { prev = <seq>, [seq] = <cursor before that edit> }
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
callback = function(ev)
|
||||
local seq = ev.data.undoseq
|
||||
if not seq then
|
||||
return
|
||||
end
|
||||
local s = seen[ev.buf] or {}
|
||||
seen[ev.buf] = s
|
||||
if s.prev and seq < s.prev and s[seq + 1] then
|
||||
vim.api.nvim_win_set_cursor(0, s[seq + 1]) -- undid: first state left behind
|
||||
elseif s.prev and seq > s.prev and s[seq] then
|
||||
vim.api.nvim_win_set_cursor(0, s[seq]) -- redid: a seq we already have
|
||||
elseif ev.data.changed and not s[seq] then
|
||||
s[seq] = ev.data.pos -- new edit
|
||||
end
|
||||
s.prev = seq
|
||||
end,
|
||||
})
|
||||
]])
|
||||
command('nnoremap <F8> u')
|
||||
--- Puts the cursor at `pre`, edits, then undoes via `undo`. Reports the
|
||||
--- column before the edit and after the undo: they must match.
|
||||
local function undone(pre, edit, undo)
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'this is a test' })
|
||||
n.poke_eventloop()
|
||||
feed('gg0' .. pre)
|
||||
n.poke_eventloop()
|
||||
local before = fn.col('.')
|
||||
feed(edit)
|
||||
n.poke_eventloop()
|
||||
-- Drained between steps: batched atoms would deliver the undo's restore
|
||||
-- only after the redo already ran.
|
||||
for _, step in ipairs(type(undo) == 'table' and undo or { undo }) do
|
||||
if step:sub(1, 1) == ':' then
|
||||
command(step:sub(2))
|
||||
else
|
||||
feed(step)
|
||||
end
|
||||
n.poke_eventloop()
|
||||
end
|
||||
return { before, fn.col('.') }
|
||||
end
|
||||
-- "a"/"o" enter Insert AFTER moving the cursor, so the session's `pos` must
|
||||
-- come from the command frame, not from where the entry command landed.
|
||||
-- The last five need `undoseq`: they are not recognizable from the keys.
|
||||
eq(
|
||||
{ { 4, 4 }, { 4, 4 }, { 4, 4 }, { 7, 7 }, { 4, 4 }, { 4, 4 }, { 4, 4 }, { 4, 4 }, { 4, 4 } },
|
||||
{
|
||||
undone('lll', 'diw', 'u'),
|
||||
undone('lll', 'aXY<Esc>', 'u'),
|
||||
undone('lll', 'oXY<Esc>', 'u'),
|
||||
undone('llllll', 'd^', 'u'),
|
||||
undone('lll', 'diw', '<F8>'), -- mapping to undo
|
||||
undone('lll', 'diwwdawdaw', '3u'), -- count
|
||||
undone('lll', 'diwwdaw', ':undo 1'),
|
||||
undone('lll', 'diw', 'g-'),
|
||||
undone('lll', 'diw', { 'u', '0', '<C-r>' }), -- redo, after moving away
|
||||
}
|
||||
)
|
||||
end)
|
||||
|
||||
it('|atom-repeat|: one recipe replays every class of atom', function()
|
||||
-- Keep in sync with the `replay()` example in runtime/doc/repeat.txt.
|
||||
n.exec_lua([[
|
||||
_G.saved = nil
|
||||
vim.api.nvim_create_autocmd('CmdAtom', { callback = function(ev) _G.last = ev.data end })
|
||||
_G.save = function() _G.saved = _G.last end
|
||||
_G.replay = function()
|
||||
local d = _G.saved
|
||||
if not d.remap and d.keys == '' then
|
||||
return -- Unreplayable Visual op: feeding `lhs` would be wrong.
|
||||
end
|
||||
vim.api.nvim_feedkeys(d.remap and d.lhs or d.keys, d.remap and 'm' or 'n', false)
|
||||
end
|
||||
]])
|
||||
n.exec(t_atom.delsurround_vim)
|
||||
command('nnoremap ,d x')
|
||||
command('nnoremap <F6> xw')
|
||||
command('let @q = "x"')
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('n', ']e', function()
|
||||
vim.api.nvim_set_current_line(vim.api.nvim_get_current_line() .. '!')
|
||||
end)
|
||||
]])
|
||||
|
||||
--- Runs `keys` on line 1, then replays that atom on line 2 (both prefixed by `pre`). Reports
|
||||
--- the atom's input/resolution pair, whether the resolution decomposes into subatoms, and
|
||||
--- whether the replay reproduced line 1.
|
||||
local function both(line, pre, keys)
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { line, line })
|
||||
feed('gg0' .. pre)
|
||||
n.poke_eventloop()
|
||||
feed(keys)
|
||||
n.poke_eventloop()
|
||||
n.exec_lua('_G.save()')
|
||||
feed('2G0' .. pre)
|
||||
n.poke_eventloop()
|
||||
n.exec_lua('_G.replay()')
|
||||
n.poke_eventloop()
|
||||
local l = get_lines()
|
||||
local d = n.exec_lua('return _G.saved')
|
||||
local subs ---@type string?
|
||||
for _, c in ipairs(d.atoms or {}) do
|
||||
subs = (subs or '') .. c.keys
|
||||
end
|
||||
return { lhs = d.lhs, keys = d.keys, remap = d.remap, subs = subs, replayed = l[1] == l[2] }
|
||||
end
|
||||
|
||||
-- Asserts the table in runtime/doc/repeat.txt.
|
||||
-- Note the symmetry: "x", a mapping to "x", and a macro of "x" all resolve to "dl".
|
||||
eq({
|
||||
{ lhs = 'dw', keys = 'dw', replayed = true },
|
||||
{ lhs = 'x', keys = 'dl', replayed = true },
|
||||
{ lhs = ',d', keys = 'dl', replayed = true },
|
||||
{ lhs = '@q', keys = 'dl', replayed = true },
|
||||
{ lhs = k('<F6>'), keys = 'dlw', subs = 'dlw', replayed = true },
|
||||
-- `keys` cannot replay these two, so the recipe feeds `lhs` with remapping.
|
||||
{ lhs = ']e', keys = '', remap = true, replayed = true },
|
||||
{ lhs = 'ds)', keys = ':call DelSurround()\n', remap = true, replayed = true },
|
||||
}, {
|
||||
both('a one two', '', 'dw'),
|
||||
both('aaa bbb', '', 'x'),
|
||||
both('aaa bbb', '', ',d'),
|
||||
both('aaa bbb', '', '@q'),
|
||||
both('aaa bbb ccc', '', '<F6>'),
|
||||
both('a b', '', ']e'),
|
||||
both('a (one)', 'f(', 'ds)'),
|
||||
})
|
||||
|
||||
-- Repeat fold command ("zf"). #9821
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'a {', ' x', '}', 'b {', ' y', '}' })
|
||||
feed('gg0zfa{')
|
||||
n.poke_eventloop()
|
||||
n.exec_lua('_G.save()')
|
||||
eq(
|
||||
{ lhs = 'zfa{', keys = 'zfa{', changed = false, operator = 'zf' },
|
||||
pick(n.exec_lua('return _G.saved'), 'lhs', 'keys', 'changed', 'operator')
|
||||
)
|
||||
feed('zR4gg0') -- open the new fold, move past it
|
||||
eq(0, fn.foldlevel(4))
|
||||
n.exec_lua('_G.replay()')
|
||||
n.poke_eventloop()
|
||||
eq({ 4, 6 }, { fn.foldclosed(4), fn.foldclosedend(4) }) -- "zf" closes what it creates
|
||||
|
||||
-- Unreplayable: a void Visual op has no keys AND no `remap`, so the recipe
|
||||
-- skips it rather than replaying a viewport-dependent selection.
|
||||
local lines = {}
|
||||
for i = 1, 30 do
|
||||
lines[i] = 'l' .. i
|
||||
end
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, lines)
|
||||
feed('gg')
|
||||
feed(k('V<C-e>d'))
|
||||
n.poke_eventloop()
|
||||
n.exec_lua('_G.save()')
|
||||
eq({ keys = '', remap = nil }, pick(n.exec_lua('return _G.saved'), 'keys', 'remap'))
|
||||
local before = get_lines()
|
||||
n.exec_lua('_G.replay()')
|
||||
n.poke_eventloop()
|
||||
eq(before, get_lines())
|
||||
|
||||
-- A repeat mapping is an atom too, and `remap` sends the recipe back to its
|
||||
-- own `lhs`: a recorder must skip it, or the repeat replays itself.
|
||||
n.exec_lua([[vim.keymap.set('n', '<F7>', function() end)]])
|
||||
feed('<F7>')
|
||||
n.poke_eventloop()
|
||||
eq(
|
||||
{ type = 'mapping', keys = '', lhs = k('<F7>'), remap = true },
|
||||
pick(n.exec_lua('return _G.last'), 'type', 'keys', 'lhs', 'remap')
|
||||
)
|
||||
end)
|
||||
|
||||
it('"," repeats the last motion atom', function()
|
||||
@@ -999,4 +1201,105 @@ describe('CmdAtom', function()
|
||||
eq(height + 2, fn.winheight(0))
|
||||
wait_active(true)
|
||||
end)
|
||||
|
||||
it('reports the original buffer', function()
|
||||
n.exec_lua([[
|
||||
_G.evs = {}
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
callback = function(ev)
|
||||
table.insert(_G.evs, { buf = ev.buf, keys = ev.data.keys, changed = ev.data.changed })
|
||||
end,
|
||||
})
|
||||
]])
|
||||
command('let g:abufs = []')
|
||||
command("autocmd CmdAtom * call add(g:abufs, expand('<abuf>')->str2nr())")
|
||||
local a = api.nvim_get_current_buf()
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa bbb' })
|
||||
command('badd other')
|
||||
local b = api.nvim_list_bufs()[2]
|
||||
command('nnoremap ]b <Cmd>bprev<CR>')
|
||||
|
||||
-- An edit reports its own buffer, and `changed`.
|
||||
feed('gg0dw')
|
||||
-- Leaving the buffer reports the buffer the command ran IN, not the one it
|
||||
-- landed in. `changed` is about that same buffer: ":bnext" edits nothing.
|
||||
feed(':bnext<CR>')
|
||||
eq(b, api.nvim_get_current_buf())
|
||||
-- Same, from a mapping, in the other direction.
|
||||
feed(']b')
|
||||
eq(a, api.nvim_get_current_buf())
|
||||
local evs = n.exec_lua('return _G.evs')
|
||||
eq({
|
||||
{ buf = a, keys = 'dw', changed = true },
|
||||
{ buf = a, keys = ':bnext\n', changed = false },
|
||||
{ buf = b, keys = ('%sbprev\n'):format(k('<Cmd>')), changed = false },
|
||||
}, { evs[#evs - 2], evs[#evs - 1], evs[#evs] })
|
||||
-- <abuf> agrees with the Lua callback's `buf`.
|
||||
local abufs = n.eval('g:abufs')
|
||||
eq({ a, a, b }, { abufs[#abufs - 2], abufs[#abufs - 1], abufs[#abufs] })
|
||||
end)
|
||||
|
||||
it('reports `moved`/`pos` for the original buffer+window', function()
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa bbb', 'ccc ddd', 'eee fff' })
|
||||
n.exec_lua([[
|
||||
_G.jump = function() vim.api.nvim_win_set_cursor(0, { 3, 0 }) end
|
||||
vim.keymap.set('n', ']c', '<Cmd>lua _G.jump()<CR>')
|
||||
]])
|
||||
atoms_start()
|
||||
|
||||
-- A plugin motion via "<Cmd>" is type="ex" (its RHS implementation), but the
|
||||
-- observed effect classifies it: moved, and did not change the buffer.
|
||||
feed('gg0]c')
|
||||
eq(3, fn.line('.'))
|
||||
eq(
|
||||
{ type = 'excmd', moved = true, changed = false, pos = { 1, 0 } },
|
||||
pick(atom_last(), 'type', 'moved', 'changed', 'pos')
|
||||
)
|
||||
-- Same observed effect as a builtin motion, which is the point.
|
||||
feed('gg0w')
|
||||
eq(
|
||||
{ type = 'motion', moved = true, changed = false },
|
||||
pick(atom_last(), 'type', 'moved', 'changed')
|
||||
)
|
||||
|
||||
-- Register-only operator also moves without changing: a motion carries no `operator`.
|
||||
feed('gg0wyb')
|
||||
eq(
|
||||
{ moved = true, changed = false, operator = 'y' },
|
||||
pick(atom_last(), 'moved', 'changed', 'operator')
|
||||
)
|
||||
|
||||
-- Switching window is not a cursor-move, even on the SAME buffer: `pos` is per-window.
|
||||
command('split')
|
||||
feed('gg')
|
||||
command('wincmd j')
|
||||
feed('3G')
|
||||
command('wincmd k')
|
||||
feed('<C-w>w')
|
||||
eq(3, fn.line('.'))
|
||||
eq({ type = 'normal', moved = false }, pick(atom_last(), 'type', 'moved'))
|
||||
|
||||
-- Both are measured on the buffer/window the action STARTED in, so acting
|
||||
-- and then navigating away still reports the effect.
|
||||
command('nnoremap ]x <Cmd>normal! dd<CR><Cmd>wincmd w<CR>')
|
||||
command('nnoremap ]y <Cmd>normal! j<CR><Cmd>wincmd w<CR>')
|
||||
feed('gg]x')
|
||||
eq({ changed = true, moved = false }, pick(atom_last(), 'changed', 'moved'))
|
||||
command('wincmd w')
|
||||
feed('gg]y')
|
||||
eq({ changed = false, moved = true }, pick(atom_last(), 'changed', 'moved'))
|
||||
|
||||
-- Switching buffer is not a cursor-move.
|
||||
command('only')
|
||||
local main = api.nvim_get_current_buf()
|
||||
command('edit other')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'x', 'y', 'z' })
|
||||
feed('3G')
|
||||
feed('<C-^>') -- back to `main`, whose cursor is on another line
|
||||
eq(main, api.nvim_get_current_buf())
|
||||
eq(
|
||||
{ type = 'normal', moved = false, pos = { 3, 0 } },
|
||||
pick(atom_last(), 'type', 'moved', 'pos')
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
Reference in New Issue
Block a user