mirror of
https://github.com/neovim/neovim.git
synced 2026-08-25 08:31:51 +00:00
fix(cmdatom): lhs not always reported in CmdAtom #41386
Problem:
An unreplayable Visual operation does not emit a CmdAtom event. That's
maybe not super important, but it hints at a flaw in how `vatom`
"voiding" is plumbed: `vatom.state=kVatomVoid` replaces the "kind", so
that info is lost to later parts in the lifecycle.
Solution:
- Define `VatomState` as "flags", so `kVatomVoid` can "poison"
`vatom.state` without losing its kind flag.
- Unify `lhs`: always report the original user input in `CmdAtom.lhs`,
for all kinds of user actions: visual, "translated"/"stuffed" cmds,
and dot-repeat (".") itself.
- Unreplayable Visual atom emits CmdAtom with non-empty `lhs` and empty
`keys`, like a mapping/macro composite.
This commit is contained in:
@@ -425,6 +425,9 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
keysequence, Ex cmdline (":cnext<CR>"),
|
||||
mapping (and its subatoms), or scroll/mouse.
|
||||
|
||||
Event is |deferred|.
|
||||
<amatch> is the atom "type" (see below).
|
||||
|
||||
Only for user input, not programmatic input:
|
||||
INPUT ATOM ~
|
||||
typed keys yes
|
||||
@@ -435,23 +438,18 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
|:normal| no
|
||||
API requests no, lol
|
||||
"@q" fed by a script no
|
||||
"multicursor" replays no
|
||||
|multicursor| cascade no
|
||||
aborted operation no
|
||||
terminal-mode keys no
|
||||
mouse drag/release no
|
||||
Hydrogen yes
|
||||
|
||||
Note: keys fed from a typed mapping's own
|
||||
execution (e.g. its Lua callback) count as
|
||||
the mapping's expansion and fold into its
|
||||
atom, "t" or not.
|
||||
Fired at the next event-loop tick, not
|
||||
synchronously.
|
||||
<amatch> (the pattern) is the atom type.
|
||||
Note: all keys fed during a mapping execution
|
||||
(e.g. Lua callback) are part of the mapping's
|
||||
expansion.
|
||||
|
||||
The |event-data| has these fields (type: `vim.event.cmdatom.data`):
|
||||
|
||||
The |event-data| has these fields; a field
|
||||
that does not apply is omitted (type, keys,
|
||||
changed, and cascade are always present):
|
||||
- arg: Typed operand of `cmd`: the "x" of "fx",
|
||||
the replacement char of |r|.
|
||||
|key-notation|, like `cmd`.
|
||||
@@ -469,10 +467,14 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
raw internal bytes (not |key-notation|):
|
||||
feed directly to |feedkeys()| or
|
||||
|nvim_feedkeys()| (mode "n") to replay. Use
|
||||
|keytrans()| to key-notation. Empty for
|
||||
a mapping whose commands have no replayable
|
||||
keys (Lua callbacks).
|
||||
- lhs: LHS (user input). Raw bytes, like `keys`.
|
||||
|keytrans()| to key-notation. Empty if
|
||||
not replayable: a Lua mapping, or
|
||||
unreplayable Visual operation (mouse, |gv|,
|
||||
Select mode).
|
||||
- lhs: User input, before it is
|
||||
resolved/translated. Mapping LHS, macro
|
||||
register ("@q"), or the keys of a Visual op.
|
||||
Raw bytes, like `keys`.
|
||||
- motionforce |forced-motion|: "v", "V", or
|
||||
"<C-V>" (|key-notation|).
|
||||
- operator: Operator: "d", "g@", "zf", ….
|
||||
@@ -496,8 +498,6 @@ CmdAtom After a user action (an "atom" of input): any
|
||||
- "jump": moves the cursor via
|
||||
absolute/shared navigation state
|
||||
(|jumplist|, marks, |star|).
|
||||
- "mouse"/"scroll" atoms are emit-only:
|
||||
never cascaded, not replayable.
|
||||
|
||||
*CmdlineChanged*
|
||||
CmdlineChanged After EVERY change inside command line. Also
|
||||
|
||||
@@ -49,6 +49,11 @@ static void mc_vsel_refresh(void)
|
||||
{
|
||||
}
|
||||
|
||||
static bool mc_following(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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)?".
|
||||
@@ -58,10 +63,9 @@ 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;
|
||||
|
||||
/// Accumulating composite atom: the executing mapping/macro's subatoms, collected while it runs and
|
||||
/// collapsed at the command end. See `vatom` for Visual composite.
|
||||
/// Accumulating composite atom: an executing mapping/macro. See `vatom` for Visual composite.
|
||||
static struct {
|
||||
CmdAtomVec atoms;
|
||||
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 macro; ///< Macro execution: captured as an "@x"-labeled atom.
|
||||
@@ -73,18 +77,18 @@ static CmdFrame *cur_frame = NULL;
|
||||
|
||||
/// State of a Visual composite atom.
|
||||
typedef enum {
|
||||
// Nothing to replay:
|
||||
kVatomNone, ///< No pending Visual atom.
|
||||
kVatomVoid, ///< Not replayable: tainted/poisoned (by mouse, gv, …). But may emit CmdAtom.
|
||||
kVatomNone = 0, ///< No pending Visual atom.
|
||||
|
||||
// Accumulating, replayable:
|
||||
kVatomTyped, ///< User input (typed, or mapping/macro): emitted/cascaded at end.
|
||||
kVatomFed, ///< Fed input (":norm! vjd", scheduled feedkeys): preps redo, no emit/cascade.
|
||||
// Kind:
|
||||
kVatomTyped = 1, ///< User input (typed, or mapping/macro): emitted/cascaded at end.
|
||||
kVatomFed = 2, ///< Fed input (":norm! vjd", scheduled feedkeys): preps redo, no emit/cascade.
|
||||
|
||||
kVatomVoid = 4, ///< Not replayable: tainted/poisoned (by mouse, gv, …). But may emit CmdAtom.
|
||||
} VatomState;
|
||||
|
||||
/// Accumulating Visual composite: the full Visual keysequence, including the selection steps.
|
||||
/// Accumulating Visual composite: the full Visual keysequence (selection keys + operator).
|
||||
static struct {
|
||||
CmdAtomVec atoms; ///< Accumulated atoms during Visual mode.
|
||||
CmdAtomVec atoms; ///< Accumulated subatoms. A void session collects them as the `lhs` label.
|
||||
VatomState state;
|
||||
} vatom;
|
||||
|
||||
@@ -287,10 +291,9 @@ static Dict atom_dict(const CmdAtom *atom)
|
||||
PUT(d, "count", INTEGER_OBJ(spec->count));
|
||||
}
|
||||
// keys/lhs are RAW bytes (typeahead encoding).
|
||||
PUT(d, "keys", CSTR_TO_OBJ(atom->keys != NULL ? atom->keys : ""));
|
||||
if (atom->lhs != NULL && *atom->lhs != NUL) {
|
||||
PUT(d, "lhs", CSTR_TO_OBJ(atom->lhs));
|
||||
}
|
||||
const char *keys = atom->keys != NULL ? atom->keys : "";
|
||||
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));
|
||||
}
|
||||
@@ -339,9 +342,9 @@ 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 (Visual.active && atom_visual_replayable()) {
|
||||
if (atom_visual_pending()) {
|
||||
// Collecting the Visual composite: subatom of the pending visual atom.
|
||||
if (vatom.state == kVatomTyped) {
|
||||
if (vatom.state & kVatomTyped) {
|
||||
// Not for kVatomFed: redo-prep must not mark the enclosing span as captured.
|
||||
atom_captures++;
|
||||
}
|
||||
@@ -410,7 +413,9 @@ static void atom_stage_flush(CmdFrame *frame)
|
||||
return;
|
||||
}
|
||||
frame->staged.changed = buf_get_changedtick(curbuf) != frame->tick;
|
||||
atom_push(true, frame->staged); // Staged commands are always edits (cascadable).
|
||||
// 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 };
|
||||
}
|
||||
|
||||
@@ -447,7 +452,7 @@ static void atom_composite_start(const char *lhs, size_t len)
|
||||
composite.tick = buf_get_changedtick(curbuf);
|
||||
}
|
||||
|
||||
/// Collapses the collected subatoms (`CmdAtom.atoms`) and emits the composite atom.
|
||||
/// Emits the composite atom with its collected subatoms (`CmdAtom.atoms`).
|
||||
///
|
||||
/// :nnoremap gj i<C-J><Esc>k$
|
||||
/// "gj" => CmdAtom{ .lhs="gj", .keys="1i<NL><Esc>k$", kAMapping }
|
||||
@@ -510,10 +515,10 @@ void atom_suppress(bool suppress)
|
||||
atom_suppressed = suppress;
|
||||
}
|
||||
|
||||
/// Block atom pushes if: cascade in-progress, internal op is executing, or vatom is voided.
|
||||
/// Block atom pushes if: cascade in-progress, or internal op is executing.
|
||||
static bool atom_blocked(void)
|
||||
{
|
||||
return mc_replaying() || atom_suppressed || (vatom.state == kVatomVoid && Visual.active);
|
||||
return mc_replaying() || atom_suppressed;
|
||||
}
|
||||
|
||||
/// Decides if the command is capturable.
|
||||
@@ -704,6 +709,18 @@ void atom_macro_start(int regname)
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts accumulating a composite for a command that stuffs its "translation" ("x" => "dl").
|
||||
void atom_stuff_start(const cmdarg_T *cap)
|
||||
{
|
||||
// Not while another composite collects: a mapping's own label wins ("nnoremap <F6> xw").
|
||||
if (!atom_has_consumers() || mc_replaying() || atom_composite_active() || !atom_is_user_input()) {
|
||||
return;
|
||||
}
|
||||
char *lhs = atom_redo_keys(atom_cmd_spec(cap));
|
||||
atom_composite_start(lhs, strlen(lhs));
|
||||
xfree(lhs);
|
||||
}
|
||||
|
||||
/// Starts accumulating a composite for a mapping resolved from typed keys (vgetorpeek()).
|
||||
void atom_map_start(const char *lhs, size_t len)
|
||||
{
|
||||
@@ -723,10 +740,16 @@ static void atom_visual_reset(void)
|
||||
atoms_free(&vatom.atoms);
|
||||
}
|
||||
|
||||
/// True if the pending visual atom is replayable (accumulating, not voided).
|
||||
/// Visual atom is pending. A void session still accumulates, for the `lhs` label.
|
||||
static bool atom_visual_pending(void)
|
||||
{
|
||||
return vatom.state != kVatomNone;
|
||||
}
|
||||
|
||||
/// Pending Visual atom is replayable (not voided).
|
||||
bool atom_visual_replayable(void)
|
||||
{
|
||||
return vatom.state == kVatomTyped || vatom.state == kVatomFed;
|
||||
return atom_visual_pending() && !(vatom.state & kVatomVoid);
|
||||
}
|
||||
|
||||
/// The pending visual atom's accumulated keys (allocated), or NULL data if none is replayable
|
||||
@@ -739,18 +762,15 @@ String atom_visual_span(void)
|
||||
return atoms_concat_keys(vatom.atoms);
|
||||
}
|
||||
|
||||
/// Ends the pending visual atom, appends `suffix`, and stages it. Or discards it if selection is
|
||||
/// unreplayable (void/absent).
|
||||
/// Ends the pending visual atom, appends `suffix`, and stages it. Or discards if unreplayable.
|
||||
///
|
||||
/// @param suffix Owned.
|
||||
/// @param spec The completing operator, or NULL.
|
||||
/// @param redoable Prep redo so "." re-executes the selection. Unreplayable (void/absent)
|
||||
/// selection preps "1v" + operator instead (Vim's fixed-size visual-repeat).
|
||||
/// @param redoable Prep redo. Unreplayable selection preps "1v" + op (fixed-size visual-repeat).
|
||||
/// @return True if the redo was prepped.
|
||||
static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoable)
|
||||
{
|
||||
if (atom_suppressed) {
|
||||
// Replay, or internal operator applied as part of another command.
|
||||
if (mc_replaying() || atom_suppressed) { // Replay, or internal op applied during another cmd.
|
||||
xfree(suffix);
|
||||
return false;
|
||||
}
|
||||
@@ -761,8 +781,22 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
prep_redo_visual("1v", 2, (CmdSpec){ 0 }); // Equal-size fallback.
|
||||
redo_append_str(suffix, -1);
|
||||
}
|
||||
// A poisoned selection is still a user action: emit it with lhs + empty keys, like a mapping
|
||||
// whose commands captured nothing (atom_composite_end()).
|
||||
bool emit = (vatom.state & kVatomVoid) && (vatom.state & kVatomTyped) && spec != NULL
|
||||
&& suffix != NULL && atom_is_user_cmd();
|
||||
char *label = NULL;
|
||||
if (emit) {
|
||||
String collected = atoms_concat_keys(vatom.atoms);
|
||||
label = xrealloc(collected.data, collected.size + strlen(suffix) + 1);
|
||||
STRCPY(label + collected.size, suffix);
|
||||
}
|
||||
xfree(suffix);
|
||||
atom_visual_reset();
|
||||
atom_visual_reset(); // End the session before staging.
|
||||
if (emit) {
|
||||
atom_stage_set((CmdAtom){ .type = kAVisual, .spec = *spec, .keys = xstrdup(""),
|
||||
.lhs = label });
|
||||
}
|
||||
return prepped;
|
||||
}
|
||||
String v = atoms_concat_keys(vatom.atoms);
|
||||
@@ -774,7 +808,7 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
prep_redo_visual(vkeys, prefix, (CmdSpec){ 0 });
|
||||
redo_append_str(suffix, -1);
|
||||
}
|
||||
if (!atom_is_user_cmd() || vatom.state != kVatomTyped) {
|
||||
if (!atom_is_user_cmd() || !(vatom.state & kVatomTyped)) {
|
||||
// Not user input (":normal! vjd", fed keys): the redo prep above is the only effect; no emit.
|
||||
xfree(vkeys);
|
||||
xfree(suffix);
|
||||
@@ -804,7 +838,7 @@ static bool atom_visual_end_suffix(char *suffix, const CmdSpec *spec, bool redoa
|
||||
|
||||
/// Ends the pending visual atom with the operator `spec` ("viwee" + "x").
|
||||
///
|
||||
/// @return True if the redo was prepped.
|
||||
/// @return True if redo was prepped.
|
||||
bool atom_visual_end(CmdSpec spec, bool redoable)
|
||||
{
|
||||
return atom_visual_end_suffix(atom_redo_keys(spec), &spec, redoable);
|
||||
@@ -939,8 +973,8 @@ InsSession atom_ins_start(int cmd, long count, VisualIns vis, bool vblock)
|
||||
.vis = vis,
|
||||
.tick = buf_get_changedtick(curbuf),
|
||||
};
|
||||
if (vis != kVInsNone) {
|
||||
if (vis == kVInsKeys && vatom.state != kVatomTyped) {
|
||||
if (vis != kVInsNone && !mc_replaying()) {
|
||||
if (vis == kVInsKeys && !(atom_visual_replayable() && (vatom.state & kVatomTyped))) {
|
||||
// The selection came from fed keys (":norm", scheduled feedkeys).
|
||||
session.typed = false;
|
||||
}
|
||||
@@ -1012,6 +1046,10 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
if (mc_replaying() || atom_suppressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Classify
|
||||
//
|
||||
const bool user = atom_is_user_cmd();
|
||||
const unsigned keycls = atom_key_class(ca->cmdchar, ca->nchar);
|
||||
// Opaque cmd that changed nothing is invisible; one that changed the buffer/selection voids the
|
||||
@@ -1044,6 +1082,10 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
&& buf_get_changedtick(curbuf) != old->tick) || ins_cascaded)) {
|
||||
map_edit = true;
|
||||
}
|
||||
|
||||
//
|
||||
// Visual session: open/continue/close, and decide if this command is one of its subatoms.
|
||||
//
|
||||
bool vis = false;
|
||||
if (Visual.active) {
|
||||
if (!old->visual.active) {
|
||||
@@ -1051,17 +1093,16 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
// Decided once, at session start.
|
||||
vatom.state = (old->keytyped || atom_composite_active()) ? kVatomTyped : kVatomFed;
|
||||
}
|
||||
// Collecting the Visual composite: gated on the session (not atom_capturable()), so fed
|
||||
// selections (":normal! vjd") still accumulate for redo-prep.
|
||||
vis = atom_visual_replayable();
|
||||
if (vis && (Visual.select || (ca->cmdchar == 'g' && ca->nchar == 'v'))) {
|
||||
// Not replayable: Select-mode input; "gv" (absolute region).
|
||||
vatom.state = kVatomVoid;
|
||||
vis = false;
|
||||
}
|
||||
if (vis && (ca->cmdchar == 'Q' || ca->cmdchar == 'q')) {
|
||||
// Recording/replay commands are meta (not part of the edit): skip, no void.
|
||||
vis = false;
|
||||
// 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).
|
||||
vis = atom_visual_pending() && ca->cmdchar != 'Q' && ca->cmdchar != 'q';
|
||||
if (vis
|
||||
&& (Visual.select
|
||||
|| (ca->cmdchar == 'g' && ca->nchar == 'v')
|
||||
|| ((keycls & (kKeyScrollMove | kKeyScrollView | kKeyMouse)) && !unchanged))) {
|
||||
// Not replayable: Select-mode input; "gv" (absolute region); the selection moved by
|
||||
// viewport-dependent keys.
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
} else if (old->visual.active) {
|
||||
if (user && old->follow && atom_visual_replayable() && kv_size(vatom.atoms) > 0) {
|
||||
@@ -1073,6 +1114,10 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
atom_visual_reset();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Capture: does this command own an atom?
|
||||
//
|
||||
if ((vis && atom_captures == old->captures && ca->oap->op_type == OP_NOP)
|
||||
|| (!Visual.active
|
||||
&& !old->visual.active
|
||||
@@ -1104,7 +1149,11 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
&& strchr("/?:!Qq", ca->cmdchar) == NULL)
|
||||
|| special_motion);
|
||||
// Mapping-internal motions are part of its recipe: queue them, the clock edge decides.
|
||||
bool follow = mapped && motion;
|
||||
bool follow = (mc_following() || mapped) && motion;
|
||||
|
||||
//
|
||||
// Route: decide the atom type and push it.
|
||||
//
|
||||
if (curcmd.redo_pending && !mouse_cmd) {
|
||||
// Not for mouse commands (middle-click paste): pasting at every cursor would use
|
||||
// viewport-dependent positions.
|
||||
@@ -1148,8 +1197,8 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kACommand, spec);
|
||||
atom.changed = changed;
|
||||
atom_push(follow, atom);
|
||||
} else if (!vis && (scroll_cmd || mouse_cmd) && !atom_composite_active()) {
|
||||
// Emit-only (viewport-dependent), and never a subatom. Composite keys must stay replayable.
|
||||
} else if ((scroll_cmd || mouse_cmd) && !atom_composite_active()) {
|
||||
// Emit-only (viewport-dependent).
|
||||
CmdSpec spec = atom_cmd_spec(ca);
|
||||
if (IS_SPECIAL(ca->cmdchar)) {
|
||||
// Wheel/mouse count is never typed: do_mousescroll() wrote its internal step there.
|
||||
@@ -1161,12 +1210,12 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
}
|
||||
if (vis && kv_size(vatom.atoms) == collected && !unchanged) {
|
||||
// Not replayable: moved the selection by non-collectible keys.
|
||||
vatom.state = kVatomVoid;
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
}
|
||||
if (vis && (curbuf != old->buf || buf_get_changedtick(curbuf) != old->tick)) {
|
||||
// Not replayable: edited buffer during selection, so the keys do not describe the change.
|
||||
vatom.state = kVatomVoid;
|
||||
vatom.state |= kVatomVoid;
|
||||
}
|
||||
if (Visual.active && user && toplevel) {
|
||||
mc_vsel_refresh();
|
||||
|
||||
@@ -55,8 +55,8 @@ struct CmdAtom {
|
||||
char *keys; ///< Resolved keysequence (typeahead encoding), including `["x][count]` prefix
|
||||
///< (unlike `CmdSpec.body`, the raw unprefixed form).
|
||||
char *text; ///< Payload: insert-session text, or Ex/search cmdline.
|
||||
char *lhs; ///< Mapping LHS or macro register ("gj", "@q") that produced this atom, or NULL.
|
||||
///< Label/hint, not replayed.
|
||||
char *lhs; ///< Unresolved user input: mapping LHS or macro register ("@q"), or Visual op.
|
||||
///< Label/hint, not replayed. NULL: untranslated, same as `keys`.
|
||||
CmdAtomType type;
|
||||
bool changed; ///< The command changed the buffer.
|
||||
bool remap; ///< Replay `keys` w/ remap. For replay of a payload mapping (vim-surround "ds'"),
|
||||
|
||||
@@ -4877,6 +4877,7 @@ static void nv_optrans(cmdarg_T *cap)
|
||||
static const char *str = "xXDCsSY&";
|
||||
|
||||
if (!checkclearopq(cap->oap)) {
|
||||
atom_stuff_start(cap);
|
||||
if (cap->count0) {
|
||||
stuffnumReadbuff(cap->count0);
|
||||
}
|
||||
@@ -5769,6 +5770,7 @@ static void nv_dot(cmdarg_T *cap)
|
||||
// If "restart_edit" is true, the last but one command is repeated
|
||||
// instead of the last command (inserting text). This is used for
|
||||
// CTRL-O <.> in insert mode.
|
||||
atom_stuff_start(cap);
|
||||
if (start_redo(cap->count0, restart_edit != 0 && Ins.moved == kInsNone) == false) {
|
||||
clearopbeep(cap->oap);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ end)
|
||||
describe('CmdAtom', function()
|
||||
before_each(clear)
|
||||
|
||||
it('a counted mapped motion carries its count in the atom', function()
|
||||
it('motion mapping', function()
|
||||
command('nnoremap j gj')
|
||||
fn.setline(1, { 'a1', 'b2', 'c3', 'd4', 'e5' })
|
||||
feed('gg')
|
||||
@@ -80,11 +80,17 @@ describe('CmdAtom', function()
|
||||
feed('gg0')
|
||||
feed('3<F6>')
|
||||
ev = atom_last()
|
||||
eq({ type = 'mapping' }, pick(ev, 'type', 'count'))
|
||||
eq({ keys = '3dl', count = 3 }, pick(ev.atoms[1], 'keys', 'count'))
|
||||
eq({ type = 'mapping', lhs = k('<F6>') }, pick(ev, 'type', 'count', 'lhs'))
|
||||
-- Only the composite carries the mapping's LHS: a subatom is its own input.
|
||||
eq({ keys = '3dl', count = 3, lhs = '3dl' }, pick(ev.atoms[1], 'keys', 'count', 'lhs'))
|
||||
-- "." repeats the mapping's EDIT as ONE atom, labeled "." (not "<F6>").
|
||||
local before = #atoms()
|
||||
feed('.')
|
||||
eq(before + 1, #atoms())
|
||||
eq({ type = 'operator', keys = '3dl', lhs = '.' }, pick(atom_last(), 'type', 'keys', 'lhs'))
|
||||
end)
|
||||
|
||||
it('a Lua-callback mapping (the "]q" default) emits a mapping atom', function()
|
||||
it('Lua-callback mapping (e.g. "]q" default)', function()
|
||||
-- Same shape as the "]q" default mapping: a Lua callback with no
|
||||
-- replayable keys. Still a user action: it publishes with an empty
|
||||
-- replay payload.
|
||||
@@ -112,7 +118,7 @@ describe('CmdAtom', function()
|
||||
end)
|
||||
]])
|
||||
feed(',e')
|
||||
eq({ keys = '', changed = true }, pick(atom_last(), 'keys', 'changed'))
|
||||
eq({ keys = '', 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>')
|
||||
@@ -371,7 +377,12 @@ describe('CmdAtom', function()
|
||||
n.poke_eventloop()
|
||||
eq(2, fn.line('.')) -- the scroll dragged the cursor: selection is lines 1-2
|
||||
feed('d')
|
||||
eq(before, #atoms()) -- not replayable: no atom published for the edit
|
||||
-- Publishes with empty `CmdAtom.keys`; the keys that produced it are in `lhs`.
|
||||
eq(before + 1, #atoms())
|
||||
eq(
|
||||
{ type = 'visual', keys = '', lhs = k('V<C-E>d'), changed = true },
|
||||
pick(atom_last(), 'type', 'keys', 'lhs', 'changed')
|
||||
)
|
||||
eq('l3', fn.getline(1)) -- the edit itself deleted both selected lines
|
||||
|
||||
-- "." on the unreplayable operation falls back to an equal-size reselect
|
||||
@@ -379,6 +390,28 @@ describe('CmdAtom', function()
|
||||
feed('.')
|
||||
eq('l5', fn.getline(1))
|
||||
|
||||
-- But a viewport key that preserves cursor ("zz") does not move the selection: still
|
||||
-- replayable, and collected like any other subatom.
|
||||
fn.setline(1, lines)
|
||||
feed('gg')
|
||||
before = #atoms()
|
||||
feed('Vzzd')
|
||||
eq('l2', fn.getline(1))
|
||||
eq(before + 1, #atoms())
|
||||
-- Nothing was translated, so lhs=keys.
|
||||
eq({ type = 'visual', keys = 'Vzzd', lhs = 'Vzzd' }, pick(atom_last(), 'type', 'keys', 'lhs'))
|
||||
feed('.')
|
||||
eq('l3', fn.getline(1))
|
||||
|
||||
-- "gv" (absolute region) is unreplayable, but emitted in `lhs`.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa bbb' })
|
||||
feed('gg0viw<Esc>')
|
||||
before = #atoms()
|
||||
feed('gvd')
|
||||
eq(' bbb', fn.getline(1))
|
||||
eq(before + 1, #atoms())
|
||||
eq({ type = 'visual', keys = '', lhs = 'gvd' }, pick(atom_last(), 'type', 'keys', 'lhs'))
|
||||
|
||||
-- A fed (":normal!") Visual-put preps the selection keysequence, like any fed visual
|
||||
-- operator (":normal! vjd"): "." re-executes "Vjp", not a bare "p".
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aa', 'bb', 'cc', 'dd', 'ee' })
|
||||
@@ -526,14 +559,15 @@ describe('CmdAtom', function()
|
||||
end)
|
||||
end)
|
||||
|
||||
it('one event per operation, for each kind of atom', 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)
|
||||
local function atom(keys, expected, lhs)
|
||||
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)
|
||||
end
|
||||
local lines = {}
|
||||
for i = 1, 20 do
|
||||
@@ -542,10 +576,17 @@ describe('CmdAtom', function()
|
||||
fn.setline(1, lines)
|
||||
feed('gg0')
|
||||
atoms_start()
|
||||
-- "." with nothing to repeat stuffs nothing.
|
||||
feed('.')
|
||||
eq({ { type = 'command', 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')
|
||||
atom('3x', '3dl')
|
||||
atom('x', 'dl', 'x')
|
||||
-- A stuffed translation UNWRAPS: keeps the resolved "type" and has no subatoms.
|
||||
-- Only `lhs` marks it as translated; it is not a composite.
|
||||
eq({ type = 'operator' }, pick(atom_last(), 'type', 'atoms'))
|
||||
atom('3x', '3dl', '3x')
|
||||
atom('D', 'd$', 'D')
|
||||
atom('dw', 'dw')
|
||||
atom('"z2dw', '"z2dw')
|
||||
atom('yy', 'yy')
|
||||
@@ -570,14 +611,33 @@ describe('CmdAtom', function()
|
||||
atom('viwd', 'viwd')
|
||||
atom('Vd', 'Vd')
|
||||
atom('<C-v>jd', '<C-V>jd')
|
||||
-- Motions.
|
||||
-- Motions: the target is relative to the cursor.
|
||||
atom('w', 'w')
|
||||
atom('3w', '3w')
|
||||
atom('fb', 'fb')
|
||||
atom('G', 'G')
|
||||
atom('$', '$')
|
||||
feed('gg0')
|
||||
atom(']]', ']]')
|
||||
-- "%" is cursor-relative.
|
||||
command('silent! nunmap %') -- the bundled matchit plugin maps it
|
||||
fn.setline(1, 'alpha (beta) gamma')
|
||||
feed('gg0f(')
|
||||
atom('%', '%')
|
||||
eq('motion', atom_last().type)
|
||||
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)
|
||||
-- Jumps: absolute/shared-state navigation, their own kind.
|
||||
atom('ma', 'ma')
|
||||
eq('command', atom_last().type) -- "m" sets state; it does not jump
|
||||
@@ -592,9 +652,16 @@ describe('CmdAtom', function()
|
||||
atom('<C-r>', '<C-R>')
|
||||
-- "." emits its resolution (like "x" => "dl").
|
||||
feed('gg0')
|
||||
atom('x', 'dl')
|
||||
atom('.', 'dl')
|
||||
atom('3.', '3dl') -- "3.": the new count replaces the captured one
|
||||
atom('x', 'dl', 'x')
|
||||
atom('.', 'dl', '.')
|
||||
atom('3.', '3dl', '3.') -- "3.": the new count replaces the captured one
|
||||
-- "." emits exactly ONE atom labeled ".": the repeated keys fold into its composite.
|
||||
atom('iQ<Esc>', '1iQ<Esc>')
|
||||
atom('.', '1iQ<Esc>', '.')
|
||||
atom('viwd', 'viwd')
|
||||
atom('.', 'viwd', '.')
|
||||
atom('cwZZ<Esc>', 'cwZZ<Esc>')
|
||||
atom('.', 'cwZZ<Esc>', '.')
|
||||
-- Payload commands: the interactively-typed cmdline completes the
|
||||
-- keysequence (not a bare "/" or ":" prefix).
|
||||
atom('/beta<CR>', '/beta<NL>')
|
||||
@@ -630,13 +697,11 @@ describe('CmdAtom', function()
|
||||
local count = #atoms()
|
||||
feed(',E') -- E492 mid-mapping: the trailing "x" never runs
|
||||
eq(count, #atoms())
|
||||
atom('x', 'dl') -- the next command is not folded into the dead composite
|
||||
eq(nil, atom_last().lhs) -- not from a mapping: omitted
|
||||
atom('x', 'dl', 'x') -- the next command is not folded into the dead composite
|
||||
command('nmap ,A ,B')
|
||||
command('nmap ,B ,A')
|
||||
feed(',A') -- E223: recursive mapping
|
||||
atom('x', 'dl')
|
||||
eq(nil, atom_last().lhs) -- not from a mapping: omitted
|
||||
atom('x', 'dl', 'x')
|
||||
-- Macro playback ("@q") emits its commands' atoms (capture-on-replay);
|
||||
-- "@q" itself is a translation, never an atom.
|
||||
local total = #atoms()
|
||||
@@ -795,9 +860,14 @@ 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.
|
||||
eq({ 'g@iw"' }, atoms_tail(1))
|
||||
-- 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'))
|
||||
eq({ '"alpha" beta' }, get_lines())
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user