mirror of
https://github.com/neovim/neovim.git
synced 2026-08-30 19:11:52 +00:00
fixfix(cmdatom): repeat Visual <Cmd>; CmdFrame + stage #41355
Problem: - Dot-repeat of a Visual selection prepared by a `<Cmd>` mapping, results in E1255 and leaves Visual mode active. - Visual-mode capture is implemented as a second, parallel "capture engine": it re-composes a CmdSpec from cmdarg_T per key and decides replayability from its own table of "void" key classes. Solution: - Model nested `normal_execute()` as a `CmdFrame` stack, instead of a single module-scoped `stage`. - Capture `<Cmd>` ":norm …" commands as subatoms from its nested frames. - Produce every command exactly once; atom_push_raw() routes the atom to the Visual composite while a selection is open, like it already does for mapping composites. - `v/pat<CR>d` is now repeatable and cascades.
This commit is contained in:
@@ -420,10 +420,10 @@ ChanOpen Just after a channel was opened.
|
||||
info as from |nvim_get_chan_info()|
|
||||
|
||||
*CmdAtom*
|
||||
CmdAtom After a user action (an input "atom"): any
|
||||
CmdAtom After a user action (an "atom" of input): any
|
||||
motion, operator, insert session, Visual-mode
|
||||
keysequence, Ex cmdline (":cnext<CR>"),
|
||||
mapping (and its sub-atoms), or scroll/mouse.
|
||||
mapping (and its subatoms), or scroll/mouse.
|
||||
|
||||
Only for user input, not programmatic input:
|
||||
INPUT ATOM ~
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
//
|
||||
// Every "user action" is an atom (emits CmdAtom event), but not every atom is "replayable".
|
||||
// - Replayable (cascade, dot-repeat) requires the full command grammar.
|
||||
// - No atoms for: mouse drag/release (TODO(justinmk)?), terminal-mode input, aborted operations,
|
||||
// command fragments (counts, register prefixes).
|
||||
// - No atoms for: mouse drag/release (TODO(justinmk)?), terminal-mode input, aborted operations.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
@@ -41,9 +40,19 @@
|
||||
|
||||
#include "input_cmdatom.c.generated.h"
|
||||
|
||||
static bool mc_replaying(void)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static void mc_vsel_refresh(void)
|
||||
{
|
||||
}
|
||||
|
||||
CmdAtomVec g_atoms = KV_INITIAL_VALUE;
|
||||
/// Total atoms ever pushed: so atom_cmd_start() can detect if a cmd already pushed its own atom.
|
||||
static uint64_t atom_pushes = 0;
|
||||
/// 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)?".
|
||||
static uint64_t atom_captures = 0;
|
||||
/// Suppresses atom pushes.
|
||||
static bool atom_suppressed = false;
|
||||
/// Mapping edited the buffer, or its insert-session cascaded: cascades as one unit, incl. motions.
|
||||
@@ -59,17 +68,14 @@ static struct {
|
||||
varnumber_T tick; ///< b:changedtick at start.
|
||||
} composite;
|
||||
|
||||
/// Staged atom, will be pushed at command end.
|
||||
static struct {
|
||||
CmdAtom atom; ///< One is staged when `keys` is non-NULL.
|
||||
varnumber_T tick; ///< b:changedtick when the atom was staged
|
||||
} stage;
|
||||
/// The executing command's frame; its `parent` chain spans nested `normal_execute()`.
|
||||
static CmdFrame *cur_frame = NULL;
|
||||
|
||||
/// State of a Visual composite atom.
|
||||
typedef enum {
|
||||
// Nothing to replay:
|
||||
kVatomNone, ///< No pending visual atom.
|
||||
kVatomVoid, ///< Visual keyseq was tainted/poisoned (by mouse, gv, …), not replayable.
|
||||
kVatomNone, ///< No pending Visual atom.
|
||||
kVatomVoid, ///< Not replayable: tainted/poisoned (by mouse, gv, …). But may emit CmdAtom.
|
||||
|
||||
// Accumulating, replayable:
|
||||
kVatomTyped, ///< User input (typed, or mapping/macro): emitted/cascaded at end.
|
||||
@@ -138,7 +144,10 @@ void atom_free_all(void)
|
||||
{
|
||||
atoms_free(&g_atoms);
|
||||
kv_destroy(g_atoms);
|
||||
atom_stage_drop();
|
||||
// A mid-command exit (e.g. ":qa!" from an option-expr) leaves live frames with staged atoms.
|
||||
for (CmdFrame *frame = cur_frame; frame != NULL; frame = frame->parent) {
|
||||
atom_free(&frame->staged);
|
||||
}
|
||||
atom_composite_abort();
|
||||
kv_destroy(composite.atoms);
|
||||
XFREE_CLEAR(curcmd.cmdline);
|
||||
@@ -330,7 +339,16 @@ static void atom_emit(const CmdAtom *atom, const char *pending, bool cascade)
|
||||
void atom_push_raw(bool cascade, CmdAtom atom)
|
||||
{
|
||||
assert(atom.keys != NULL);
|
||||
atom_pushes++;
|
||||
if (Visual.active && atom_visual_replayable()) {
|
||||
// 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++;
|
||||
}
|
||||
kv_push(vatom.atoms, atom);
|
||||
return;
|
||||
}
|
||||
atom_captures++;
|
||||
if (atom.type == kAVisual && kv_size(atom.atoms) > 0) {
|
||||
// The completing operator is the only subatom that could have edited.
|
||||
CmdAtom *last = &kv_A(atom.atoms, kv_size(atom.atoms) - 1);
|
||||
@@ -364,42 +382,36 @@ static void atom_push(bool cascade, CmdAtom atom)
|
||||
}
|
||||
|
||||
/// Stages an atom built before its command executes (do_pending_operator() prep-exempt, Visual
|
||||
/// ops); will be pushed at command end, once `changed` is known.
|
||||
/// ops), in the command's frame; pushed at frame end, once `changed` is known.
|
||||
static void atom_stage_set(CmdAtom atom)
|
||||
{
|
||||
assert(!atom_staged()); // If this happens, the stage may need to become a stack...
|
||||
atom_stage_drop();
|
||||
assert(cur_frame != NULL);
|
||||
assert(!atom_staged()); // One stage per frame: a second would discard a captured command.
|
||||
atom_free(&cur_frame->staged);
|
||||
if (atom_blocked()) {
|
||||
// Now, not at flush: drop the atom of an internal operator (atom_suppress()).
|
||||
atom_free(&atom);
|
||||
return;
|
||||
}
|
||||
assert(atom.keys != NULL);
|
||||
stage.atom = atom;
|
||||
stage.tick = buf_get_changedtick(curbuf);
|
||||
cur_frame->staged = atom;
|
||||
}
|
||||
|
||||
/// True if an atom is staged for the current command.
|
||||
/// True if an atom is staged for the current command (frame).
|
||||
static bool atom_staged(void)
|
||||
{
|
||||
return stage.atom.keys != NULL;
|
||||
return cur_frame != NULL && cur_frame->staged.keys != NULL;
|
||||
}
|
||||
|
||||
/// Discards the staged atom.
|
||||
static void atom_stage_drop(void)
|
||||
/// Pushes the frame's staged atom (no-op if none).
|
||||
static void atom_stage_flush(CmdFrame *frame)
|
||||
{
|
||||
atom_free(&stage.atom); // `keys=NULL` means "nothing staged".
|
||||
}
|
||||
|
||||
/// Pushes the staged atom (no-op if none).
|
||||
static void atom_stage_flush(void)
|
||||
{
|
||||
if (!atom_staged()) {
|
||||
if (frame->staged.keys == NULL) {
|
||||
return;
|
||||
}
|
||||
stage.atom.changed = buf_get_changedtick(curbuf) != stage.tick;
|
||||
atom_push(true, stage.atom); // Staged commands are always edits (cascadable).
|
||||
stage.atom = (CmdAtom){ 0 };
|
||||
frame->staged.changed = buf_get_changedtick(curbuf) != frame->tick;
|
||||
atom_push(true, frame->staged); // Staged commands are always edits (cascadable).
|
||||
frame->staged = (CmdAtom){ 0 };
|
||||
}
|
||||
|
||||
/// Queues an LHS-replay atom: a mapping that edited invisibly (:normal/:call, "ds'") re-runs
|
||||
@@ -498,10 +510,10 @@ void atom_suppress(bool suppress)
|
||||
atom_suppressed = suppress;
|
||||
}
|
||||
|
||||
/// Block atom pushes if: cascade in-progress, internal op is executing, or vatom is accumulating.
|
||||
/// Block atom pushes if: cascade in-progress, internal op is executing, or vatom is voided.
|
||||
static bool atom_blocked(void)
|
||||
{
|
||||
return atom_suppressed || (vatom.state != kVatomNone && Visual.active);
|
||||
return mc_replaying() || atom_suppressed || (vatom.state == kVatomVoid && Visual.active);
|
||||
}
|
||||
|
||||
/// Decides if the command is capturable.
|
||||
@@ -727,42 +739,6 @@ String atom_visual_span(void)
|
||||
return atoms_concat_keys(vatom.atoms);
|
||||
}
|
||||
|
||||
/// Captures a typed Visual-mode command into the pending visual atom (vatom): one subatom of
|
||||
/// the accumulating "viwee"-style keysequence.
|
||||
static void atom_capture_visual(cmdarg_T *ca, const CmdBaseline *old)
|
||||
{
|
||||
if (!atom_visual_replayable()) {
|
||||
return;
|
||||
}
|
||||
unsigned keycls = atom_key_class(ca->cmdchar, ca->nchar);
|
||||
if (Visual.select || ca->cmdchar >= 0x100
|
||||
|| (keycls & (kKeyPayload | kKeyScrollMove)) != 0
|
||||
|| (ca->cmdchar == 'g' && ca->nchar == 'v')) {
|
||||
// Not replayable: mouse/special keys, motions with an interactively-typed payload, Select mode,
|
||||
// "gv" (an absolute region), scrolling that moves the cursor (viewport-dependent extents).
|
||||
vatom.state = kVatomVoid;
|
||||
return;
|
||||
}
|
||||
if ((keycls & kKeyScrollView) != 0) {
|
||||
// A viewport scroll (C-E/C-Y) does not change the selection, UNLESS it dragged the cursor along
|
||||
// (viewport edge, 'scrolloff'), which moved the selection end.
|
||||
if (!equalpos(old->pos, curwin->w_cursor)) {
|
||||
vatom.state = kVatomVoid;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ca->cmdchar == 'Q' || ca->cmdchar == 'q' || ca->cmdchar == '"') {
|
||||
// Skip: recording/replay commands are meta (not part of the edit); a register spec ('"x') is
|
||||
// re-added by the operator that ends the selection (redo_prefix()).
|
||||
return;
|
||||
}
|
||||
bool operand = nv_nchar_is_arg(ca->cmdchar);
|
||||
// Omit `regname`, it would prefix '"x' to every command captured after a register spec.
|
||||
CmdSpec spec = { .count = ca->count0, .cmd = ca->cmdchar,
|
||||
.cmd2 = operand ? NUL : ca->nchar, .arg = operand ? ca->nchar : NUL };
|
||||
kv_push(vatom.atoms, ((CmdAtom){ .type = kAMotion, .spec = spec, .keys = atom_redo_keys(spec) }));
|
||||
}
|
||||
|
||||
/// Ends the pending visual atom, appends `suffix`, and stages it. Or discards it if selection is
|
||||
/// unreplayable (void/absent).
|
||||
///
|
||||
@@ -1005,37 +981,37 @@ static void atom_ins_push(const InsSession *session, bool cascade)
|
||||
}
|
||||
|
||||
/// Samples the pre-command state at normal_execute() entry; atom_cmd_end() diffs against it to
|
||||
/// classify the command (motion, Visual-mode transition, edit).
|
||||
void atom_cmd_start(CmdBaseline *old)
|
||||
/// classify the command (motion, Visual-mode transition, edit). Pushes the frame (`cur_frame`).
|
||||
void atom_cmd_start(CmdFrame *old)
|
||||
{
|
||||
old->pos = curwin->w_cursor;
|
||||
old->buf = curbuf;
|
||||
old->tick = buf_get_changedtick(curbuf);
|
||||
old->visual = Visual;
|
||||
old->keytyped = KeyTyped;
|
||||
old->pushes = atom_pushes;
|
||||
old->captures = atom_captures;
|
||||
// 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 = atom_staged();
|
||||
old->staged = (CmdAtom){ 0 };
|
||||
old->parent = cur_frame;
|
||||
cur_frame = old;
|
||||
curcmd.op_global = false;
|
||||
atom_redo_reset();
|
||||
}
|
||||
|
||||
/// Captures the typed command's atom: in Visual mode into `vatom`, else one atom per command.
|
||||
/// Captures the typed command's atom: one atom per command, produced from the CmdFrame diff and
|
||||
/// routed by atom_push_raw() (emit, or collect as a mapping/Visual subatom).
|
||||
///
|
||||
/// 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 CmdBaseline *old, bool toplevel)
|
||||
static void atom_capture_cmd(cmdarg_T *ca, const CmdFrame *old, bool toplevel)
|
||||
{
|
||||
// Not atom_blocked(): Visual capture must run while the vatom accumulates.
|
||||
if (atom_suppressed) {
|
||||
if (mc_replaying() || atom_suppressed) {
|
||||
return;
|
||||
}
|
||||
// Non-user input (":normal", scripted macro) pushes no atoms, but still accumulates the vatom,
|
||||
// so its operator can prep a redo: ":normal! vjd" is dot-repeatable.
|
||||
const bool user = atom_is_user_cmd();
|
||||
const unsigned keycls = atom_key_class(ca->cmdchar, ca->nchar);
|
||||
// Opaque cmd that changed nothing is invisible; one that changed the buffer/selection voids the
|
||||
@@ -1068,13 +1044,25 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
&& buf_get_changedtick(curbuf) != old->tick) || ins_cascaded)) {
|
||||
map_edit = true;
|
||||
}
|
||||
bool vis = false;
|
||||
if (Visual.active) {
|
||||
if (!old->visual.active) {
|
||||
atom_visual_reset();
|
||||
// Decided once, at session start.
|
||||
vatom.state = (old->keytyped || atom_composite_active()) ? kVatomTyped : kVatomFed;
|
||||
}
|
||||
atom_capture_visual(ca, old);
|
||||
// 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;
|
||||
}
|
||||
} else if (old->visual.active) {
|
||||
if (user && old->follow && atom_visual_replayable() && kv_size(vatom.atoms) > 0) {
|
||||
// Follow-motion ("q="): a selection abandoned without an operator (<Esc>, "v" toggle) still
|
||||
@@ -1084,10 +1072,17 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
} else {
|
||||
atom_visual_reset();
|
||||
}
|
||||
} else if (atom_capturable(old->consumers, old->keytyped)
|
||||
&& atom_pushes == old->pushes && !atom_staged()
|
||||
&& ca->oap->op_type == OP_NOP
|
||||
&& stuff_empty() && !ins_cascaded) {
|
||||
}
|
||||
if ((vis && atom_captures == old->captures && ca->oap->op_type == OP_NOP)
|
||||
|| (!Visual.active
|
||||
&& !old->visual.active
|
||||
&& atom_capturable(old->consumers, old->keytyped)
|
||||
&& atom_captures == old->captures
|
||||
&& !atom_staged()
|
||||
&& ca->oap->op_type == OP_NOP
|
||||
&& stuff_empty()
|
||||
&& !ins_cascaded)) {
|
||||
const size_t collected = kv_size(vatom.atoms);
|
||||
// KeyTyped survives stuffing but not macro playback; mapping/macro-fed commands are covered by
|
||||
// atom_composite_active().
|
||||
bool special_motion = (keycls & kKeyMotion) != 0;
|
||||
@@ -1131,23 +1126,29 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
} else {
|
||||
atom_free(&atom);
|
||||
}
|
||||
} else if (ca->searchbuf != NULL && (ca->cmdchar == '/' || ca->cmdchar == '?')) {
|
||||
// Payload typed in the cmdline ("/pat<CR>"). Emit-only.
|
||||
} else if (ca->searchbuf != NULL && (ca->cmdchar == '/' || ca->cmdchar == '?')
|
||||
&& !(vis && unchanged)) {
|
||||
// Payload typed in the cmdline ("/pat<CR>"). Emit-only. Not if pattern was not found.
|
||||
CmdAtom atom = atom_from_cmdline(kAMotion, ca, ca->searchbuf);
|
||||
atom.changed = changed;
|
||||
atom_push(false, atom);
|
||||
} else if (curcmd.cmdline != NULL && (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) {
|
||||
// Same for ":cnext<CR>" or "<Cmd>cnext<CR>".
|
||||
} else if (!vis && curcmd.cmdline != NULL
|
||||
&& (ca->cmdchar == ':' || ca->cmdchar == K_COMMAND)) {
|
||||
// Same for ":cnext<CR>" or "<Cmd>cnext<CR>". Never a Visual subatom.
|
||||
CmdAtom atom = atom_from_cmdline(kAEx, ca, curcmd.cmdline);
|
||||
atom.changed = changed;
|
||||
atom_push(false, atom);
|
||||
} else if (replayable) {
|
||||
} else if (replayable && (!vis || (keycls & kKeyPayload) == 0)) {
|
||||
// Non-redoable command (u, zz, q=): never cascaded as an edit.
|
||||
CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kACommand,
|
||||
atom_cmd_spec(ca));
|
||||
CmdSpec spec = atom_cmd_spec(ca);
|
||||
if (vis) {
|
||||
// Omit `regname`: would prefix '"x' to every command collected; op-end re-adds it later.
|
||||
spec.regname = 0;
|
||||
}
|
||||
CmdAtom atom = atom_from_spec(motion ? kAMotion : jump_cmd ? kAJump : kACommand, spec);
|
||||
atom.changed = changed;
|
||||
atom_push(follow, atom);
|
||||
} else if ((scroll_cmd || mouse_cmd) && !atom_composite_active()) {
|
||||
} else if (!vis && (scroll_cmd || mouse_cmd) && !atom_composite_active()) {
|
||||
// Emit-only (viewport-dependent), and never a subatom. Composite keys must stay replayable.
|
||||
CmdSpec spec = atom_cmd_spec(ca);
|
||||
if (IS_SPECIAL(ca->cmdchar)) {
|
||||
@@ -1158,18 +1159,26 @@ static void atom_capture_cmd(cmdarg_T *ca, const CmdBaseline *old, bool toplevel
|
||||
atom.changed = changed;
|
||||
atom_push(false, atom);
|
||||
}
|
||||
if (vis && kv_size(vatom.atoms) == collected && !unchanged) {
|
||||
// Not replayable: moved the selection by non-collectible keys.
|
||||
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;
|
||||
}
|
||||
if (Visual.active && user && toplevel) {
|
||||
mc_vsel_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes a cmd at normal_execute() exit: captures its atom, pushes the staged one, ends the
|
||||
/// composite.
|
||||
void atom_cmd_end(cmdarg_T *ca, const CmdBaseline *old, bool toplevel)
|
||||
/// 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)
|
||||
{
|
||||
atom_capture_cmd(ca, old, toplevel);
|
||||
if (!old->staged) {
|
||||
// Flush only what this cmd staged. In case of nested :norm (e.g. 'indentexpr' during "gq").
|
||||
atom_stage_flush();
|
||||
}
|
||||
atom_stage_flush(old);
|
||||
|
||||
// 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
|
||||
@@ -1178,4 +1187,5 @@ void atom_cmd_end(cmdarg_T *ca, const CmdBaseline *old, bool toplevel)
|
||||
map_edit = false;
|
||||
atom_composite_end(ca->oap->op_type != OP_NOP ? "operator" : Visual.active ? "visual" : "");
|
||||
}
|
||||
cur_frame = old->parent;
|
||||
}
|
||||
|
||||
@@ -13,18 +13,20 @@
|
||||
/// Pending atom(s). Multiple atoms may queue; they cascade as a batch (mc_clock_edge).
|
||||
extern CmdAtomVec g_atoms;
|
||||
|
||||
/// Pre-command state sampled at normal_execute() entry; atom_cmd_end() diffs it to classify.
|
||||
typedef struct {
|
||||
/// Pre-command state sampled at entry + storage for its "staged" atom. atom_cmd_end() finalizes it.
|
||||
typedef struct CmdFrame CmdFrame;
|
||||
struct CmdFrame {
|
||||
pos_T pos; ///< Cursor position.
|
||||
const buf_T *buf; ///< Current buffer.
|
||||
varnumber_T tick; ///< b:changedtick
|
||||
VisualState visual; ///< Visual-mode state (active/start/mode are diffed).
|
||||
bool keytyped; ///< KeyTyped
|
||||
uint64_t pushes; ///< `atom_pushes` (total atoms ever pushed).
|
||||
uint64_t captures; ///< Capture counter.
|
||||
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).
|
||||
bool staged; ///< atom_staged()
|
||||
} CmdBaseline;
|
||||
CmdAtom staged; ///< Atom staged in this frame. `keys == NULL`: none.
|
||||
CmdFrame *parent; ///< Enclosing frame (nested normal_execute()); NULL at toplevel.
|
||||
};
|
||||
|
||||
#include "input_cmdatom.h.generated.h"
|
||||
|
||||
@@ -1094,8 +1094,8 @@ static int normal_execute(VimState *state, int key)
|
||||
static int depth = 0;
|
||||
depth++;
|
||||
|
||||
CmdBaseline atom_old;
|
||||
atom_cmd_start(&atom_old);
|
||||
CmdFrame frame;
|
||||
atom_cmd_start(&frame);
|
||||
|
||||
NormalState *s = (NormalState *)state;
|
||||
s->command_finished = false;
|
||||
@@ -1269,7 +1269,7 @@ static int normal_execute(VimState *state, int key)
|
||||
|
||||
finish:
|
||||
normal_finish_command(s);
|
||||
atom_cmd_end(&s->ca, &atom_old, depth == 1);
|
||||
atom_cmd_end(&s->ca, &frame, depth == 1);
|
||||
xfree(s->ca.searchbuf);
|
||||
depth--;
|
||||
return 1;
|
||||
|
||||
@@ -4,9 +4,8 @@ local n = require('test.functional.testnvim')()
|
||||
|
||||
local m = {}
|
||||
|
||||
--- Get buffer lines as a table.
|
||||
function m.get_lines()
|
||||
return n.api.nvim_buf_get_lines(0, 0, -1, true)
|
||||
return n.buf_lines(0)
|
||||
end
|
||||
|
||||
--- vim.keycode(): |key-notation| => the raw bytes of the CmdAtom event's keys/lhs.
|
||||
@@ -26,10 +25,9 @@ function m.atoms_start()
|
||||
]])
|
||||
end
|
||||
|
||||
--- Gets the collected CmdAtom event-data (the events are deferred: drain
|
||||
--- the event loop first).
|
||||
--- Gets the collected CmdAtom event-data.
|
||||
function m.atoms()
|
||||
n.poke_eventloop()
|
||||
n.poke_eventloop() -- CmdAtom is deferred, so drain the event loop first.
|
||||
return n.exec_lua('return _G.atoms')
|
||||
end
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ describe('CmdAtom', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('fires for typed input, never for programmatic sources', function()
|
||||
it('fires for user input, not programmatic sources', function()
|
||||
atoms_start()
|
||||
-- Drain deferred CmdAtom events, then return + clear the collected list.
|
||||
local function take()
|
||||
@@ -334,7 +334,7 @@ describe('CmdAtom', function()
|
||||
eq(1, #take())
|
||||
end)
|
||||
|
||||
it('visual atom keys re-execute on replay; unreplayable ops fall back to equal-size', function()
|
||||
it('visual atom replay; fallback to equal-size if unreplayable', function()
|
||||
-- The core use-case for a plugin: observe CmdAtom, capture a Visual-mode
|
||||
-- operation's resolved `keys`, and replay them verbatim to re-execute it.
|
||||
fn.setline(1, { 'foo bar', 'longword bar' })
|
||||
@@ -344,19 +344,20 @@ describe('CmdAtom', function()
|
||||
eq({ ' bar', 'longword bar' }, get_lines())
|
||||
local ev = atom_last()
|
||||
eq('visual', ev.type)
|
||||
-- Replay the captured keys at line 2. The keysequence RE-EXECUTES (not an
|
||||
-- equal-size reselect): "iw" selects THAT line's word (the longer one),
|
||||
-- so the delete adapts to the new context.
|
||||
|
||||
-- Replay the captured keys at line 2. The keysequence RE-EXECUTES (not an equal-size reselect):
|
||||
-- "iw" selects THAT line's word (the longer one), so the delete adapts to the new context.
|
||||
feed('j0')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq({ ' bar', ' bar' }, get_lines())
|
||||
-- Builtin |.| replays the same keysequence: own-sized re-execution, not
|
||||
-- Vim's equal-size reselect (|visual-repeat|).
|
||||
|
||||
-- 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 = {}
|
||||
@@ -372,11 +373,13 @@ describe('CmdAtom', function()
|
||||
feed('d')
|
||||
eq(before, #atoms()) -- not replayable: no atom published for the edit
|
||||
eq('l3', fn.getline(1)) -- the edit itself deleted both selected lines
|
||||
|
||||
-- "." on the unreplayable operation falls back to an equal-size reselect
|
||||
-- ("1v" + operator): it deletes the same number of lines at the cursor.
|
||||
feed('.')
|
||||
eq('l5', fn.getline(1))
|
||||
-- A fed (":normal!") Visual put preps the selection keysequence, like any fed visual
|
||||
|
||||
-- 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' })
|
||||
feed('ggyy')
|
||||
@@ -384,6 +387,7 @@ describe('CmdAtom', function()
|
||||
eq({ 'aa', 'cc', 'dd', 'ee' }, get_lines())
|
||||
feed('j.')
|
||||
eq({ 'aa', 'aa', 'bb', 'ee' }, get_lines())
|
||||
|
||||
-- {Visual}r<C-V><CR> replaces with a literal <CR> (REPLACE_CR_NCHAR): inexpressible as
|
||||
-- spec chars, so the literal keys compose the redo tail, which "." replays.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'abcd', 'efgh' })
|
||||
@@ -391,6 +395,42 @@ describe('CmdAtom', function()
|
||||
eq({ '\r\rcd', 'efgh' }, get_lines())
|
||||
feed('j0.')
|
||||
eq({ '\r\rcd', '\r\rgh' }, get_lines())
|
||||
|
||||
-- <Cmd> ":norm" mapping that extends the selection: "." replays it. #41323
|
||||
command('xmap <M-m> <Cmd>normal! $<CR>')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa', 'bbb' })
|
||||
feed('gg0v<M-m>d') -- "$" in Visual is one past EOL, so "d" swallows the newline (= plain v$d)
|
||||
eq({ 'bbb' }, get_lines())
|
||||
command('let v:errmsg = ""')
|
||||
feed('.')
|
||||
eq('', api.nvim_get_vvar('errmsg'))
|
||||
eq('n', fn.mode()) -- Not in Visual.
|
||||
eq({ '' }, get_lines())
|
||||
|
||||
-- <Cmd> ":norm" captured as nested subatoms: the atom is "ved", not the <Cmd>.
|
||||
command('xmap <M-w> <Cmd>normal! e<CR>')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'one two three', 'four five six' })
|
||||
feed('gg0v<M-w>d')
|
||||
eq({ ' two three', 'four five six' }, get_lines())
|
||||
eq({ type = 'visual', keys = 'ved' }, pick(atom_last(), 'type', 'keys'))
|
||||
feed('j0.')
|
||||
eq({ ' two three', ' five six' }, get_lines())
|
||||
|
||||
-- Buffer-editing <Cmd> is unreplayable (void), so "." fallsback to equal-size reselect.
|
||||
command('xmap <M-a> <Cmd>call append(1, "X")<CR>')
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'ab', 'cd' })
|
||||
feed('gg0vl<M-a>d')
|
||||
eq({ '', 'X', 'cd' }, get_lines())
|
||||
feed('3gg0.')
|
||||
eq({ '', 'X', '' }, get_lines())
|
||||
|
||||
-- A search-extended selection re-executes: the payload travels in the collected keys.
|
||||
api.nvim_buf_set_lines(0, 0, -1, true, { 'ab META x', 'cdef META y' })
|
||||
feed('gg0v/META<CR>d')
|
||||
eq({ 'ETA x', 'cdef META y' }, get_lines())
|
||||
eq({ type = 'visual', keys = k('v/META<NL>d') }, pick(atom_last(), 'type', 'keys'))
|
||||
feed('j0.')
|
||||
eq({ 'ETA x', 'ETA y' }, get_lines())
|
||||
end)
|
||||
|
||||
it('a mapping can repeat the last visual atom', function()
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('Visual mode and operator', function()
|
||||
feed_command([[call setline(1, 'KiwiRaspberryDateWatermelonPeach')]])
|
||||
feed_command('/^Kiwi')
|
||||
feed('vWcNo<esc>l.fD2vd.')
|
||||
expect('NoNoberry')
|
||||
expect('NoNo')
|
||||
|
||||
-- Same, using the vmap that maps to an Ex command.
|
||||
feed_command([[call setline(1, 'JambuRambutanBananaTangerineMango')]])
|
||||
|
||||
@@ -553,7 +553,7 @@ func Test_visual_mode_maps()
|
||||
call setline(1, 'KiwiRaspberryDateWatermelonPeach')
|
||||
call cursor(1, 1)
|
||||
exe "normal vWcNo\<Esc>l.fD2vd."
|
||||
call assert_equal('NoNoberry', getline(1))
|
||||
call assert_equal('NoNo', getline(1))
|
||||
|
||||
call setline(1, 'JambuRambutanBananaTangerineMango')
|
||||
call cursor(1, 1)
|
||||
|
||||
Reference in New Issue
Block a user