mirror of
https://github.com/neovim/neovim.git
synced 2026-08-25 08:31:51 +00:00
feat(cmdatom): mappings capture continuation
Problem:
A mapping that ends mid-operation (`nnoremap ,D d`) emits a content-free
"mapping" atom plus a `pending` field, and the "continuation" motion
arrives as a sibling atom. Consumers must stitch the two together (which
has broken cases, e.g. Insert-opening mappings (",i") lose their session
entirely).
- ",i": the session atom is dropped bc the mapping RHS is consumed
before the session starts (typebuf_maplen()==0), so
atom_is_user_input()=false.
- ":normal"-in-opfunc: the opfunc internal "v..y" session (a) became
kVatomTyped just because the deferred composite was open, masking the
real operator capture via atom_captures, and (b) its nested frames
re-derived the outer redo.
- "Motion" based on `moved=true`, has false negatives.
- `CmdAtom.remap` is unnecessary, and clutters the docs/usage.
Solution:
- Introduce `frame_id` to identify CmdFrames.
- Classify `type=motion` better, via `NV_MOTION` flag on the `nv_cmds` table.
- Drop `CmdAtom.pending`, `CmdAtom.remap`.
- Defer atom_composite_end() at the clock edge while an operator is
pending, Visual is active, or `restart_edit` is set: the composite
keeps collecting, so the continuation is captured in the mapping atom.
- ",i": Now an open composite counts as user input.
- ":normal"-in-opfunc: Now handled correctly.
- `remap` is now decided by `composite.payload || 0 subatoms`.
atom_payload_mark() records the read the resolution never captures.
- `toplevel` is now decided by `CmdFrame.parent == NULL`.
before/after:
INPUT BEFORE AFTER
---------------------------------------------------------------------
,D w {mapping lhs=,D pending=operator} {operator lhs=,Dw keys=dw}
+ {operator keys=dw}
ysiw" {mapping lhs=ys pending=operator} {operator lhs=ysiw" keys=g@iw"}
+ {operator lhs=g@iw"}
,v d {mapping pending=visual} {visual lhs=,vd keys=viwd}
+ {visual lhs=viwd}
,i XY<Esc> {normal keys=i lhs=,iXY<Esc>} {insert keys=1iXY<Esc> text=XY}
This commit is contained in:
@@ -461,6 +461,7 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
Raw internal bytes (use |keytrans()| to get
|
||||
key-notation): feed to |nvim_feedkeys()|
|
||||
(mode "n") to replay. Empty: unreplayable.
|
||||
Nil: lossy capture, replay `lhs` (mode "m").
|
||||
- lhs: User input, before it is
|
||||
resolved/translated. Mapping LHS plus any
|
||||
payload it read (|getchar()|), macro register
|
||||
@@ -468,21 +469,12 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
`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`
|
||||
- moved: Cursor moved.
|
||||
- operator: Operator: "d", "g@", "zf", ….
|
||||
|key-notation|, like `cmd`.
|
||||
- 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.
|
||||
@@ -499,7 +491,9 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
absolute/shared navigation state
|
||||
(|jumplist|, marks, |star|).
|
||||
- "mapping"
|
||||
- "motion"
|
||||
- "motion": Non-jump motion. May emit
|
||||
`moved=false` ("w" at buffer end, failed
|
||||
"fx", …).
|
||||
- "mouse"
|
||||
- "normal": Non-motion/jump command ("u",
|
||||
CTRL-R, "za", CTRL-W, …).
|
||||
|
||||
@@ -567,6 +567,8 @@ CONCEPTS
|
||||
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.
|
||||
|
||||
IMPLEMENTATION
|
||||
|
||||
|
||||
@@ -53,52 +53,69 @@ 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"`
|
||||
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
|
||||
,Dw "dw" Incomplete mapping: ended mid-
|
||||
operation (`:nnoremap ,D d`),
|
||||
captures its continuation ("w").
|
||||
ds) ":call …<NL>)" "Payload" mapping (e.g. vim-surround
|
||||
"ds'" reads |getchar()|).
|
||||
]q nil Lua mapping.
|
||||
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
|
||||
To repeat an atom, feed `keys` (mode "n"), or `lhs` (mode "m") if `keys=nil`. >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)
|
||||
vim.api.nvim_feedkeys(a.keys or a.lhs, a.keys and 'n' or 'm', false)
|
||||
end
|
||||
<
|
||||
*motion-repeat*
|
||||
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
|
||||
"/pat<CR>", "]c"). Drop the `pattern` to repeat any non-edit action, motion or
|
||||
not ("zz", "zfa{", CTRL-W_w): >lua
|
||||
|
||||
local last ---@type vim.event.cmdatom.data?
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
pattern = 'motion',
|
||||
callback = function(ev)
|
||||
if not ev.data.moved or ev.data.changed or ev.data.lhs == ',' then
|
||||
return -- Skip non-motions, edits, and this mapping itself.
|
||||
if ev.data.changed or ev.data.lhs == ',' then
|
||||
return -- Skip edits, and this mapping itself.
|
||||
end
|
||||
last = ev.data
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', ',', function()
|
||||
-- CmdAtom is deferred: schedule the replay, in case "," follows a motion.
|
||||
-- CmdAtom is deferred; schedule the replay, in case "," follows a motion.
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
replay(last)
|
||||
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
<
|
||||
*dot-repeat*
|
||||
Example: This "." mapping repeats ANY edit, including operations provided by
|
||||
plugins, without the need for "announcement" via vim-repeat or similar. >lua
|
||||
|
||||
local last ---@type vim.event.cmdatom.data?
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
callback = function(ev)
|
||||
if ev.data.changed and ev.data.lhs ~= '.' then
|
||||
last = ev.data
|
||||
end
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', '.', function()
|
||||
-- CmdAtom is deferred; schedule the replay, in case "." follows an edit.
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -11,15 +11,13 @@ error('Cannot require a meta file')
|
||||
--- @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 High-level user input: mapping LHS + any payload it read, or macro register ("gj", "ds'", "@q"). Raw bytes.
|
||||
--- @field keys? string Resolved keysequence, raw bytes. Replay via `feedkeys(keys, 'n')`. Nil: lossy capture, replay via `feedkeys(lhs, 'm')` instead. Empty: unreplayable.
|
||||
--- @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? '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 'excmd'|'insert'|'jump'|'mapping'|'motion'|'mouse'|'normal'|'operator'|'scroll'|'visual'
|
||||
--- @field undoseq? integer Undo state after the action (`undotree().seq_cur`). Decreases on undo.
|
||||
|
||||
@@ -4712,8 +4712,10 @@ void get_user_input(const typval_T *const argvars, typval_T *const rettv, const
|
||||
|
||||
const int save_ex_normal_busy = ex_normal_busy;
|
||||
ex_normal_busy = 0;
|
||||
atom_payload_start();
|
||||
rettv->vval.v_string = getcmdline_prompt(secret ? NUL : '@', p, get_echo_hl_id(),
|
||||
xp_type, xp_arg, input_callback, false, NULL);
|
||||
atom_payload_end();
|
||||
ex_normal_busy = save_ex_normal_busy;
|
||||
callback_free(&input_callback);
|
||||
|
||||
|
||||
@@ -1951,6 +1951,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number
|
||||
if (!simplify) {
|
||||
no_reduce_keys++;
|
||||
}
|
||||
atom_payload_start();
|
||||
while (true) {
|
||||
if (cursor_flag == 'm' || (cursor_flag == NUL && msg_col > 0)) {
|
||||
ui_cursor_goto(msg_row, msg_col);
|
||||
@@ -1990,6 +1991,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number
|
||||
}
|
||||
break;
|
||||
}
|
||||
atom_payload_end();
|
||||
no_mapping--;
|
||||
allow_keys--;
|
||||
if (!simplify) {
|
||||
@@ -2245,7 +2247,7 @@ static int char_iter(const uint8_t **itp, int nomap)
|
||||
/// - When there is no match yet, return map_result_nomatch, need to get more
|
||||
/// typeahead.
|
||||
/// - On failure (out of memory) return map_result_fail.
|
||||
static int handle_mapping(int *keylenp, const bool *timedout, int *mapdepth)
|
||||
static int handle_mapping(int *keylenp, const bool *timedout, int *mapdepth, bool advance)
|
||||
FUNC_ATTR_NONNULL_ARG(1)
|
||||
{
|
||||
mapblock_T *mp = NULL;
|
||||
@@ -2495,7 +2497,7 @@ static int handle_mapping(int *keylenp, const bool *timedout, int *mapdepth)
|
||||
(size_t)(keylen - typebuf.tb_maplen));
|
||||
// A typed key sequence resolved this mapping (not a nested expansion:
|
||||
// those keys come from another mapping): open its composite.
|
||||
atom_map_start(mp->m_keys, (size_t)mp->m_keylen);
|
||||
atom_map_start(mp->m_keys, (size_t)mp->m_keylen, !advance);
|
||||
}
|
||||
|
||||
cmd_silent = (typebuf.tb_silent > 0);
|
||||
@@ -2779,7 +2781,8 @@ static int vgetorpeek(bool advance)
|
||||
break;
|
||||
} else if (typebuf.tb_len > 0) {
|
||||
// Check for a mapping in "typebuf".
|
||||
map_result_T result = (map_result_T)handle_mapping(&keylen, &timedout, &mapdepth);
|
||||
map_result_T result = (map_result_T)handle_mapping(&keylen, &timedout, &mapdepth,
|
||||
advance);
|
||||
|
||||
if (result == map_result_retry) {
|
||||
// try mapping again
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "nvim/autocmd.h"
|
||||
#include "nvim/buffer.h"
|
||||
#include "nvim/eval/typval_defs.h"
|
||||
#include "nvim/eval/vars.h"
|
||||
#include "nvim/ex_docmd.h"
|
||||
#include "nvim/globals.h"
|
||||
#include "nvim/input.h"
|
||||
@@ -34,6 +35,7 @@
|
||||
#include "nvim/ops.h"
|
||||
#include "nvim/option_vars.h"
|
||||
#include "nvim/register.h"
|
||||
#include "nvim/state.h"
|
||||
#include "nvim/state_defs.h"
|
||||
#include "nvim/strings.h"
|
||||
#include "nvim/vim_defs.h"
|
||||
@@ -50,11 +52,19 @@ 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)?".
|
||||
@@ -63,13 +73,19 @@ static uint64_t atom_captures = 0;
|
||||
static bool atom_suppressed = false;
|
||||
/// Mapping edited the buffer, or its insert-session cascaded: cascades as one unit, incl. motions.
|
||||
static bool map_edit = false;
|
||||
/// Ticks per command frame (CmdFrame.id).
|
||||
static uint64_t frame_id = 0;
|
||||
|
||||
/// Accumulating composite atom: an executing mapping/macro. See `vatom` for Visual composite.
|
||||
static struct {
|
||||
CmdAtomVec atoms; ///< Subatoms of the mapping/macro.
|
||||
char *lhs; ///< Label: mapping LHS or macro "@x" (NULL: not collecting).
|
||||
bool queued; ///< A cascadable atom was queued (g_atoms) while collecting.
|
||||
bool lossy; ///< Capture lost part of the mapping (incomplete insert, payload with no
|
||||
///< capturing atom): `keys` cannot replay it, `lhs` can.
|
||||
bool macro; ///< Macro execution: captured as an "@x"-labeled atom.
|
||||
uint64_t frame; ///< CmdFrame already executing when a lookahead resolved this mapping
|
||||
///< ("f(" + mapped key in one batch). 0: none.
|
||||
CmdOrigin origin; ///< State at start.
|
||||
} composite;
|
||||
|
||||
@@ -96,20 +112,18 @@ static struct {
|
||||
|
||||
/// Per-command capture scratch.
|
||||
static struct {
|
||||
bool redo_pending; ///< The command prepped the change atom (prep_redo*()).
|
||||
uint64_t redo_frame; ///< CmdFrame that prepped (prep_redo*()); frame end captures it. 0: none.
|
||||
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. Two slices mark keys read by getchar(), which
|
||||
/// the redo body never sees: 'operatorfunc' input, and the mapping payload ("ds'" reads "'").
|
||||
/// Interactively typed keys of the executing command. Collected during a composite (its `lhs`
|
||||
/// suffix) and eval-read (the frame's payload slice, see atom_payload_start()).
|
||||
static struct {
|
||||
kvec_t(uint8_t) keys;
|
||||
bool opfunc_active; ///< Collecting 'operatorfunc' input.
|
||||
size_t opfunc_start, opfunc_end; ///< opfunc slice: keys[opfunc_start..opfunc_end)
|
||||
size_t map_start; ///< mapping slice: keys[map_start..kv_size(keys))
|
||||
size_t map_start; ///< This composite's slice: keys[map_start..kv_size(keys)), its lhs suffix.
|
||||
} typed;
|
||||
|
||||
static const char *const type_names[] = {
|
||||
@@ -332,7 +346,9 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
}
|
||||
// keys/lhs are RAW bytes (typeahead encoding).
|
||||
const char *keys = atom->keys != NULL ? atom->keys : "";
|
||||
PUT(d, "keys", CSTR_TO_OBJ(keys));
|
||||
if (!atom->remap) {
|
||||
PUT(d, "keys", CSTR_TO_OBJ(keys));
|
||||
}
|
||||
PUT(d, "lhs", CSTR_TO_OBJ(atom->lhs != NULL && *atom->lhs != NUL ? atom->lhs : keys));
|
||||
if (*force != NUL) {
|
||||
PUT(d, "motionforce", CSTR_TO_OBJ(force));
|
||||
@@ -345,9 +361,6 @@ 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));
|
||||
}
|
||||
@@ -356,7 +369,7 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
}
|
||||
|
||||
/// Schedules a CmdAtom event.
|
||||
static void atom_emit(const CmdAtom *atom, const char *pending)
|
||||
static void atom_emit(const CmdAtom *atom)
|
||||
{
|
||||
if (!has_event(EVENT_CMDATOM)) {
|
||||
return;
|
||||
@@ -369,19 +382,18 @@ static void atom_emit(const CmdAtom *atom, const char *pending)
|
||||
}
|
||||
PUT(data, "atoms", ARRAY_OBJ(atoms));
|
||||
}
|
||||
if (*pending != NUL) {
|
||||
PUT(data, "pending", CSTR_TO_OBJ(pending));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// Emits a CmdAtom event, or collects it as a subatom of a composite. If `cascade` is true, queues
|
||||
/// a copy for mcursor cascade.
|
||||
/// Emits a CmdAtom event, or collects it as a subatom of an open scope. If `cascade` is true,
|
||||
/// queues a copy for mcursor cascade. Takes ownership of the atom's allocated members.
|
||||
///
|
||||
/// Takes ownership of the atom's allocated members. Caller sets `atom.changed`.
|
||||
/// Both scopes (composite, Visual session) can be open. Routed by scope kind, not depth:
|
||||
/// a collecting Visual session takes the atom, its atom then lands in the composite (",v"+"d").
|
||||
/// Depth would invert "v@q", where the "@q" composite opened LAST yet collects nothing.
|
||||
void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
{
|
||||
assert(atom.keys != NULL);
|
||||
@@ -392,7 +404,6 @@ void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
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) {
|
||||
// Not for kVatomFed: redo-prep must not mark the enclosing span as captured.
|
||||
atom_captures++;
|
||||
@@ -408,15 +419,27 @@ void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
last->changed = atom.changed;
|
||||
}
|
||||
}
|
||||
if (cascade && composite.lhs != NULL) {
|
||||
composite.queued = true;
|
||||
// `composite.frame`: the command that was executing when a peek opened the composite is not part
|
||||
// of it.
|
||||
const bool collect = composite.lhs != NULL
|
||||
&& (cur_frame == NULL || cur_frame->id != composite.frame);
|
||||
if (cascade) {
|
||||
CmdAtom copy = atom;
|
||||
copy.keys = xstrdup(atom.keys);
|
||||
copy.text = NULL; // replay (mc_execute()) reads only type/keys/remap: not the text,
|
||||
copy.lhs = NULL; // nor the label,
|
||||
copy.atoms = (CmdAtomVec)KV_INITIAL_VALUE; // nor the decomposition
|
||||
kv_push(g_atoms, copy);
|
||||
if (collect) {
|
||||
composite.queued = true;
|
||||
}
|
||||
}
|
||||
if (composite.lhs != NULL) {
|
||||
if (collect) {
|
||||
kv_push(composite.atoms, atom);
|
||||
} else {
|
||||
if (atom.type != kAInsertSpan) {
|
||||
// Spans are cascade-internal; only emit the whole session (kAInsert).
|
||||
atom_emit(&atom, "");
|
||||
atom_emit(&atom);
|
||||
}
|
||||
atom_free(&atom);
|
||||
}
|
||||
@@ -504,6 +527,8 @@ static void atom_composite_start(const char *lhs, size_t len)
|
||||
xfree(composite.lhs);
|
||||
composite.lhs = xmemdupz(lhs, len);
|
||||
composite.queued = false;
|
||||
composite.lossy = false;
|
||||
composite.frame = 0;
|
||||
composite.origin = atom_origin();
|
||||
}
|
||||
|
||||
@@ -511,15 +536,14 @@ static void atom_composite_start(const char *lhs, size_t len)
|
||||
///
|
||||
/// :nnoremap gj i<C-J><Esc>k$
|
||||
/// "gj" => CmdAtom{ .lhs="gj", .keys="1i<NL><Esc>k$", kAMapping }
|
||||
static void atom_composite_end(const char *pending)
|
||||
static void atom_composite_end(void)
|
||||
{
|
||||
composite.macro = false; // "@x" capture ends with its composite.
|
||||
if (composite.lhs == NULL) {
|
||||
return;
|
||||
}
|
||||
// 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;
|
||||
// LHS-replay when the capture is lossy, or captured nothing (Ex/Lua edits).
|
||||
const bool remap = composite.lossy || kv_size(composite.atoms) == 0;
|
||||
char *lhs = atom_composite_lhs();
|
||||
XFREE_CLEAR(composite.lhs);
|
||||
CmdAtom atom;
|
||||
@@ -546,7 +570,7 @@ static void atom_composite_end(const char *pending)
|
||||
atom.atoms = composite.atoms; // Subatoms.
|
||||
composite.atoms = (CmdAtomVec)KV_INITIAL_VALUE; // Reset.
|
||||
}
|
||||
atom_emit(&atom, pending);
|
||||
atom_emit(&atom);
|
||||
atom_free(&atom);
|
||||
}
|
||||
|
||||
@@ -571,7 +595,9 @@ bool atom_is_user_cmd(void)
|
||||
/// typed "i", mapped "gj" => true; "." (stuffed redo), "@r" => false
|
||||
static bool atom_is_user_input(void)
|
||||
{
|
||||
return KeyTyped || (atom_is_user_cmd() && typebuf_maplen() > 0);
|
||||
// An open composite is user input even after its keys were consumed (":nnoremap ,i i").
|
||||
return KeyTyped
|
||||
|| (atom_is_user_cmd() && (typebuf_maplen() > 0 || atom_composite_active()));
|
||||
}
|
||||
|
||||
/// Suppresses atom pushes. For internal operators.
|
||||
@@ -635,11 +661,20 @@ unsigned atom_key_class(int cmd, int arg)
|
||||
return kKeyJump;
|
||||
case Ctrl_T:
|
||||
return kKeyJump | kKeyInsFlush;
|
||||
// Multiplexed: one nv_cmds entry => many commands. NV_MOTION cannot tag them; char 2 decides.
|
||||
case 'g':
|
||||
return (arg == ';' || arg == ',') ? kKeyJump : 0;
|
||||
if (arg == ';' || arg == ',') {
|
||||
return kKeyJump;
|
||||
}
|
||||
return strchr("gjk0^$_meEoM", arg) != NULL ? kKeyMotion : 0;
|
||||
case '[':
|
||||
case ']':
|
||||
return arg == 'C' ? kKeyJump : 0; // "]C"/"[C": jump to the next/previous cursor
|
||||
if (arg == 'C') {
|
||||
return kKeyJump; // "]C"/"[C": jump to the next/previous cursor
|
||||
}
|
||||
return strchr("[](){}mMcsz#*/", arg) != NULL ? kKeyMotion : 0;
|
||||
case 'z':
|
||||
return (arg == 'j' || arg == 'k') ? kKeyMotion : 0;
|
||||
case '*':
|
||||
case '#':
|
||||
case '\'':
|
||||
@@ -691,22 +726,53 @@ void atom_cmdline_set(int firstc, const char *line, size_t len)
|
||||
curcmd.cmdline = xmemdupz(line, len);
|
||||
}
|
||||
|
||||
/// Opens/closes the 'operatorfunc' slice of the typed-key stream.
|
||||
void atom_opfunc_slice(bool active)
|
||||
/// True while collecting typed keys: during a composite (for `lhs`), or a CmdFrame's payload slice.
|
||||
static bool atom_typed_collecting(void)
|
||||
{
|
||||
typed.opfunc_active = active;
|
||||
if (active) {
|
||||
typed.opfunc_start = kv_size(typed.keys);
|
||||
return atom_composite_active()
|
||||
|| (cur_frame != NULL && cur_frame->payload_start != SIZE_MAX);
|
||||
}
|
||||
|
||||
/// Opens the CmdFrame's payload slice: keys from getchar(), input() append to `CmdAtom.keys`.
|
||||
void atom_payload_start(void)
|
||||
{
|
||||
if (cur_frame != NULL && cur_frame->payload_start == SIZE_MAX && !mc_replaying()) {
|
||||
cur_frame->payload_start = cur_frame->payload_end = kv_size(typed.keys);
|
||||
}
|
||||
typed.opfunc_end = kv_size(typed.keys);
|
||||
}
|
||||
|
||||
/// Closes (or extends) the payload slice.
|
||||
void atom_payload_end(void)
|
||||
{
|
||||
if (cur_frame != NULL && cur_frame->payload_start != SIZE_MAX && !mc_replaying()) {
|
||||
cur_frame->payload_end = kv_size(typed.keys);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains the CmdFrame's payload slice to `CmdAtom.keys`.
|
||||
static void atom_payload_append(CmdAtom *atom, CmdFrame *frame)
|
||||
{
|
||||
size_t plen = frame->payload_start == SIZE_MAX ? 0 : frame->payload_end - frame->payload_start;
|
||||
if (plen == 0 || atom->keys == NULL) {
|
||||
return;
|
||||
}
|
||||
size_t klen = strlen(atom->keys);
|
||||
atom->keys = xrealloc(atom->keys, klen + plen + 1);
|
||||
memcpy(atom->keys + klen, typed.keys.items + frame->payload_start, plen);
|
||||
atom->keys[klen + plen] = NUL;
|
||||
frame->payload_start = SIZE_MAX;
|
||||
}
|
||||
|
||||
/// Collects a typed key (gotchars()) into the stream.
|
||||
void atom_typed_add(const uint8_t *chars, size_t len)
|
||||
{
|
||||
if (!typed.opfunc_active && !atom_composite_active()) {
|
||||
if (mc_replaying() || !atom_typed_collecting()) {
|
||||
return;
|
||||
}
|
||||
if (len == 3 && chars[0] == K_SPECIAL
|
||||
&& (atom_key_class(TERMCAP2KEY(chars[1], chars[2]), NUL) & kKeySynthetic)) {
|
||||
return; // Not user input: K_IGNORE from a mapping resolved during peek/K_EVENT/…
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
kv_push(typed.keys, chars[i]);
|
||||
}
|
||||
@@ -716,7 +782,7 @@ void atom_typed_add(const uint8_t *chars, size_t len)
|
||||
/// into typeahead and will be collected again (ungetchars()).
|
||||
void atom_typed_del(size_t len)
|
||||
{
|
||||
if (!typed.opfunc_active && !atom_composite_active()) {
|
||||
if (mc_replaying() || !atom_typed_collecting()) {
|
||||
return;
|
||||
}
|
||||
kv_size(typed.keys) -= MIN(len, kv_size(typed.keys));
|
||||
@@ -729,16 +795,18 @@ static void atom_redo_reset(void)
|
||||
if (!atom_is_user_cmd()) {
|
||||
return;
|
||||
}
|
||||
curcmd.redo_pending = false;
|
||||
curcmd.redo_frame = 0;
|
||||
curcmd.ins_cascaded = false;
|
||||
XFREE_CLEAR(curcmd.cmdline);
|
||||
// The opfunc slice resets per command; the stream itself is truncated only once the
|
||||
// mapping slice ends too (with its composite).
|
||||
// The stream is truncated only once the mapping slice ends too (with its composite).
|
||||
if (!atom_composite_active()) {
|
||||
kv_size(typed.keys) = 0;
|
||||
typed.map_start = 0;
|
||||
// Truncation voids the payload slice.
|
||||
for (CmdFrame *frame = cur_frame; frame != NULL; frame = frame->parent) {
|
||||
frame->payload_start = SIZE_MAX;
|
||||
}
|
||||
}
|
||||
typed.opfunc_start = typed.opfunc_end = kv_size(typed.keys);
|
||||
}
|
||||
|
||||
/// Marks the running command as already applying to every cursor (see `curcmd.op_global`).
|
||||
@@ -749,15 +817,15 @@ void atom_op_global_set(void)
|
||||
}
|
||||
|
||||
/// Claims the prepped redo as the command's atom (prep_redo()). Only toplevel user commands (a
|
||||
/// nested redo-prep is not an atom). Declines Ex/Lua operators.
|
||||
/// nested redo-prep is not an atom). Declines Lua operators.
|
||||
void atom_redo_set(CmdSpec spec)
|
||||
{
|
||||
if (spec.cmd == ':' || spec.cmd == K_COMMAND || spec.cmd == K_LUA) {
|
||||
if (spec.cmd == K_LUA) {
|
||||
atom_redo_reset();
|
||||
return;
|
||||
}
|
||||
if (atom_is_user_cmd()) {
|
||||
curcmd.redo_pending = true;
|
||||
curcmd.redo_frame = cur_frame != NULL ? cur_frame->id : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +855,9 @@ void atom_stuff_start(const cmdarg_T *cap)
|
||||
}
|
||||
|
||||
/// Starts accumulating a composite for a mapping resolved from typed keys (vgetorpeek()).
|
||||
void atom_map_start(const char *lhs, size_t len)
|
||||
///
|
||||
/// @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()
|
||||
|| reg_executing != 0 || ex_normal_busy != 0 || !(State & MODE_NORMAL)
|
||||
@@ -798,9 +868,22 @@ void atom_map_start(const char *lhs, size_t len)
|
||||
// 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_end();
|
||||
}
|
||||
const char *op = get_vim_var_str(VV_OP); // v:operator
|
||||
if (get_real_state() == MODE_OP_PENDING && *op != NUL) {
|
||||
// Op-pending mapping (:omap) continues the operator (vim-sneak "dz(b").
|
||||
assert(lhs[len] == NUL);
|
||||
char *full = concat_str(op, lhs);
|
||||
atom_composite_start(full, strlen(full));
|
||||
xfree(full);
|
||||
typed.map_start = kv_size(typed.keys);
|
||||
return;
|
||||
}
|
||||
atom_composite_start(lhs, len);
|
||||
if (peeked && cur_frame != NULL) {
|
||||
composite.frame = cur_frame->id;
|
||||
}
|
||||
typed.map_start = kv_size(typed.keys);
|
||||
}
|
||||
|
||||
@@ -809,6 +892,8 @@ static void atom_visual_reset(void)
|
||||
{
|
||||
vatom.state = kVatomNone;
|
||||
atoms_free(&vatom.atoms);
|
||||
vatom.origin = (CmdOrigin){ 0 };
|
||||
mc_vsel_clear();
|
||||
}
|
||||
|
||||
/// Visual atom is pending. A void session still accumulates, for the `lhs` label.
|
||||
@@ -1076,6 +1161,10 @@ void atom_ins_end(const InsSession *session, bool busy)
|
||||
bool visual = session->vis != kVInsNone;
|
||||
if (!session->typed || busy || restart_edit != 0 || !atom_buf_has_consumers()
|
||||
|| (visual && session->vis != kVInsKeys)) {
|
||||
if (session->typed && (busy || restart_edit != 0) && atom_composite_active()) {
|
||||
// Incomplete session (i_CTRL-O): its resolution is never captured.
|
||||
composite.lossy = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
atom_ins_push(session, true);
|
||||
@@ -1105,12 +1194,15 @@ void atom_cmd_start(CmdFrame *old)
|
||||
old->visual = Visual;
|
||||
old->keytyped = KeyTyped;
|
||||
old->captures = atom_captures;
|
||||
old->id = ++frame_id;
|
||||
// Sampled: "q=" toggled DURING a command must not apply to it retroactively.
|
||||
old->follow = false;
|
||||
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;
|
||||
old->staged = (CmdAtom){ 0 };
|
||||
old->payload_start = SIZE_MAX;
|
||||
old->payload_end = 0;
|
||||
old->parent = cur_frame;
|
||||
cur_frame = old;
|
||||
curcmd.op_global = false;
|
||||
@@ -1122,7 +1214,7 @@ void atom_cmd_start(CmdFrame *old)
|
||||
///
|
||||
/// Skipped for a command that stuffed keys ("x" stuffs "dl": its resolution is the atom), or that
|
||||
/// already captured its own atom (do_pending_operator(), insert spans).
|
||||
static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
static void atom_capture_cmd(cmdarg_T *ca, CmdFrame *old)
|
||||
{
|
||||
if (mc_replaying() || atom_suppressed) {
|
||||
return;
|
||||
@@ -1170,7 +1262,8 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
if (!old->visual.active) {
|
||||
atom_visual_reset();
|
||||
// Decided once, at session start.
|
||||
vatom.state = (old->keytyped || atom_composite_active()) ? kVatomTyped : kVatomFed;
|
||||
vatom.state = (old->keytyped || (atom_composite_active() && atom_is_user_cmd()))
|
||||
? kVatomTyped : kVatomFed;
|
||||
vatom.origin = old->origin;
|
||||
}
|
||||
// Decided by the session (not atom_capturable()), so fed selections (":normal! vjd") still
|
||||
@@ -1221,31 +1314,21 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
&& !scroll_cmd)
|
||||
|| special_motion;
|
||||
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
|
||||
&& strchr("/?:!Qq", ca->cmdchar) == NULL)
|
||||
|| special_motion);
|
||||
// Note: an operator's motion belongs to the operator (`finish_op`).
|
||||
bool motion = (nv_is_motion(ca->cmdchar) || special_motion) && !changed
|
||||
&& !finish_op && !jump_cmd;
|
||||
// Mapping-internal motions are part of its recipe: queue them, the clock edge decides.
|
||||
bool follow = (mc_following() || mapped) && motion;
|
||||
|
||||
//
|
||||
// Route: decide the atom type and push it.
|
||||
//
|
||||
if (curcmd.redo_pending && !mouse_cmd) {
|
||||
if (curcmd.redo_frame == old->id && !mouse_cmd) {
|
||||
// Not for mouse commands (middle-click paste): pasting at every cursor would use
|
||||
// viewport-dependent positions.
|
||||
CmdAtom atom = atom_from_redo(kAOperator);
|
||||
size_t plen = typed.opfunc_end - typed.opfunc_start;
|
||||
if (atom.keys != NULL && plen > 0) {
|
||||
// The opfunc's payload is absent from the captured redo: append it.
|
||||
size_t klen = strlen(atom.keys);
|
||||
atom.keys = xrealloc(atom.keys, klen + plen + 1);
|
||||
memcpy(atom.keys + klen, typed.keys.items + typed.opfunc_start, plen);
|
||||
atom.keys[klen + plen] = NUL;
|
||||
}
|
||||
// The payload ('operatorfunc' getchar()) is not in the captured redo, append it.
|
||||
atom_payload_append(&atom, old);
|
||||
// Cascade only on an OBSERVABLE effect: an edit or register write. A redoable operator that
|
||||
// did neither is a no-op (vim-surround "ysa[" whose surround char was <Esc>).
|
||||
bool effect = changed || reg_max_ts(true) > old->reg_ts;
|
||||
@@ -1261,10 +1344,11 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf);
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, atom);
|
||||
} else if (!vis && curcmd.cmdline != NULL
|
||||
&& (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) {
|
||||
} 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(kAExcmd, ca, curcmd.cmdline);
|
||||
// Payload read during the cmdline execution (`ds)` getchar() => ")").
|
||||
atom_payload_append(&atom, old);
|
||||
atom.origin = old->origin;
|
||||
atom_push(false, atom);
|
||||
} else if (replayable && (!vis || (keycls & kKeyPayload) == 0)) {
|
||||
@@ -1297,24 +1381,34 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
// Not replayable: edited buffer during selection, so the keys do not describe the change.
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
if (Visual.active && user && toplevel) {
|
||||
if (Visual.active && user && old->parent == NULL) {
|
||||
mc_vsel_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes a cmd at normal_execute() exit: captures its atom, pushes its staged one, ends the
|
||||
/// composite. Pops the frame.
|
||||
void atom_cmd_end(cmdarg_T *ca, CmdFrame *old, bool toplevel)
|
||||
void atom_cmd_end(cmdarg_T *ca, CmdFrame *old)
|
||||
{
|
||||
atom_capture_cmd(ca, old, toplevel);
|
||||
atom_capture_cmd(ca, old);
|
||||
atom_stage_flush(old);
|
||||
if (atom_composite_active() && old->payload_start != SIZE_MAX
|
||||
&& old->payload_end > old->payload_start) {
|
||||
// An eval-read payload with no capturing atom (Lua mapping's getchar()).
|
||||
composite.lossy = true;
|
||||
}
|
||||
|
||||
// The clock edge. Only at toplevel: cascading from a nested normal_execute() would recurse.
|
||||
// Deferred while a mapping executes (its keys are still in typebuf), so its commands collapse as
|
||||
// one unit; likewise for a macro's LAST command, which may stuff a translation ("x" => "dl").
|
||||
if (toplevel && typebuf_typed() && stuff_empty()) {
|
||||
if (old->parent == NULL && !mc_replaying() && typebuf_typed() && stuff_empty()) {
|
||||
mc_clock_edge(map_edit);
|
||||
map_edit = false;
|
||||
atom_composite_end(ca->oap->op_type != OP_NOP ? "operator" : Visual.active ? "visual" : "");
|
||||
// Mapping contains its continuation. While op-pending, selection-active, or insert-will-resume
|
||||
// (i_CTRL-O), composite keeps collecting: ",Dw" (":nnoremap ,D d") is one atom, `keys="dw"`.
|
||||
if (ca->oap->op_type == OP_NOP && !Visual.active && restart_edit == 0) {
|
||||
atom_composite_end();
|
||||
}
|
||||
}
|
||||
cur_frame = old->parent;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,13 @@ struct CmdFrame {
|
||||
VisualState visual; ///< Visual-mode state (active/start/mode are diffed).
|
||||
bool keytyped; ///< KeyTyped
|
||||
uint64_t captures; ///< Capture counter.
|
||||
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).
|
||||
Timestamp reg_ts; ///< Max register timestamp (to detect a per-cursor register write).
|
||||
CmdAtom staged; ///< Atom staged in this frame. `keys == NULL`: none.
|
||||
CmdAtom staged; ///< Atom staged in this frame (`keys=NULL`: none).
|
||||
size_t payload_start; ///< Payload slice of key stream (`SIZE_MAX`: none):
|
||||
size_t payload_end; ///< `typed.keys[payload_start..payload_end)`
|
||||
CmdFrame *parent; ///< Enclosing frame (nested normal_execute()); NULL at toplevel.
|
||||
};
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ struct CmdAtom {
|
||||
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.
|
||||
bool remap; ///< If true, `keys` cannot replay: payload mapping (vim-surround "ds'") edits
|
||||
///< invisibly (:norm/Ex). Must replay `lhs` instead.
|
||||
};
|
||||
|
||||
/// Key classes (atom_key_class()).
|
||||
|
||||
@@ -164,6 +164,7 @@ typedef void (*nv_func_T)(cmdarg_T *cap);
|
||||
#define NV_NCW 0x200 // not allowed in command-line window
|
||||
#define NV_NCH_ARG 0x400 // second char is a typed operand (mark/register name),
|
||||
// not part of the command name (see NV_LANG for f/t/r)
|
||||
#define NV_MOTION 0x800 // Motion command.
|
||||
|
||||
// Generally speaking, every Normal mode command should either clear any
|
||||
// pending operator (with *clearop*()), or set the motion type variable
|
||||
@@ -190,15 +191,15 @@ static const struct nv_cmd {
|
||||
{ Ctrl_E, nv_scroll_line, 0, true },
|
||||
{ Ctrl_F, nv_page, NV_STS, FORWARD },
|
||||
{ Ctrl_G, nv_ctrlg, 0, 0 },
|
||||
{ Ctrl_H, nv_ctrlh, 0, 0 },
|
||||
{ Ctrl_H, nv_ctrlh, NV_MOTION, 0 },
|
||||
{ Ctrl_I, nv_pcmark, 0, 0 },
|
||||
{ NL, nv_down, 0, false },
|
||||
{ NL, nv_down, NV_MOTION, false },
|
||||
{ Ctrl_K, nv_error, 0, 0 },
|
||||
{ Ctrl_L, nv_clear, 0, 0 },
|
||||
{ CAR, nv_down, 0, true },
|
||||
{ Ctrl_N, nv_down, NV_STS, false },
|
||||
{ CAR, nv_down, NV_MOTION, true },
|
||||
{ Ctrl_N, nv_down, NV_STS|NV_MOTION, false },
|
||||
{ Ctrl_O, nv_ctrlo, 0, 0 },
|
||||
{ Ctrl_P, nv_up, NV_STS, false },
|
||||
{ Ctrl_P, nv_up, NV_STS|NV_MOTION, false },
|
||||
{ Ctrl_Q, nv_visual, 0, false },
|
||||
{ Ctrl_R, nv_redo_or_register, 0, 0 },
|
||||
{ Ctrl_S, nv_ignore, 0, 0 },
|
||||
@@ -216,23 +217,23 @@ static const struct nv_cmd {
|
||||
{ Ctrl_RSB, nv_ident, NV_NCW, 0 },
|
||||
{ Ctrl_HAT, nv_hat, NV_NCW, 0 },
|
||||
{ Ctrl__, nv_error, 0, 0 },
|
||||
{ ' ', nv_right, 0, 0 },
|
||||
{ ' ', nv_right, NV_MOTION, 0 },
|
||||
{ '!', nv_operator, 0, 0 },
|
||||
{ '"', nv_regname, NV_NCH_NOP|NV_NCH_ARG|NV_KEEPREG, 0 },
|
||||
{ '#', nv_ident, 0, 0 },
|
||||
{ '$', nv_dollar, 0, 0 },
|
||||
{ '%', nv_percent, 0, 0 },
|
||||
{ '$', nv_dollar, NV_MOTION, 0 },
|
||||
{ '%', nv_percent, NV_MOTION, 0 },
|
||||
{ '&', nv_optrans, 0, 0 },
|
||||
{ '\'', nv_gomark, NV_NCH_ALW|NV_NCH_ARG, true },
|
||||
{ '(', nv_brace, 0, BACKWARD },
|
||||
{ ')', nv_brace, 0, FORWARD },
|
||||
{ '(', nv_brace, NV_MOTION, BACKWARD },
|
||||
{ ')', nv_brace, NV_MOTION, FORWARD },
|
||||
{ '*', nv_ident, 0, 0 },
|
||||
{ '+', nv_down, 0, true },
|
||||
{ ',', nv_csearch, 0, true },
|
||||
{ '-', nv_up, 0, true },
|
||||
{ '+', nv_down, NV_MOTION, true },
|
||||
{ ',', nv_csearch, NV_MOTION, true },
|
||||
{ '-', nv_up, NV_MOTION, true },
|
||||
{ '.', nv_dot, NV_KEEPREG, 0 },
|
||||
{ '/', nv_search, 0, false },
|
||||
{ '0', nv_beginline, 0, 0 },
|
||||
{ '0', nv_beginline, NV_MOTION, 0 },
|
||||
{ '1', nv_ignore, 0, 0 },
|
||||
{ '2', nv_ignore, 0, 0 },
|
||||
{ '3', nv_ignore, 0, 0 },
|
||||
@@ -243,71 +244,71 @@ static const struct nv_cmd {
|
||||
{ '8', nv_ignore, 0, 0 },
|
||||
{ '9', nv_ignore, 0, 0 },
|
||||
{ ':', nv_colon, 0, 0 },
|
||||
{ ';', nv_csearch, 0, false },
|
||||
{ ';', nv_csearch, NV_MOTION, false },
|
||||
{ '<', nv_operator, NV_RL, 0 },
|
||||
{ '=', nv_operator, 0, 0 },
|
||||
{ '>', nv_operator, NV_RL, 0 },
|
||||
{ '?', nv_search, 0, false },
|
||||
{ '@', nv_at, NV_NCH_NOP|NV_NCH_ARG, false },
|
||||
{ 'A', nv_edit, 0, 0 },
|
||||
{ 'B', nv_bck_word, 0, 1 },
|
||||
{ 'B', nv_bck_word, NV_MOTION, 1 },
|
||||
{ 'C', nv_abbrev, NV_KEEPREG, 0 },
|
||||
{ 'D', nv_abbrev, NV_KEEPREG, 0 },
|
||||
{ 'E', nv_wordcmd, 0, true },
|
||||
{ 'F', nv_csearch, NV_NCH_ALW|NV_LANG, BACKWARD },
|
||||
{ 'G', nv_goto, 0, true },
|
||||
{ 'H', nv_scroll, 0, 0 },
|
||||
{ 'E', nv_wordcmd, NV_MOTION, true },
|
||||
{ 'F', nv_csearch, NV_NCH_ALW|NV_LANG|NV_MOTION, BACKWARD },
|
||||
{ 'G', nv_goto, NV_MOTION, true },
|
||||
{ 'H', nv_scroll, NV_MOTION, 0 },
|
||||
{ 'I', nv_edit, 0, 0 },
|
||||
{ 'J', nv_join, 0, 0 },
|
||||
{ 'K', nv_ident, 0, 0 },
|
||||
{ 'L', nv_scroll, 0, 0 },
|
||||
{ 'M', nv_scroll, 0, 0 },
|
||||
{ 'N', nv_next, 0, SEARCH_REV },
|
||||
{ 'L', nv_scroll, NV_MOTION, 0 },
|
||||
{ 'M', nv_scroll, NV_MOTION, 0 },
|
||||
{ 'N', nv_next, NV_MOTION, SEARCH_REV },
|
||||
{ 'O', nv_open, 0, 0 },
|
||||
{ 'P', nv_put, 0, 0 },
|
||||
{ 'Q', nv_regreplay, 0, 0 },
|
||||
{ 'R', nv_Replace, 0, false },
|
||||
{ 'S', nv_subst, NV_KEEPREG, 0 },
|
||||
{ 'T', nv_csearch, NV_NCH_ALW|NV_LANG, BACKWARD },
|
||||
{ 'T', nv_csearch, NV_NCH_ALW|NV_LANG|NV_MOTION, BACKWARD },
|
||||
{ 'U', nv_Undo, 0, 0 },
|
||||
{ 'W', nv_wordcmd, 0, true },
|
||||
{ 'W', nv_wordcmd, NV_MOTION, true },
|
||||
{ 'X', nv_abbrev, NV_KEEPREG, 0 },
|
||||
{ 'Y', nv_abbrev, NV_KEEPREG, 0 },
|
||||
{ 'Z', nv_Zet, NV_NCH_NOP|NV_NCW, 0 },
|
||||
{ '[', nv_brackets, NV_NCH_ALW, BACKWARD },
|
||||
{ '\\', nv_error, 0, 0 },
|
||||
{ ']', nv_brackets, NV_NCH_ALW, FORWARD },
|
||||
{ '^', nv_beginline, 0, BL_WHITE | BL_FIX },
|
||||
{ '_', nv_lineop, 0, 0 },
|
||||
{ '^', nv_beginline, NV_MOTION, BL_WHITE | BL_FIX },
|
||||
{ '_', nv_lineop, NV_MOTION, 0 },
|
||||
{ '`', nv_gomark, NV_NCH_ALW|NV_NCH_ARG, false },
|
||||
{ 'a', nv_edit, NV_NCH, 0 },
|
||||
{ 'b', nv_bck_word, 0, 0 },
|
||||
{ 'b', nv_bck_word, NV_MOTION, 0 },
|
||||
{ 'c', nv_operator, 0, 0 },
|
||||
{ 'd', nv_operator, 0, 0 },
|
||||
{ 'e', nv_wordcmd, 0, false },
|
||||
{ 'f', nv_csearch, NV_NCH_ALW|NV_LANG, FORWARD },
|
||||
{ 'e', nv_wordcmd, NV_MOTION, false },
|
||||
{ 'f', nv_csearch, NV_NCH_ALW|NV_LANG|NV_MOTION, FORWARD },
|
||||
{ 'g', nv_g_cmd, NV_NCH_ALW, false },
|
||||
{ 'h', nv_left, NV_RL, 0 },
|
||||
{ 'h', nv_left, NV_RL|NV_MOTION, 0 },
|
||||
{ 'i', nv_edit, NV_NCH, 0 },
|
||||
{ 'j', nv_down, 0, false },
|
||||
{ 'k', nv_up, 0, false },
|
||||
{ 'l', nv_right, NV_RL, 0 },
|
||||
{ 'j', nv_down, NV_MOTION, false },
|
||||
{ 'k', nv_up, NV_MOTION, false },
|
||||
{ 'l', nv_right, NV_RL|NV_MOTION, 0 },
|
||||
{ 'm', nv_mark, NV_NCH_NOP|NV_NCH_ARG, 0 },
|
||||
{ 'n', nv_next, 0, 0 },
|
||||
{ 'n', nv_next, NV_MOTION, 0 },
|
||||
{ 'o', nv_open, 0, 0 },
|
||||
{ 'p', nv_put, 0, 0 },
|
||||
{ 'q', nv_q, NV_NCH|NV_NCH_ARG, 0 },
|
||||
{ 'r', nv_replace, NV_NCH_NOP|NV_LANG, 0 },
|
||||
{ 's', nv_subst, NV_KEEPREG, 0 },
|
||||
{ 't', nv_csearch, NV_NCH_ALW|NV_LANG, FORWARD },
|
||||
{ 't', nv_csearch, NV_NCH_ALW|NV_LANG|NV_MOTION, FORWARD },
|
||||
{ 'u', nv_undo, 0, 0 },
|
||||
{ 'w', nv_wordcmd, 0, false },
|
||||
{ 'w', nv_wordcmd, NV_MOTION, false },
|
||||
{ 'x', nv_abbrev, NV_KEEPREG, 0 },
|
||||
{ 'y', nv_operator, 0, 0 },
|
||||
{ 'z', nv_zet, NV_NCH_ALW, 0 },
|
||||
{ '{', nv_findpar, 0, BACKWARD },
|
||||
{ '|', nv_pipe, 0, 0 },
|
||||
{ '}', nv_findpar, 0, FORWARD },
|
||||
{ '{', nv_findpar, NV_MOTION, BACKWARD },
|
||||
{ '|', nv_pipe, NV_MOTION, 0 },
|
||||
{ '}', nv_findpar, NV_MOTION, FORWARD },
|
||||
{ '~', nv_tilde, 0, 0 },
|
||||
|
||||
// pound sign
|
||||
@@ -338,29 +339,29 @@ static const struct nv_cmd {
|
||||
{ K_NOP, nv_nop, 0, 0 },
|
||||
{ K_INS, nv_edit, 0, 0 },
|
||||
{ K_KINS, nv_edit, 0, 0 },
|
||||
{ K_BS, nv_ctrlh, 0, 0 },
|
||||
{ K_UP, nv_up, NV_SSS|NV_STS, false },
|
||||
{ K_BS, nv_ctrlh, NV_MOTION, 0 },
|
||||
{ K_UP, nv_up, NV_SSS|NV_STS|NV_MOTION, false },
|
||||
{ K_S_UP, nv_page, NV_SS, BACKWARD },
|
||||
{ K_DOWN, nv_down, NV_SSS|NV_STS, false },
|
||||
{ K_DOWN, nv_down, NV_SSS|NV_STS|NV_MOTION, false },
|
||||
{ K_S_DOWN, nv_page, NV_SS, FORWARD },
|
||||
{ K_LEFT, nv_left, NV_SSS|NV_STS|NV_RL, 0 },
|
||||
{ K_S_LEFT, nv_bck_word, NV_SS|NV_RL, 0 },
|
||||
{ K_C_LEFT, nv_bck_word, NV_SSS|NV_RL|NV_STS, 1 },
|
||||
{ K_RIGHT, nv_right, NV_SSS|NV_STS|NV_RL, 0 },
|
||||
{ K_S_RIGHT, nv_wordcmd, NV_SS|NV_RL, false },
|
||||
{ K_C_RIGHT, nv_wordcmd, NV_SSS|NV_RL|NV_STS, true },
|
||||
{ K_LEFT, nv_left, NV_SSS|NV_STS|NV_RL|NV_MOTION, 0 },
|
||||
{ K_S_LEFT, nv_bck_word, NV_SS|NV_RL|NV_MOTION, 0 },
|
||||
{ K_C_LEFT, nv_bck_word, NV_SSS|NV_RL|NV_STS|NV_MOTION, 1 },
|
||||
{ K_RIGHT, nv_right, NV_SSS|NV_STS|NV_RL|NV_MOTION, 0 },
|
||||
{ K_S_RIGHT, nv_wordcmd, NV_SS|NV_RL|NV_MOTION, false },
|
||||
{ K_C_RIGHT, nv_wordcmd, NV_SSS|NV_RL|NV_STS|NV_MOTION, true },
|
||||
{ K_PAGEUP, nv_page, NV_SSS|NV_STS, BACKWARD },
|
||||
{ K_KPAGEUP, nv_page, NV_SSS|NV_STS, BACKWARD },
|
||||
{ K_PAGEDOWN, nv_page, NV_SSS|NV_STS, FORWARD },
|
||||
{ K_KPAGEDOWN, nv_page, NV_SSS|NV_STS, FORWARD },
|
||||
{ K_END, nv_end, NV_SSS|NV_STS, false },
|
||||
{ K_KEND, nv_end, NV_SSS|NV_STS, false },
|
||||
{ K_S_END, nv_end, NV_SS, false },
|
||||
{ K_C_END, nv_end, NV_SSS|NV_STS, true },
|
||||
{ K_HOME, nv_home, NV_SSS|NV_STS, 0 },
|
||||
{ K_KHOME, nv_home, NV_SSS|NV_STS, 0 },
|
||||
{ K_S_HOME, nv_home, NV_SS, 0 },
|
||||
{ K_C_HOME, nv_goto, NV_SSS|NV_STS, false },
|
||||
{ K_END, nv_end, NV_SSS|NV_STS|NV_MOTION, false },
|
||||
{ K_KEND, nv_end, NV_SSS|NV_STS|NV_MOTION, false },
|
||||
{ K_S_END, nv_end, NV_SS|NV_MOTION, false },
|
||||
{ K_C_END, nv_end, NV_SSS|NV_STS|NV_MOTION, true },
|
||||
{ K_HOME, nv_home, NV_SSS|NV_STS|NV_MOTION, 0 },
|
||||
{ K_KHOME, nv_home, NV_SSS|NV_STS|NV_MOTION, 0 },
|
||||
{ K_S_HOME, nv_home, NV_SS|NV_MOTION, 0 },
|
||||
{ K_C_HOME, nv_goto, NV_SSS|NV_STS|NV_MOTION, false },
|
||||
{ K_DEL, nv_abbrev, 0, 0 },
|
||||
{ K_KDEL, nv_abbrev, 0, 0 },
|
||||
{ K_UNDO, nv_kundo, 0, 0 },
|
||||
@@ -431,6 +432,14 @@ bool nv_nchar_is_arg(int cmdchar)
|
||||
return idx >= 0 && (nv_cmds[idx].cmd_flags & (NV_LANG|NV_NCH_ARG)) != 0;
|
||||
}
|
||||
|
||||
/// True if `cmdchar` is a motion command. Multiplexed handlers (g, [, ], z) are classified by
|
||||
/// atom_key_class().
|
||||
bool nv_is_motion(int cmdchar)
|
||||
{
|
||||
int idx = find_command(cmdchar);
|
||||
return idx >= 0 && (nv_cmds[idx].cmd_flags & NV_MOTION) != 0;
|
||||
}
|
||||
|
||||
/// Search for a command in the commands table.
|
||||
///
|
||||
/// @return -1 for invalid command.
|
||||
@@ -1085,15 +1094,6 @@ normal_end:
|
||||
|
||||
static int normal_execute(VimState *state, int key)
|
||||
{
|
||||
// Like most things in Vim, `toplevel` is a lie: exec_normal() happily sets it to true (inherited
|
||||
// from Vim). So we track `depth` instead.
|
||||
// - depth=0: No command executing (idle, between commands, sitting in vgetc()).
|
||||
// - depth=1: Outermost command-frame.
|
||||
// - depth=2: First nested frame (e.g. the "dd" inside ":normal! dd").
|
||||
// - depth=n: And so on...
|
||||
static int depth = 0;
|
||||
depth++;
|
||||
|
||||
CmdFrame frame;
|
||||
atom_cmd_start(&frame);
|
||||
|
||||
@@ -1269,9 +1269,8 @@ static int normal_execute(VimState *state, int key)
|
||||
|
||||
finish:
|
||||
normal_finish_command(s);
|
||||
atom_cmd_end(&s->ca, &frame, depth == 1);
|
||||
atom_cmd_end(&s->ca, &frame);
|
||||
xfree(s->ca.searchbuf);
|
||||
depth--;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1563,14 +1562,14 @@ void restore_visual_mode(void)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for a balloon-eval special item to include when searching for an
|
||||
/// identifier. When "dir" is BACKWARD "ptr[-1]" must be valid!
|
||||
/// Check for a <cexpr> special item to include when searching for an identifier: "ptr->arg",
|
||||
/// "list[idx]", "s.var". When `dir` is BACKWARD `ptr[-1]` must be valid!
|
||||
///
|
||||
/// @return true if the character at "*ptr" should be included.
|
||||
/// @return true if the character at `*ptr` should be included.
|
||||
///
|
||||
/// @param dir the direction of searching, is either FORWARD or BACKWARD
|
||||
/// @param *colp is in/decremented if "ptr[-dir]" should also be included.
|
||||
/// @param bnp points to a counter for square brackets.
|
||||
/// @param dir Direction of searching, either FORWARD or BACKWARD.
|
||||
/// @param *colp Is in/decremented if "ptr[-dir]" should also be included.
|
||||
/// @param bnp Points to a counter for square brackets.
|
||||
static bool find_is_eval_item(const char *const ptr, int *const colp, int *const bnp, const int dir)
|
||||
{
|
||||
// Accept everything inside [].
|
||||
@@ -1615,7 +1614,7 @@ static bool find_is_eval_item(const char *const ptr, int *const colp, int *const
|
||||
/// - FIND_IDENT: find an identifier (keyword)
|
||||
/// - FIND_STRING: find any non-white text
|
||||
/// - FIND_IDENT + FIND_STRING: find any non-white text, identifier preferred.
|
||||
/// - FIND_EVAL: find text useful for C program debugging
|
||||
/// - FIND_EVAL: also include "->", "[]" and "." (<cexpr>)
|
||||
/// @param offset If not NULL, gets cursor position relative to start of `text`.
|
||||
/// @return Text length, or zero if no text is found.
|
||||
size_t find_ident_under_cursor(char **text, int find_type, int *offset)
|
||||
|
||||
@@ -3687,10 +3687,8 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
|
||||
case OP_FUNCTION:
|
||||
// Restore linebreak, so that when the user edits it looks as before.
|
||||
restore_lbr(lbr_saved);
|
||||
atom_opfunc_slice(true); // Keys the opfunc reads interactively (getchar) are its payload.
|
||||
// call 'operatorfunc'
|
||||
op_function(oap);
|
||||
atom_opfunc_slice(false);
|
||||
break;
|
||||
|
||||
case OP_INSERT:
|
||||
|
||||
@@ -78,6 +78,16 @@ m.minisurround_vim = [[
|
||||
nnoremap <expr> ys MiniSurroundSetup()
|
||||
]]
|
||||
|
||||
--- Minimal vim-sneak: :omap whose ":call" reads a 2-char getchar() and moves the cursor.
|
||||
m.minisneak_vim = [[
|
||||
function! MiniSneak() abort
|
||||
let c1 = nr2char(getchar())
|
||||
let c2 = nr2char(getchar())
|
||||
call search('\V' . c1 . c2, 'W')
|
||||
endfunction
|
||||
onoremap <silent> z :<C-U>call MiniSneak()<CR>
|
||||
]]
|
||||
|
||||
--- Minimal vim-surround "ds": a ":call" mapping whose edit runs through :normal inside a
|
||||
--- function, with a getchar() payload naming the surround to delete.
|
||||
m.delsurround_vim = [[
|
||||
|
||||
@@ -52,6 +52,38 @@ describe('dot-repeat', function()
|
||||
feed('j0.')
|
||||
eq({ 'acbzacbonexy', 'czacbtwo' }, get_lines())
|
||||
end)
|
||||
|
||||
it('of a visual op does not churn showcmd', function()
|
||||
local screen = Screen.new(40, 8, { ext_messages = true })
|
||||
command('set showcmd')
|
||||
fn.setline(1, { 'foo bar', 'longword two' })
|
||||
feed('gg0viWgU')
|
||||
feed('2gg0')
|
||||
screen:expect({ any = '%^longword' })
|
||||
local updates = {}
|
||||
function screen:_handle_msg_showcmd(msg)
|
||||
local text = table.concat(vim.tbl_map(function(chunk)
|
||||
return chunk[2]
|
||||
end, msg))
|
||||
if text ~= '' then
|
||||
updates[#updates + 1] = text
|
||||
end
|
||||
self.showcmd = msg
|
||||
end
|
||||
feed('.')
|
||||
retry(nil, nil, function()
|
||||
eq({ 'FOO bar', 'LONGWORD two' }, get_lines())
|
||||
end)
|
||||
screen:sleep(50)
|
||||
-- Stuffed (replayed) keys must not display in 'showcmd': ext_messages UIs redraw per showcmd
|
||||
-- change, which would paint the replay's transient states (its in-progress Visual selection).
|
||||
eq(
|
||||
{},
|
||||
vim.tbl_filter(function(s)
|
||||
return s:find('[UW]') ~= nil
|
||||
end, updates)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('CmdAtom', function()
|
||||
@@ -107,7 +139,7 @@ describe('CmdAtom', function()
|
||||
feed(']q')
|
||||
eq(3, fn.line('.')) -- the mapping did run (:cnext)
|
||||
eq(
|
||||
{ type = 'mapping', lhs = ']q', keys = '', changed = false },
|
||||
{ type = 'mapping', lhs = ']q', changed = false },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys', 'changed')
|
||||
)
|
||||
-- An empty-keys mapping that DOES edit still reports it: `changed` is
|
||||
@@ -118,7 +150,7 @@ describe('CmdAtom', function()
|
||||
end)
|
||||
]])
|
||||
feed(',e')
|
||||
eq({ keys = '', lhs = ',e', changed = true }, pick(atom_last(), 'keys', 'lhs', 'changed'))
|
||||
eq({ lhs = ',e', changed = true }, pick(atom_last(), 'keys', 'lhs', '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>')
|
||||
@@ -138,6 +170,25 @@ describe('CmdAtom', function()
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(cmdev.keys))
|
||||
eq('N3', fn.getline(1))
|
||||
|
||||
-- <expr> mapping that returns a "<Cmd>lua …<CR>" (dot-repeat idiom #41387) captures the same
|
||||
-- way: the constructed command is the atom.
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('n', ',x', function()
|
||||
return '<Cmd>call setline(1, "E" . v:count1)<CR>'
|
||||
end, { expr = true })
|
||||
]])
|
||||
feed('2,x')
|
||||
cmdev = atom_last()
|
||||
eq({
|
||||
type = 'excmd',
|
||||
lhs = ',x',
|
||||
keys = k('2<Cmd>call setline(1, "E" . v:count1)<NL>'),
|
||||
count = 2,
|
||||
}, pick(cmdev, 'type', 'lhs', 'keys', 'count'))
|
||||
fn.setline(1, 'reset')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(cmdev.keys))
|
||||
eq('E2', 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>')
|
||||
@@ -211,12 +262,11 @@ describe('CmdAtom', function()
|
||||
feed('vf,d')
|
||||
eq({ 'ef' }, get_lines())
|
||||
eq({ '3dl', 'vf,d' }, atoms_tail(2))
|
||||
eq(3, atoms()[#atoms() - 1].count)
|
||||
-- Structured decomposition: operator/motion/operand as fields, no byte-parsing.
|
||||
local op = atoms()[#atoms() - 1] -- "3dl"
|
||||
eq(
|
||||
{ operator = 'd', cmd = 'l', changed = true },
|
||||
pick(op, 'operator', 'cmd', 'cmdarg', 'motionforce', 'changed')
|
||||
{ operator = 'd', cmd = 'l', count = 3, changed = true },
|
||||
pick(op, 'operator', 'cmd', 'cmdarg', 'count', 'motionforce', 'changed')
|
||||
)
|
||||
-- A visual atom carries the completing operator's fields, and decomposes
|
||||
-- into its commands ("v", "f," and the operator).
|
||||
@@ -357,24 +407,15 @@ describe('CmdAtom', function()
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq({ ' bar', ' bar' }, get_lines())
|
||||
|
||||
-- Builtin "." replays the same keysequence, not Vim's equal-size reselect.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar', 'longword bar' })
|
||||
feed('gg0viwd')
|
||||
eq({ ' bar', 'longword bar' }, get_lines())
|
||||
feed('j0.')
|
||||
eq({ ' bar', ' bar' }, get_lines())
|
||||
|
||||
-- A viewport scroll that drags the cursor along (edge/'scrolloff') grows
|
||||
-- the selection by a viewport-dependent amount: void, no atom.
|
||||
local lines = {}
|
||||
for i = 1, 30 do
|
||||
lines[i] = 'l' .. i
|
||||
end
|
||||
local lines = vim.tbl_map(function(i)
|
||||
return 'l' .. i
|
||||
end, fn.range(1, 30))
|
||||
fn.setline(1, lines)
|
||||
feed('gg')
|
||||
local before = #atoms()
|
||||
feed('V<C-e>')
|
||||
n.poke_eventloop()
|
||||
eq(2, fn.line('.')) -- the scroll dragged the cursor: selection is lines 1-2
|
||||
feed('d')
|
||||
-- Publishes with empty `CmdAtom.keys`; the keys that produced it are in `lhs`.
|
||||
@@ -489,13 +530,9 @@ describe('CmdAtom', function()
|
||||
]])
|
||||
--- Count of captured visual atoms.
|
||||
local function nvisual()
|
||||
local count = 0
|
||||
for _, a in ipairs(atoms()) do
|
||||
if a.type == 'visual' then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
return #vim.tbl_filter(function(a)
|
||||
return a.type == 'visual'
|
||||
end, atoms())
|
||||
end
|
||||
fn.setline(1, { 'foo bar', 'longword bar' })
|
||||
feed('gg0')
|
||||
@@ -562,18 +599,16 @@ describe('CmdAtom', function()
|
||||
it('one event per user action', function()
|
||||
n.clear({ args = { '--clean' }, args_rm = { '--cmd' } })
|
||||
--- Feeds `keys`, asserts exactly ONE new event, with the given keys.
|
||||
local function atom(keys, expected, lhs)
|
||||
local function atom(keys, expected, lhs, type_)
|
||||
local before = #atoms()
|
||||
feed(keys)
|
||||
local evs = atoms()
|
||||
eq({ before + 1, k(expected) }, { #evs, evs[#evs].keys })
|
||||
eq(k(lhs or expected), evs[#evs].lhs)
|
||||
eq(
|
||||
{ before + 1, k(expected), k(lhs or expected), type_ or evs[#evs].type },
|
||||
{ #evs, evs[#evs].keys, evs[#evs].lhs, evs[#evs].type }
|
||||
)
|
||||
end
|
||||
local lines = {}
|
||||
for i = 1, 20 do
|
||||
lines[i] = 'alpha beta gamma delta epsilon zeta'
|
||||
end
|
||||
fn.setline(1, lines)
|
||||
fn.setline(1, fn['repeat']({ 'alpha beta gamma delta epsilon zeta' }, 20))
|
||||
feed('gg0')
|
||||
atoms_start()
|
||||
-- "." with nothing to repeat stuffs nothing.
|
||||
@@ -622,32 +657,22 @@ describe('CmdAtom', function()
|
||||
command('silent! nunmap %') -- the bundled matchit plugin maps it
|
||||
fn.setline(1, 'alpha (beta) gamma')
|
||||
feed('gg0f(')
|
||||
atom('%', '%')
|
||||
eq('motion', atom_last().type)
|
||||
atom('%', '%', nil, 'motion')
|
||||
fn.setline(1, 'alpha beta gamma delta epsilon zeta')
|
||||
-- G/gg (absolute line), H/M/L (viewport) are motions: multicursor replay is meaningful? (but
|
||||
-- cursors may be "merged").
|
||||
feed('gg0')
|
||||
atom('G', 'G')
|
||||
eq('motion', atom_last().type)
|
||||
atom('gg', 'gg')
|
||||
eq('motion', atom_last().type)
|
||||
atom('L', 'L')
|
||||
eq('motion', atom_last().type)
|
||||
atom('H', 'H')
|
||||
eq('motion', atom_last().type)
|
||||
atom('M', 'M')
|
||||
eq('motion', atom_last().type)
|
||||
atom('G', 'G', nil, 'motion')
|
||||
atom('gg', 'gg', nil, 'motion')
|
||||
atom('L', 'L', nil, 'motion')
|
||||
atom('H', 'H', nil, 'motion')
|
||||
atom('M', 'M', nil, 'motion')
|
||||
-- Jumps: absolute/shared-state navigation, their own kind.
|
||||
atom('ma', 'ma')
|
||||
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)
|
||||
atom('ma', 'ma', nil, 'normal') -- "m" sets state; it does not jump
|
||||
atom('`a', '`a', nil, 'jump')
|
||||
atom('<C-o>', '<C-O>', nil, 'jump')
|
||||
-- Non-redoable commands: still emitted, as type "command".
|
||||
atom('zz', 'zz')
|
||||
eq('normal', atom_last().type)
|
||||
atom('zz', 'zz', nil, 'normal')
|
||||
atom('u', 'u')
|
||||
atom('<C-r>', '<C-R>')
|
||||
-- "." emits its resolution (like "x" => "dl").
|
||||
@@ -674,23 +699,19 @@ describe('CmdAtom', function()
|
||||
-- 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>')
|
||||
-- A mapping is ALWAYS an atom, even when its commands capture no
|
||||
-- replayable keys ("]q" = :cnext): the event has empty keys.
|
||||
fn.setqflist({ { text = 'one' }, { text = 'two' } })
|
||||
feed(']q')
|
||||
eq(
|
||||
{ type = 'mapping', lhs = ']q', keys = '' },
|
||||
pick(atom_last(), 'type', 'lhs', 'keys', 'pending')
|
||||
)
|
||||
-- A mapping that ends mid-operation says what it awaits.
|
||||
-- A mapping that ends mid-operation captures its continuation. ",D" emits nothing until the
|
||||
-- typed "w" finishes the operation: one atom captures both.
|
||||
command('nnoremap ,D d')
|
||||
command('nnoremap ,V v')
|
||||
before = #atoms()
|
||||
feed(',D')
|
||||
eq({ lhs = ',D', pending = 'operator' }, pick(atom_last(), 'lhs', 'pending'))
|
||||
atom('w', 'dw') -- the supplied motion completes the operation
|
||||
eq(before, #atoms())
|
||||
atom('w', 'dw', ',Dw') -- The supplied motion finishes the operation
|
||||
-- Same for a mapping that opens Visual mode: "<Esc>" abandons the selection and closes it.
|
||||
feed(',V')
|
||||
eq({ lhs = ',V', pending = 'visual' }, pick(atom_last(), 'lhs', 'pending'))
|
||||
eq(before + 1, #atoms())
|
||||
feed('<Esc>')
|
||||
eq({ type = 'mapping', lhs = k(',V<Esc>') }, pick(atom_last(), 'type', 'lhs', 'keys'))
|
||||
-- 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')
|
||||
@@ -701,9 +722,9 @@ describe('CmdAtom', function()
|
||||
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 = ',x', keys = 'j' },
|
||||
{ type = 'motion', lhs = ',w', keys = 'w' },
|
||||
}, atoms_tail(2, 'type', 'lhs', 'keys', 'pending'))
|
||||
}, atoms_tail(2, 'type', 'lhs', 'keys'))
|
||||
command('set notimeout&')
|
||||
command('nunmap ,x')
|
||||
command('nunmap ,w')
|
||||
@@ -771,7 +792,7 @@ describe('CmdAtom', function()
|
||||
-- mapping: one event, keeping the command's own type and structure).
|
||||
local evs = atoms()
|
||||
eq(1, #evs)
|
||||
-- Inapplicable fields (count/reg/arg/motionforce/text/pending/atoms here) are omitted.
|
||||
-- Inapplicable fields (count/reg/arg/motionforce/text/atoms here) are omitted.
|
||||
eq({
|
||||
type = 'operator',
|
||||
keys = 'dw',
|
||||
@@ -880,34 +901,75 @@ describe('CmdAtom', function()
|
||||
feed('gg0')
|
||||
atoms_start()
|
||||
feed('ysiw"')
|
||||
-- The atom is the redobuff plus the getchar()'d payload: a replayed opfunc reads the same wrap
|
||||
-- char.
|
||||
-- Two atoms: the mapping ends mid-operation ("ys" => "g@"), then the operator the typed "iw"
|
||||
-- completed. Only the mapping has a translated `lhs`; the operator is its own input.
|
||||
eq({
|
||||
{ type = 'mapping', keys = '', lhs = 'ys', pending = 'operator' },
|
||||
{ type = 'operator', keys = 'g@iw"', lhs = 'g@iw"' },
|
||||
}, atoms_tail(2, 'type', 'keys', 'lhs', 'pending'))
|
||||
-- One atom: the mapping captures the operation finished by "iw". `keys` is the redobuff plus
|
||||
-- the getchar() payload: a replayed opfunc reads the same wrap char.
|
||||
eq(
|
||||
{ type = 'operator', keys = 'g@iw"', lhs = 'ysiw"' },
|
||||
pick(atom_last(), 'type', 'keys', 'lhs')
|
||||
)
|
||||
eq({ '"alpha" beta' }, get_lines())
|
||||
end)
|
||||
|
||||
it('a ":call" payload mapping publishes its resolved RHS', function()
|
||||
-- The atom published to CmdAtom carries the RESOLVED RHS (the ":call" line).
|
||||
it('":call" payload mapping appends payload to `keys`', function()
|
||||
n.exec(t_atom.delsurround_vim)
|
||||
fn.setline(1, { 'a (one)', 'b (two)' })
|
||||
feed('gg0f(')
|
||||
atoms_start()
|
||||
feed('ds)') -- ")" is the getchar()'d payload
|
||||
eq({ 'a one', 'b (two)' }, get_lines())
|
||||
local ev = atoms()[#atoms()]
|
||||
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'))
|
||||
local ev = atom_last()
|
||||
-- getchar() payload is appended to `keys`, replayable.
|
||||
eq({ lhs = 'ds)', keys = ':call DelSurround()\n)' }, pick(ev, 'lhs', 'keys'))
|
||||
feed('2G0f(')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'm', false)]]):format(ev.lhs))
|
||||
n.poke_eventloop()
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq({ 'a one', 'b two' }, get_lines())
|
||||
|
||||
-- input() payload is appended to `keys`, replayable.
|
||||
n.exec([[
|
||||
function! Suffix() abort
|
||||
call setline('.', getline('.') .. input('suffix: '))
|
||||
endfunction
|
||||
nnoremap ,s :<C-U>call Suffix()<CR>
|
||||
]])
|
||||
feed('gg')
|
||||
feed(',sX<CR>')
|
||||
n.poke_eventloop()
|
||||
eq('a oneX', fn.getline(1))
|
||||
ev = atom_last()
|
||||
eq({ lhs = k(',sX<CR>'), keys = ':call Suffix()\nX\r' }, pick(ev, 'lhs', 'keys'))
|
||||
feed('j')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq('b twoX', fn.getline(2))
|
||||
|
||||
-- Operator-pending mapping (:omap custom motion, like vim-sneak "z") fully captured.
|
||||
n.exec(t_atom.minisneak_vim)
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aa (x) here', 'bb (y) here' })
|
||||
feed('gg0')
|
||||
n.poke_eventloop()
|
||||
feed('dzhe')
|
||||
n.poke_eventloop()
|
||||
eq('here', fn.getline(1))
|
||||
ev = atom_last()
|
||||
eq(
|
||||
{ type = 'operator', operator = 'd', lhs = 'dzhe', keys = 'd:call MiniSneak()\nhe' },
|
||||
pick(ev, 'type', 'operator', 'lhs', 'keys')
|
||||
)
|
||||
feed('j0')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq('here', fn.getline(2))
|
||||
|
||||
-- Burst input ("f(" and the mapping arrive together): "f" peeks for a composing char with
|
||||
-- mappings enabled, so "ds" resolves while "f(" is still executing. The motion is still its
|
||||
-- own atom, and the K_IGNORE left by the peek stays out of `lhs`.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'a (one)' })
|
||||
feed('gg0')
|
||||
n.poke_eventloop()
|
||||
feed('f(ds)')
|
||||
eq({ 'a one' }, get_lines())
|
||||
eq({
|
||||
{ type = 'motion', keys = 'f(', lhs = 'f(' },
|
||||
{ type = 'excmd', keys = ':call DelSurround()\n)', lhs = 'ds)' },
|
||||
}, atoms_tail(2, 'type', 'keys', 'lhs'))
|
||||
end)
|
||||
|
||||
it('|restore-undo-cursor|: `pos` + `undoseq` restore across every undo form', function()
|
||||
@@ -983,10 +1045,7 @@ describe('CmdAtom', function()
|
||||
_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)
|
||||
vim.api.nvim_feedkeys(d.keys or d.lhs, d.keys and 'n' or 'm', false)
|
||||
end
|
||||
]])
|
||||
n.exec(t_atom.delsurround_vim)
|
||||
@@ -1015,11 +1074,11 @@ describe('CmdAtom', function()
|
||||
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] }
|
||||
local subs = d.atoms
|
||||
and table.concat(vim.tbl_map(function(c)
|
||||
return c.keys
|
||||
end, d.atoms))
|
||||
return { lhs = d.lhs, keys = d.keys, subs = subs, replayed = l[1] == l[2] }
|
||||
end
|
||||
|
||||
-- Asserts the table in runtime/doc/repeat.txt.
|
||||
@@ -1030,9 +1089,10 @@ describe('CmdAtom', function()
|
||||
{ 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 },
|
||||
-- No `keys` (Lua callback): the recipe feeds `lhs` with remapping.
|
||||
{ lhs = ']e', replayed = true },
|
||||
-- getchar() ")" is appended to `keys`, replayable.
|
||||
{ lhs = 'ds)', keys = ':call DelSurround()\n)', replayed = true },
|
||||
}, {
|
||||
both('a one two', '', 'dw'),
|
||||
both('aaa bbb', '', 'x'),
|
||||
@@ -1058,34 +1118,75 @@ describe('CmdAtom', function()
|
||||
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)
|
||||
-- Unreplayable: a void Visual op has empty keys, so the recipe feeds nothing
|
||||
-- rather than replaying a viewport-dependent selection.
|
||||
api.nvim_buf_set_lines(
|
||||
0,
|
||||
0,
|
||||
-1,
|
||||
true,
|
||||
vim.tbl_map(function(i)
|
||||
return 'l' .. i
|
||||
end, fn.range(1, 30))
|
||||
)
|
||||
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'))
|
||||
eq({ keys = '' }, pick(n.exec_lua('return _G.saved'), 'keys'))
|
||||
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.
|
||||
-- A repeat mapping is an atom too, and absent `keys` 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')
|
||||
{ type = 'mapping', lhs = k('<F7>') },
|
||||
pick(n.exec_lua('return _G.last'), 'type', 'keys', 'lhs')
|
||||
)
|
||||
end)
|
||||
|
||||
it('"." mapping repeats the last edit atom, plugin operations included', function()
|
||||
n.exec_lua([[
|
||||
local last ---@type vim.event.cmdatom.data?
|
||||
vim.api.nvim_create_autocmd('CmdAtom', {
|
||||
callback = function(ev)
|
||||
if ev.data.changed and ev.data.lhs ~= '.' then
|
||||
last = ev.data
|
||||
end
|
||||
end,
|
||||
})
|
||||
vim.keymap.set('n', '.', function()
|
||||
vim.schedule(function()
|
||||
if last then
|
||||
vim.api.nvim_feedkeys(last.keys or last.lhs, last.keys and 'n' or 'm', false)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
]])
|
||||
n.exec(t_atom.delsurround_vim)
|
||||
fn.setline(1, { 'aa bb', 'a (one)', 'b (two)' })
|
||||
-- "Builtin" edit.
|
||||
feed('gg0dw')
|
||||
n.poke_eventloop()
|
||||
feed('.')
|
||||
retry(nil, 1000, function()
|
||||
eq('', fn.getline(1))
|
||||
end)
|
||||
-- Payload mapping: builtin "." could not repeat this (without e.g. vim-repeat).
|
||||
feed('2G0f(ds)')
|
||||
n.poke_eventloop()
|
||||
eq('a one', fn.getline(2))
|
||||
feed('3G0f(.')
|
||||
retry(nil, 1000, function()
|
||||
eq('b two', fn.getline(3))
|
||||
end)
|
||||
end)
|
||||
|
||||
it('"," repeats the last motion atom', function()
|
||||
-- Keep in sync with the example in runtime/doc/repeat.txt.
|
||||
n.exec_lua([[
|
||||
@@ -1247,20 +1348,28 @@ describe('CmdAtom', function()
|
||||
]])
|
||||
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.
|
||||
-- A plugin motion via "<Cmd>" is type="excmd" (its RHS implementation); `moved`
|
||||
-- reports the same observed effect as the builtin motion below it.
|
||||
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')
|
||||
)
|
||||
-- A motion that did not move is still a motion; `moved` is the separate question.
|
||||
eq(
|
||||
{ { 'motion', false }, { 'motion', false }, { 'motion', false }, { 'normal', false } },
|
||||
vim.tbl_map(function(keys)
|
||||
feed(keys)
|
||||
local a = atom_last()
|
||||
return { a.type, a.moved }
|
||||
end, { 'G$w', 'gg0fz', 'gg0h', 'gg0zz' })
|
||||
)
|
||||
|
||||
-- Register-only operator also moves without changing: a motion carries no `operator`.
|
||||
feed('gg0wyb')
|
||||
|
||||
Reference in New Issue
Block a user