diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 42c28cdc4b..63b368e76a 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -425,6 +425,9 @@ CmdAtom After a user action (an "atom" of input): any keysequence, Ex cmdline (":cnext"), mapping (and its subatoms), or scroll/mouse. + Event is |deferred|. + 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. - (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 "" (|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 diff --git a/src/nvim/input_cmdatom.c b/src/nvim/input_cmdatom.c index 9bda298249..10a549fb8c 100644 --- a/src/nvim/input_cmdatom.c +++ b/src/nvim/input_cmdatom.c @@ -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 ik$ /// "gj" => CmdAtom{ .lhs="gj", .keys="1ik$", 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 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(); diff --git a/src/nvim/input_cmdatom_defs.h b/src/nvim/input_cmdatom_defs.h index 3011ca7d5e..6d6486bc00 100644 --- a/src/nvim/input_cmdatom_defs.h +++ b/src/nvim/input_cmdatom_defs.h @@ -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'"), diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 161cc1d6e6..2f688042ed 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -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); } diff --git a/test/functional/editor/cmdatom_spec.lua b/test/functional/editor/cmdatom_spec.lua index 43a5b965a3..fe9ee77cc0 100644 --- a/test/functional/editor/cmdatom_spec.lua +++ b/test/functional/editor/cmdatom_spec.lua @@ -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') 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('') }, 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 ""). + 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')) -- "" is opaque too, but unlike a Lua callback its command is text (like a ":" mapping). command('nnoremap ,c call setline(1, "N" . v:count)') @@ -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('Vd'), 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') + 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('jd', '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('', '') -- "." 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', '1iQ') + atom('.', '1iQ', '.') + atom('viwd', 'viwd') + atom('.', 'viwd', '.') + atom('cwZZ', 'cwZZ') + atom('.', 'cwZZ', '.') -- Payload commands: the interactively-typed cmdline completes the -- keysequence (not a bare "/" or ":" prefix). atom('/beta', '/beta') @@ -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)