feat(input)!: CmdAtom event #41297

Problem:
There is no unified notion of a "user action".

Vim processes input by one-char-at-a-time, and mostly throws away any
hints it might gather about the user's action, with one exception: it
stores the last _edit_ action (the "redo buffer", encoded as
unstructured `["x][v][count]body` bytes).

Plugins can only observe individual keys (vim.on_key) and high-level
effects (TextChanged, CursorMoved).

Solution:
- Users can subscribe to `CmdAtom` events to handle any user action.
  - Event is deferred; handlers cannot cancel or interfere with user
    actions.
- Capture `CmdSpec` from the normal/insert/visual subsystems.
  - typeahead/readahead stay unstructured (`buffheader_T`): they are key
    streams, not commands.
  - the redo/record buffers become `StringBuilder`: fewer
    allocations/copies.
- Repurpose the input/redo engine to accept `CmdSpec` objects.

"atom": one repeatable unit of user input, as a resolved (post-mapping)
keysequence plus structured fields. Only user actions, not `:normal`,
API calls, or non-"t" `feedkeys`.

BREAKING: dot-repeat of an Insert session, replays the entire session
including cursor-moves (:help ins-repeat).

BREAKING: dot-repeat of a Visual operation, replays the selection
instead of operating on a fixed-size region.
This commit is contained in:
Justin M. Keyes
2026-08-14 09:30:31 -04:00
committed by GitHub
parent 485ae7e31a
commit 64a301184e
63 changed files with 3577 additions and 909 deletions

View File

@@ -419,6 +419,85 @@ ChanOpen Just after a channel was opened.
Sets these |v:event| keys:
info as from |nvim_get_chan_info()|
*CmdAtom*
CmdAtom After a user action (an input "atom"): any
motion, operator, insert session, Visual-mode
keysequence, Ex cmdline (":cnext<CR>"),
mapping (and its sub-atoms), or scroll/mouse.
Only for user input, not programmatic input:
INPUT ATOM ~
typed keys yes
|nvim_input()| yes
|nvim_feedkeys()| with "t" (see note)
dot-repeat |.| yes
mapping, typed "@q" yes
|:normal| no
API requests no, lol
"@q" fed by a script no
"multicursor" replays 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.
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`.
- atoms: The constituent atoms of a composite
(multi-command mapping, Visual sequence),
in-order. A consumer of e.g. "motion" atoms
may want to inspect these "children".
- cascade: Queued for "multicursor" replay.
- changed: Buffer was changed.
- cmd: Command/motion/object name: "w", "fx" is
"f", "iw", "gJ", "<Home>", …. |key-notation|
(not raw bytes).
- count: Effective |count|; omitted if none.
- keys: Resolved (post-mapping) keysequence,
raw internal bytes (not |key-notation|):
feed directly to |feedkeys()| or
|nvim_feedkeys()| (mode "n") to replay. Use
|keytrans()| to key-notation. Empty for
a mapping whose commands have no replayable
keys (|<Cmd>|/Lua commands).
- lhs: LHS (user input). Raw bytes, like `keys`.
- motionforce |forced-motion|: "v", "V", or
"<C-V>" (|key-notation|).
- operator: Operator: "d", "g@", "zf", ….
|key-notation|, like `cmd`.
- pending: What is awaiting input after
a mapping: "operator", "visual".
- reg: Register name.
- text: Payload text: inserted text of an
insert session (after its last cursor-move,
like |quote.|), Ex cmdline, or search.
Literal text, not key encoding. Examples:
- typed "iab<Esc>" → text="ab"
- typed ":cnext<CR>" → text="cnext"
- typed "iab<Left>c<Esc>" → text="c"
- type: "command", "ex", "insert", "jump",
"mapping", "motion", "mouse", "operator",
"scroll", "visual".
- "command": neither edits nor moves the
cursor (undo, folds, CTRL-W commands, …).
- "jump": moves the cursor via
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
triggered during mappings! Use |<Cmd>| instead

View File

@@ -169,6 +169,39 @@ Other references:
- https://github.com/neovim/neovim/pull/18375
- https://github.com/neovim/neovim/pull/21605
==============================================================================
State *dev-state*
"Editor state" is represented by these (sometimes overlapping) concepts:
- shada (`src/nvim/shada.c`): user data: registers, marks, …. Msgpack format.
- sessions/views (`src/nvim/ex_session.c`): windows/layout, per-window
cursor/options, CWD, …. Vimscript format: commands, not merged data.
Overlaps shada for the buffer list and global variables.
- context (`src/nvim/context.c`): |nvim_get_context()|.
Currently interfaces shada and other state. Needs more thought.
- scoped execution ("temporary state"): `ctx_switch()`, `vim._with()`,
|nvim_win_call()|, |nvim_buf_call()|, `cmdmod_T`. Temporary switches, not
snapshots. The C mechanisms overlap with each other; `vim._with()` is the
unified interface.
- reentrancy protection: save/restore around nested execution. `InsState`,
`RedoState`.
- TODO: ad hoc pairs remain for typeahead (`tasave_T`), |v:event|, view
state, cmdline, with inconsistent nesting semantics (stack vs refcount vs
single-shot).
LONG-TERM VISION(?): shada is the authority on all user-data state, sessions
on layout state? New state kinds extend the shada schema instead of adding
a format or an in-memory sidecar. Eliminate context.c?
LUA FUNCTIONS ACROSS PROCESSES
The test harness transfers Lua closures between Nvim instances
(`test/functional/testnvim/exec_lua.lua`). This works ONLY between same-build
instances (LuaJIT bytecode not portable across versions/builds) and only for
trusted peers. Upvalues are limited to msgpack-able values (not
function/userdata; tables lose metatables and identity).
==============================================================================
Filesystem *dev-filesystem*
@@ -502,6 +535,109 @@ When doing an `i` or `a` command, `normal_cmd()` will call the `edit()` function
It contains a loop that waits for the next character and handles it. It
returns when leaving Insert mode.
==============================================================================
Multicursor *dev-multicursor* *dev-cmdatom*
Multicursor is essentially a bunch of extmarks that "cascade" the user action
(|CmdAtom|) driven by the primary cursor.
An "atom" is any user action, as a resolved ("elemental", post-mapping)
keysequence, the same material ("redobuf") replayed by dot-repeat
(|single-repeat|). Whereas a macro is a series of meaningless characters
whose meaning is context-dependent, assigned at the moment of execution, an
atom scopes an input sequence to one, semantic, user action. Examples: "x" is
captured as "dl", an insert session as `ciwfoo<Esc>`, a Visual-mode edit as
`viweed`.
CONCEPTS
- ATOM: A repeatable unit of user input: the resolved keysequence, plus
structured fields (`CmdSpec`). Every user action emits a |CmdAtom| event.
- SPAN: Fragment of an Insert or Visual sequence, cascades but does not emit
a CmdAtom.
- COMPOSITE: An atom composed of subatoms (`CmdAtom.atoms`): a mapping/macro's
commands, or a Visual sequence.
- INSERT-SESSION: All changes from an insert-mode session, until <Esc>.
- INSERTION: An `Ins.start`..cursor "undo unit" within a session.
- INSERT-CASCADE: Replay an insert-session span-by-span as it is typed (each
span is packaged as a quasi-session: `i` + keys + `<Esc>`), via nested
`edit()`. Pending literal text is a PREVIEW until its span completes.
- REPLAY: Execute keys (dot-repeat, or atom).
- CASCADE: Replay queued atoms at the "clock edge" at every cursor. Visual
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.
IMPLEMENTATION
Capture happens at the `atom_xx()` hooks.
- `prep_redo()` marks a redoable command (operators, `r`, `J`, `p`, …). Then
`atom_cmd_end()` takes the final redobuff as the atom, (including
payloads, e.g. `d/pat<CR>`). Prep-exempt commands (yank, `D`, folds) are
reconstructed instead, in `atom_capture_op()`.
- `atom_cmd_start()` delimits a command in `normal_execute()`. Everything not
covered above is decided here (motions, jumps, scrolls, mouse).
- `atom_ins_start()` delimits an insert session.
- `atom_map_start()`, `atom_macro_start()` delimits a composite.
- `atom_visual_end()` completes the pending Visual atom, `viweex` => `viweed`.
<Esc> discards it; non-replayable commands VOID it.
- `atom_cmdline_set()` gets an invoked cmdline payload (e.g. `/pat<CR>` is
a motion atom, `:cnext<CR>` is an "ex" atom.)
DOT-REPEAT
The redo buffer itself is the pending change atom (`RedoBuf`); the register,
Visual flag and count are FIELDS, and the command body is bytes appended
during execution by the `redo_append_xx()` family (Vim's `AppendToRedobuff*`).
The old `["x][v][count]body` byte encoding no longer exists; `prep_redo()`
stores the prefix structurally, and consumers recompose it at the boundaries
(`redo_keys()`, `start_redo()`, `atom_from_redo()`).
THE CASCADE
Cursors are `Context` entries in `mc_cursors`, tracked as extmarks. The
extmark is authoritative: it follows edits, and user code may delete cursors
by deleting their extmarks. Cursor-local state is swapped in on replay, so
e.g. each cursor reads/writes its own registers.
CmdAtoms queued for cascade (`g_atoms`) are replayed at every cursor on the
"clock edge": the completion of a toplevel `normal_execute()`. While
a mapping is executing (its keys are still in typebuf) the edge is deferred
(subatoms queue/accumulate).
Undo is buffer-global and applied in bulk. Each cascade adds exactly one undo
state, and undo/redo is itself never cascaded (`u_doit()` calls `atom_op_global_set()`).
The insert-cascade PREVIEW/COMMIT model (why text is previewed instead of
replayed) is documented at `mc_ins_span`. Sessions that cannot insert-cascade
(count `3iZ`, Replace mode, blockwise `CTRL-V c`) are captured at <Esc>.
- See `CmdAtomType` and `atom_key_class()` to understand how input is
classified to decide whether and how it can be replayed.
RENDERING
Display is owned by `runtime/lua/vim/_core/mcursor.lua`. the C core maintains
the model, as extmarks:
- "nvim.multicursor": the tracking marks (cursor positions).
- "nvim.multicursor.cursor": selection-END display cursors, during Visual mode
(each cursor displays at its own selection end). Consumers (UIs) that
display cursors use these positions
while any exist, instead of the tracking marks.
- "nvim.multicursor.visual": the pending selection ranges.
GUI SUPPORT
A GUI gets a working display for free, via cell highlights. If it wants to
draw "real" cursors, it is expected to:
- receive positions via the `win_extmark` UI event: the tracking marks are
ui_watched, so visible cursor positions are pushed per-redraw. (Known
issue: win_extmark does not give a removal signal; re-query when in doubt.)
- or query the namespaces above with |nvim_buf_get_extmarks()|; prefer the
"nvim.multicursor.cursor" positions while any exist (in-progress Visual
selection).
- render selections from "nvim.multicursor.visual", or simply keep their
"hl-MultiCursorVisual" grid highlights.
==============================================================================

View File

@@ -343,9 +343,13 @@ out of Insert mode. This is very handy if you prefer to use the Insert mode
all the time, just like editors that don't have a separate Normal mode. You
can use CTRL-O if you want to map a function key to a command.
*ins-repeat*
The changes (inserted or deleted characters) before and after these keys can
be undone separately. Only the last change can be redone and always behaves
like an "i" command.
be undone separately. Relative cursor-moves (the cursor keys, <S-Left>,
<Home>, CTRL-G j, …) are part of the insert: |.| repeats the whole insert,
including cursor-moves. After a jump (mouse, scroll, <PageUp>, …) only the
last change can be redone, and it behaves like an "i" command. The |quote.|
register always holds just the text typed after the last cursor-move.
char action ~
-----------------------------------------------------------------------
@@ -445,6 +449,8 @@ will be repeatable by using |.| to the expected
Lorem ipsum (dolor)
Cursor keys are captured by |.| (|ins-repeat|), so these mappings are only
needed to keep the cursor-move from starting a new undo block.
Using CTRL-O splits undo: the text typed before and after it is undone
separately. If you want to avoid this (e.g., in a mapping) you might be able

View File

@@ -122,6 +122,20 @@ EDITOR
cmdwin via |c_CTRL-F|.
• Behavior of |:restart| changed. Use "!" (|:restart!|) to get the old behavior.
• |ZR| now performs |:restart|. Add a count to change the behavior.
• |ins-repeat| replays the full Insert session, including cursor-moves.
Undo and |quote.| still restart at each cursor-move. Use |i_CTRL-O| to get
the old behavior: >vim
inoremap <Left> <C-o><Left>
inoremap <Right> <C-o><Right>
• |visual-repeat| is "semantic" instead of "fixed-size". See
|visual-fixed-size| if you prefer Vim's "fixed-size" Visual-repeat.
• |Q| adds a |multicursor| instead of replaying the last recorded register.
To get the old behavior: >lua
vim.keymap.set('n', 'Q', function()
local reg = vim.fn.reg_recorded()
return reg == '' and '' or ('@' .. reg)
end, { expr = true })
<
EVENTS
@@ -297,6 +311,8 @@ EVENTS
• |:delmarks| now triggers the |MarkSet| autocommand with line==col==0, same
as |nvim_buf_del_mark()|
• |ChanClose| is triggered after a channel is closed, before it is removed.
• |CmdAtom| fires after each atomic user action (motion, mapping, operator,
insert session, visual, Ex command, …).
• |SessionWritePre| event emits just before |:mksession|.
• |TextPutPre| and |TextPutPost| are triggered before/after putting text.
• |TabMoved| is triggered when tabs are reordered.

View File

@@ -2570,6 +2570,7 @@ A jump table for the options with a short description can be found at |Q_op|.
|ChanClose|,
|ChanInfo|,
|ChanOpen|,
|CmdAtom|,
|CmdUndefined|,
|CmdlineChanged|,
|CmdlineEnter|,

View File

@@ -1,7 +1,7 @@
*repeat.txt* Nvim
VIM REFERENCE MANUAL by Bram Moolenaar
NVIM REFERENCE MANUAL
Repeating commands, Vim scripts and debugging *repeating*
@@ -19,6 +19,9 @@ Single repeats *single-repeat*
included in 'cpoptions'. Does not repeat a
command-line command.
*@:*
@: Repeat last command-line [count] times.
Simple changes can be repeated with the "." command. Without a count, the
count of the last change is used. If you enter a count, it will replace the
last one. |v:count| and |v:count1| will be set.
@@ -27,12 +30,34 @@ If the last change included a specification of a numbered register, the
register number will be incremented. See |redo-register| for an example how
to use this.
Note that when repeating a command that used a Visual selection, the same SIZE
of area is used, see |visual-repeat|.
Repeating an insert |ins-repeat| re-executes the whole session, including
non-jump cursor-moves: "iab<Left>c<Esc>." produces "acb", not "c". Note that
jumps (mouse, |i_<PageUp>|, …) split an insert session.
*@:*
@: Repeat last command-line [count] times.
Repeating a Visual-mode command re-executes the captured keysequence,
selection included. See |visual-repeat|.
*motion-repeat*
Example: a "," mapping that repeats the last motion, like |;| but for any
motion ("3w", "fx", "/pat<CR>"): remember the last "motion" |CmdAtom| and
replay its keys: >lua
local last ---@type string?
vim.api.nvim_create_autocmd('CmdAtom', {
pattern = 'motion',
callback = function(ev)
last = ev.data.keys
end,
})
vim.keymap.set('n', ',', function()
-- CmdAtom is deferred: schedule the replay after any pending event,
-- so `last` is fresh when "," follows a motion.
vim.schedule(function()
if last then
vim.api.nvim_feedkeys(last, 'n', false) -- "n": already resolved
end
end)
end)
<
==============================================================================
Multiple repeats *multi-repeat*
@@ -156,6 +181,9 @@ Q Repeat the last recorded register [count] times.
register for each selected line.
See |visual-repeat|, |default-mappings|.
*v_Q*
{Visual}Q Place a cursor on each line of the Visual selection.
*:@*
:[addr]@{0-9a-z".=*+} Execute the contents of register {0-9a-z".=*+} as an
Ex command. First set cursor at line [addr] (default
@@ -180,6 +208,11 @@ Q Repeat the last recorded register [count] times.
:[addr]@@ Repeat the previous :@{register}. First set cursor at
line [addr] (default is current line).
==============================================================================
Multiple cursors *mcursor* *multicursor*
todo
==============================================================================
Using Vim scripts *using-scripts*

View File

@@ -150,7 +150,6 @@ you never want any default mappings, call |:mapclear| early in your config.
- <C-L> |CTRL-L-default|
- & |&-default|
- - |dir-mappings|
- Q |v_Q-default|
- @ |v_@-default|
- # |v_#-default|
- * |v_star-default|
@@ -232,6 +231,7 @@ New Features *nvim-features*
MAJOR COMPONENTS
- API |API|
- Command atoms |CmdAtom|
- Job control |job-control|
- LSP framework |lsp|
- Lua scripting |lua| |-l|
@@ -335,6 +335,10 @@ Commands:
- |:uptime|
Editor:
- |ins-repeat| replays the full Insert session, including cursor-moves:
"iab<Left>c<Esc>." produces "acb", not "c".
- |visual-repeat| is "semantic" instead of "fixed-size". If you prefer Vim's
"fixed-size" Visual-repeat, see |visual-fixed-size|.
- Interactive |Ex-mode| is implemented as persistent, insert-mode |cmdwin|.
- |prompt-buffer| supports multiline input/paste, undo/redo, and o/O normal
commands.
@@ -402,7 +406,9 @@ Input/Mappings:
Normal commands:
- |gO| shows a filetype-defined "outline" of the current buffer.
- |Q| replays the last recorded macro instead of switching to Ex mode.
- |gQ| restores |multicursor|s.
- |Q| adds a |multicursor| ([count]Q: at each match; {Visual}Q: at each
selected line |v_Q|) instead of switching to Ex mode.
- [count]q: enters |Ex-mode|.
- |ZR| performs |:restart|
- |v_al| selects the whole buffer; |v_il| selects the current line without

View File

@@ -359,17 +359,39 @@ See |v_b_r_example|.
==============================================================================
6. Repeating *visual-repeat*
When repeating a Visual mode operator, the operator will be applied to the
same amount of text as the last time:
- Linewise Visual mode: The same number of lines.
- Blockwise Visual mode: The same number of lines and columns.
- Normal Visual mode within one line: The same number of characters.
- Normal Visual mode with several lines: The same number of lines, in the
last line the same number of characters as in the last line the last time.
The start of the text is the Cursor position. If the "$" command was used as
one of the last commands to extend the highlighted text, the repeating will
be applied up to the rightmost column of the longest line. Any count passed
to the `.` command is not used.
Repeating a Visual operation works "semantically": |.| replays the captured
keysequence (Visual selection plus operator) at the cursor: "viwd" then "."
deletes the word at the cursor, whatever its size. (Note: this is unlike Vim,
where Visual-repeat works on the same fixed-size region. If you prefer that
behavior, see |visual-fixed-size|.)
A [count] given to "." is prepended to the replayed keys. If the selection was
extended by a command that cannot be replayed (mouse, search, Ex command,
scroll), "." instead applies the operator to a same-size area, as if "1v"
(|v|) preceded it.
*visual-fixed-size*
Example: To mimic Vim's fixed-size Visual dot-repeat: after a Visual
operation, reselect the fixed-size region ("1v", |v|) and applies the
operator: >lua
local vop ---@type string?
vim.api.nvim_create_autocmd('CmdAtom', {
---@param ev {data: vim.event.cmdatom.data}
callback = function(ev)
if ev.data.type == 'visual' then
vop = ev.data.atoms and ev.data.atoms[#ev.data.atoms].keys or nil
elseif ev.data.changed then
vop = nil -- the last change is no longer the Visual one
end
end,
})
vim.keymap.set('n', '.', function()
vim.schedule(function()
vim.api.nvim_feedkeys(vop and ('1v' .. vop) or '.', 'n', false)
end)
end)
<
Visual mode |default-mappings| "@" and "Q" repeat a register for all selected
lines if the selection is linewise. See |v_@-default| and |v_Q-default| for

View File

@@ -105,6 +105,7 @@ error('Cannot require a meta file')
--- |'ChanClose'
--- |'ChanInfo'
--- |'ChanOpen'
--- |'CmdAtom'
--- |'CmdUndefined'
--- |'CmdlineChanged'
--- |'CmdlineEnter'

View File

@@ -4,6 +4,23 @@
-- See also `vim.api.keyset.events` in `api_keysets.gen.lua`.
error('Cannot require a meta file')
--- Data for the CmdAtom event.
--- @class vim.event.cmdatom.data
--- @field arg? string Typed operand of `cmd` ("fx" => "x").
--- @field atoms? vim.event.cmdatom.data[] Subatoms of a composite (mapping, Visual sequence).
--- @field cascade boolean Queued for multicursor replay.
--- @field changed boolean Changed the buffer.
--- @field cmd? string Command/motion/object name ("w", "f", "iw", "gJ").
--- @field count? integer Effective count.
--- @field keys string Resolved keysequence, raw bytes: feed to nvim_feedkeys() to replay.
--- @field lhs? string Mapping LHS or macro register ("gj", "@q"). Raw bytes, like `keys`.
--- @field motionforce? 'v'|'V'|'<C-V>' forced-motion type.
--- @field operator? string Operator name ("d", "g~", "g@"). key-notation.
--- @field pending? 'operator'|'visual' Mapping ended mid-operation.
--- @field reg? string Register name.
--- @field text? string Inserted text, or the Ex/search cmdline.
--- @field type 'command'|'ex'|'insert'|'jump'|'mapping'|'motion'|'mouse'|'operator'|'scroll'|'visual'
--- @class vim.event.lspattach.data
--- @field client_id integer

View File

@@ -2193,6 +2193,7 @@ vim.go.ei = vim.go.eventignore
--- `ChanClose`,
--- `ChanInfo`,
--- `ChanOpen`,
--- `CmdAtom`,
--- `CmdUndefined`,
--- `CmdlineChanged`,
--- `CmdlineEnter`,

View File

@@ -31,6 +31,7 @@
#include "nvim/drawline.h"
#include "nvim/drawscreen.h"
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/vars.h"
@@ -45,6 +46,7 @@
#include "nvim/highlight_group.h"
#include "nvim/input.h"
#include "nvim/input_defs.h"
#include "nvim/insert.h"
#include "nvim/insexpand.h"
#include "nvim/keycodes.h"
#include "nvim/log.h"
@@ -1508,7 +1510,7 @@ Dict nvim_get_context(Dict(context) *opts, Arena *arena, Error *err)
types = opts->types;
}
int int_types = types.size > 0 ? 0 : kCtxAll;
CtxStateFlags int_types = types.size > 0 ? 0 : kCtxAll;
if (types.size > 0) {
for (size_t i = 0; i < types.size; i++) {
if (types.items[i].type == kObjectTypeString) {
@@ -1554,7 +1556,7 @@ Object nvim_load_context(Dict dict, Error *err)
ctx_from_dict(dict, &ctx, err);
if (!ERROR_SET(err)) {
ctx_load(&ctx, kCtxAll);
ctx_load(&ctx, kCtxAll, 0);
}
ctx_free(&ctx);

View File

@@ -25,6 +25,7 @@ return {
ChanClose = false,
ChanInfo = false, -- info was received about channel
ChanOpen = false, -- channel was opened
CmdAtom = false, -- after an atomic user operation (motion, operator, insert, mapping, …)
CmdUndefined = false, -- command undefined
CmdlineChanged = false, -- command line was modified
CmdlineEnter = false, -- after entering cmdline mode

View File

@@ -1449,7 +1449,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
static bool filechangeshell_busy = false;
proftime_T wait_time;
bool did_save_redobuff = false;
save_redo_T save_redo;
RedoState save_redo;
const bool save_KeyTyped = KeyTyped;
ESTACK_CHECK_DECLARATION;
CtxSwitch aco = { 0 };
@@ -1578,7 +1578,8 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
} else {
sfname = TO_SLASH_SAVE(fname);
// Don't try expanding the following events.
if (event == EVENT_CMDLINECHANGED
if (event == EVENT_CMDATOM
|| event == EVENT_CMDLINECHANGED
|| event == EVENT_CMDLINEENTER
|| event == EVENT_CMDLINELEAVEPRE
|| event == EVENT_CMDLINELEAVE
@@ -1652,7 +1653,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
if (!autocmd_busy) {
save_search_patterns();
if (!ins_compl_active()) {
saveRedobuff(&save_redo);
save_redobuff(&save_redo);
did_save_redobuff = true;
}
curbuf->b_did_filetype = curbuf->b_keep_filetype;
@@ -1768,7 +1769,7 @@ bool apply_autocmds_group(event_T event, char *fname, char *fname_io, bool force
if (!autocmd_busy) {
restore_search_patterns();
if (did_save_redobuff) {
restoreRedobuff(&save_redo);
restore_redobuff(&save_redo);
}
curbuf->b_did_filetype = false;
while (au_pending_free_buf != NULL) {

View File

@@ -43,15 +43,15 @@
#include "nvim/option_defs.h"
#include "nvim/option_vars.h"
#include "nvim/os/fs.h"
#include "nvim/register.h"
#include "nvim/shada.h"
#include "nvim/state_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"
#include "context.c.generated.h"
int kCtxAll = (kCtxRegs | kCtxJumps | kCtxBufs | kCtxGVars | kCtxSFuncs | kCtxFuncs);
/// Nesting depth of ctx_switch() calls that changed curwin.
static int _ctx_switch_depth = 0;
@@ -74,17 +74,22 @@ void ctx_free(Context *ctx)
api_free_array(ctx->funcs);
}
/// Saves the editor state to a context.
///
/// Use "flags" to select particular types of context.
/// Saves the editor state (ALL THE THINGS!!!1) to a context.
///
/// @param ctx Save to this context.
/// @param flags Flags, see ContextTypeFlags enum.
void ctx_save(Context *ctx, const int flags)
/// @param flags State types to save.
void ctx_save(Context *ctx, const CtxStateFlags flags)
FUNC_ATTR_NONNULL_ALL
{
ctx->buf = curbuf->handle;
ctx->pos = (pos_T) {
.lnum = curwin->w_cursor.lnum,
.col = curwin->w_cursor.col,
.coladd = curwin->w_cursor.coladd,
};
if (flags & kCtxRegs) {
ctx->regs = shada_encode_regs();
ctx->regs = shada_encode_regs(false, 0);
}
if (flags & kCtxJumps) {
@@ -125,32 +130,37 @@ void ctx_save(Context *ctx, const int flags)
}
}
/// Loads (restores) the editor state from a Context snapshot.
///
/// Use "flags" to select particular types of context.
/// Loads (restores) the editor state from a Context snapshot. Restores registers EXACTLY, unless
/// kCtxMergeReg is specified.
///
/// @param ctx Load from this context.
/// @param flags Flags, see ContextTypeFlags enum.
void ctx_load(Context *ctx, const int flags)
/// @param flags State types to load.
/// @param loadflags Controls load behavior.
void ctx_load(Context *ctx, const CtxStateFlags flags, const CtxLoadFlags loadflags)
FUNC_ATTR_NONNULL_ALL
{
Object op_shada = get_option_value(kOptShada, OPT_GLOBAL);
set_option_value(kOptShada, STATIC_CSTR_AS_OBJ("!,'100,%"), OPT_GLOBAL);
// TODO(jkeyes): restore window, mode, pos?
if (flags & kCtxRegs) {
shada_read_string(ctx->regs, kShaDaWantInfo | kShaDaForceit);
if (!(loadflags & kCtxMergeReg)) {
// Avoid shada "merge" behavior for registers; restore "exact", don't merge.
for (int i = 0; i < NUM_SAVED_REGISTERS; i++) {
free_register(get_y_register(i));
}
}
shada_read_string(ctx->regs, kShaDaWantInfo | kShaDaForceit | kShaDaNanos | kShaDaNoHistory);
}
if (flags & kCtxJumps) {
shada_read_string(ctx->jumps, kShaDaWantInfo | kShaDaForceit);
shada_read_string(ctx->jumps, kShaDaWantInfo | kShaDaForceit | kShaDaNoHistory);
}
if (flags & kCtxBufs) {
shada_read_string(ctx->bufs, kShaDaWantInfo | kShaDaForceit);
shada_read_string(ctx->bufs, kShaDaWantInfo | kShaDaForceit | kShaDaNoHistory | kShaDaNoOpt);
}
if (flags & kCtxGVars) {
shada_read_string(ctx->gvars, kShaDaWantInfo | kShaDaForceit);
shada_read_string(ctx->gvars, kShaDaWantInfo | kShaDaForceit | kShaDaNoHistory | kShaDaNoOpt);
}
if (flags & kCtxFuncs) {
@@ -158,9 +168,6 @@ void ctx_load(Context *ctx, const int flags)
do_cmdline_cmd(ctx->funcs.items[i].data.string.data);
}
}
set_option_value(kOptShada, op_shada, OPT_GLOBAL);
optval_free(op_shada);
}
/// Convert readfile()-style array to String
@@ -215,12 +222,12 @@ Dict ctx_to_dict(Context *ctx, Arena *arena)
/// @param[out] err Error object.
///
/// @return types of included context items.
int ctx_from_dict(Dict dict, Context *ctx, Error *err)
CtxStateFlags ctx_from_dict(Dict dict, Context *ctx, Error *err)
FUNC_ATTR_NONNULL_ALL
{
assert(ctx != NULL);
int types = 0;
CtxStateFlags types = 0;
for (size_t i = 0; i < dict.size && !ERROR_SET(err); i++) {
KeyValuePair item = dict.items[i];
if (item.value.type != kObjectTypeArray) {

View File

@@ -1,12 +1,11 @@
#pragma once
#include <stddef.h> // IWYU pragma: keep
#include <stdint.h>
#include "nvim/context_defs.h" // IWYU pragma: export
#include "nvim/macros_defs.h"
extern int kCtxAll;
/// Pool of temporary scratch windows (fka "autocmd windows"), for ctx_switch().
EXTERN kvec_t(CtxWin) ctx_win_vec INIT( = KV_INITIAL_VALUE);
#define ctx_win (ctx_win_vec.items)

View File

@@ -10,15 +10,23 @@
#include "nvim/types_defs.h"
typedef struct {
String regs; ///< Registers.
String jumps; ///< Jumplist.
String bufs; ///< Buffer list.
String gvars; ///< Global variables.
Array funcs; ///< Functions.
pos_T pos; ///< Current cursor position (cache, see `mark`).
uint32_t mark; ///< Extmark id tracking `pos` across buffer edits.
colnr_T curswant; ///< Preferred column ("curswant"); -1 if unset.
handle_T buf; ///< Current buffer handle.
String regs; ///< Registers (shada msgpack string).
String jumps; ///< Jumplist (shada msgpack string).
String bufs; ///< Buffer list (shada msgpack string).
String gvars; ///< Global variables (shada msgpack string).
Array funcs; ///< Functions.
} Context;
typedef kvec_t(Context) ContextVec;
#define CONTEXT_INIT (Context) { \
.pos = { 0 }, \
.mark = 0, \
.curswant = -1, \
.buf = 0, \
.regs = STRING_INIT, \
.jumps = STRING_INIT, \
.bufs = STRING_INIT, \
@@ -33,8 +41,14 @@ typedef enum {
kCtxGVars = 8, ///< Global variables
kCtxSFuncs = 16, ///< Script functions
kCtxFuncs = 32, ///< Functions
kCtxAll = kCtxRegs | kCtxJumps | kCtxBufs | kCtxGVars | kCtxSFuncs | kCtxFuncs,
} CtxStateFlags;
/// "How" to load, orthogonal to "what" (CtxStateFlags).
typedef enum {
kCtxMergeReg = 1, ///< Merge incoming registers with existing.
} CtxLoadFlags;
/// Temporary, hidden window (fka "autocmd window"): a pooled window created to temporarily show
/// a buffer that has no window (ctx_switch() on a buffer target), to handle the side effects. When
/// switches nest we may need more than one.

View File

@@ -260,9 +260,7 @@ int inc_cursor(void)
return inc(&curwin->w_cursor);
}
/// Decrement the line pointer 'p' crossing line boundaries as necessary.
///
/// @return 1 when crossing a line, -1 when at start of file, 0 otherwise.
/// Decrement the cursor position. See dec() for return values.
int dec_cursor(void)
{
return dec(&curwin->w_cursor);

View File

@@ -6778,7 +6778,7 @@ static void f_reltimefloat(typval_T *argvars, typval_T *rettv, EvalFuncData fptr
rettv->v_type = VAR_FLOAT;
rettv->vval.v_float = 0;
if (list2proftime(&argvars[0], &tm) == OK) {
rettv->vval.v_float = (float_T)profile_signed(tm) / 1000000000.0;
rettv->vval.v_float = (float_T)profile_signed(tm) / NS_PER_SEC;
}
}

View File

@@ -1009,7 +1009,7 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, typval_T *rett
proftime_T call_start;
bool started_profiling = false;
bool did_save_redo = false;
save_redo_T save_redo;
RedoState save_redo;
ESTACK_CHECK_DECLARATION;
// If depth of calling is getting too high, don't execute the function
@@ -1023,7 +1023,7 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, typval_T *rett
// Save search patterns and redo buffer.
save_search_patterns();
if (!ins_compl_active()) {
saveRedobuff(&save_redo);
save_redobuff(&save_redo);
did_save_redo = true;
}
fp->uf_calls++;
@@ -1368,7 +1368,7 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, typval_T *rett
}
// restore search patterns and redo buffer
if (did_save_redo) {
restoreRedobuff(&save_redo);
restore_redobuff(&save_redo);
}
restore_search_patterns();
}

View File

@@ -165,7 +165,7 @@ bool loop_close(Loop *loop, bool wait)
if ((uv_loop_close(&loop->uv) != UV_EBUSY) || !wait) {
break;
}
uint64_t elapsed_s = (os_hrtime() - start) / 1000000000; // seconds
uint64_t elapsed_s = (os_hrtime() - start) / NS_PER_SEC;
if (elapsed_s >= 2) {
// Some libuv resource was not correctly deref'd. Log and bail.
rv = false;

View File

@@ -1201,9 +1201,9 @@ void do_bang(int addr_count, exarg_T *eap, bool forceit, bool do_in, bool do_out
// buffername.
char *cmd = vim_strsave_escaped(prevcmd, "%#");
AppendToRedobuffLit(cmd, -1);
redo_append_lit(cmd, -1);
xfree(cmd);
AppendToRedobuff("\n");
redo_append_str(S_LEN("\n"));
bangredo = false;
}
// Add quotes around the command, for shells that need them.

View File

@@ -7075,9 +7075,7 @@ void update_topline_cursor(void)
}
/// Save the current State and go to Normal mode.
///
/// @return true if the typeahead could be saved.
bool save_current_state(save_state_T *sst)
void save_current_state(save_state_T *sst)
FUNC_ATTR_NONNULL_ALL
{
sst->save_msg_scroll = msg_scroll;
@@ -7096,7 +7094,6 @@ bool save_current_state(save_state_T *sst)
// from an event handler and makes sure we don't hang when the argument
// ends with half a command.
save_typeahead(&sst->tabuf);
return sst->tabuf.typebuf_valid;
}
void restore_current_state(save_state_T *sst)
@@ -7185,21 +7182,20 @@ static void ex_normal(exarg_T *eap)
ex_normal_busy++;
save_state_T save_state;
if (save_current_state(&save_state)) {
// Repeat the :normal command for each line in the range. When no
// range given, execute it just once, without positioning the cursor
// first.
do {
if (eap->addr_count != 0) {
curwin->w_cursor.lnum = eap->line1++;
curwin->w_cursor.col = 0;
check_cursor_moved(curwin);
}
save_current_state(&save_state);
// Repeat the :normal command for each line in the range. When no
// range given, execute it just once, without positioning the cursor
// first.
do {
if (eap->addr_count != 0) {
curwin->w_cursor.lnum = eap->line1++;
curwin->w_cursor.col = 0;
check_cursor_moved(curwin);
}
exec_normal_cmd((arg != NULL ? arg : eap->arg),
eap->forceit ? REMAP_NONE : REMAP_YES, false);
} while (eap->addr_count > 0 && eap->line1 <= eap->line2 && !got_int);
}
exec_normal_cmd((arg != NULL ? arg : eap->arg),
eap->forceit ? REMAP_NONE : REMAP_YES, false);
} while (eap->addr_count > 0 && eap->line1 <= eap->line2 && !got_int);
// Might not return to the main loop when in an event handler.
update_topline_cursor();

View File

@@ -48,6 +48,7 @@
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/input.h"
#include "nvim/input_cmdatom.h"
#include "nvim/insert.h"
#include "nvim/keycodes.h"
#include "nvim/lua/executor.h"
@@ -952,6 +953,11 @@ static uint8_t *command_line_enter(int firstc, int count, int indent, bool clear
}
}
if (!s->gotesc && ccline.level == 1) {
// An accepted toplevel cmdline is the executing command's payload (":cnext<CR>", "/pat<CR>").
atom_cmdline_set(s->firstc, ccline.cmdbuff, (size_t)ccline.cmdlen);
}
if (s->gotesc) {
abandon_cmdline();
}

File diff suppressed because it is too large Load Diff

1123
src/nvim/input_cmdatom.c Normal file

File diff suppressed because it is too large Load Diff

30
src/nvim/input_cmdatom.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "nvim/buffer_defs.h" // buf_T
#include "nvim/eval/typval_defs.h" // varnumber_T
#include "nvim/input_cmdatom_defs.h" // IWYU pragma: export
#include "nvim/normal_defs.h" // VisualState, cmdarg_T
#include "nvim/pos_defs.h"
#include "nvim/register_defs.h" // Timestamp
/// 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 {
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).
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;
#include "input_cmdatom.h.generated.h"

View File

@@ -0,0 +1,81 @@
#pragma once
#include <stdbool.h>
#include "klib/kvec.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/input_defs.h"
// Concepts (see :help dev-cmdatom):
// - atom, composite (atom with subatoms)
// - insert-session
// - INSERTION
// - span
// - replay, cascade, insert-cascade
typedef enum CmdAtomType {
kACommand, ///< Not an (operator) edit, nor a cursor-move (u zz <c-w>l …). Never cascades,
///< except as part of a mapping's composite.
kAEx, ///< Ex command (":cnext<CR>"): the typed cmdline is the payload.
kAInsert, ///< Insert session: entry command + text + <Esc>.
kAInsertSpan, ///< Span (chunk) of an ongoing insert-session, cascaded mid-session.
kAJump, ///< Cursor movement by absolute/shared navigation (jumplist, marks, `*`):
///< not followable (its target would collapse cursors onto one position).
kAMapping, ///< Subcommands of a mapping/macro, collapsed into one atom (`lhs`).
kAMotion, ///< Motion (cascades in "q=" follow-motion mode).
kAMouse, ///< Mouse action: emit-only (not replayable).
kAOperator, ///< Operator+motion, or a self-contained edit command.
kAScroll, ///< Scroll (CTRL-Y/D/…, wheel): emit-only, like kAMouse.
kAVisual, ///< Visual-mode sequence ("viwee" + operator).
} CmdAtomType;
/// How an insert-session was entered from Visual mode.
typedef enum {
kVInsNone, ///< Not entered from Visual mode.
kVInsKeys, ///< Redo opens with the selection's captured keys: replayable.
kVInsOther, ///< Redo without the captured keys (Ex/Lua-motion selection, forced
///< motion, or the "1v" fixed-size fallback for a void selection).
} VisualIns;
/// The insert-session delimited by atom_ins_start()/atom_ins_end().
typedef struct {
bool typed; ///< Session is user input (typed, or via mapping/macro).
VisualIns vis; ///< Session was entered from Visual mode.
varnumber_T tick; ///< b:changedtick at start (the session atom's `changed` baseline).
} InsSession;
typedef struct CmdAtom CmdAtom;
typedef kvec_t(CmdAtom) CmdAtomVec;
/// One repeatable operation. `keys` is the replay payload; `spec` is the structured form.
struct CmdAtom {
CmdSpec spec; ///< Structured fields.
CmdAtomVec atoms; ///< Composite (multi-command mapping, Visual sequence): its subatoms,
///< in order; their keys concatenate to `keys`. Empty for non-composite.
char *keys; ///< Resolved keysequence (typeahead encoding).
char *text; ///< Payload: insert-session text, or Ex or search cmdline.
char *lhs; ///< Mapping LHS or macro register ("gj", "@q") that produced this atom, or NULL.
///< Label/hint, not replayed.
CmdAtomType type;
bool changed; ///< The command changed the buffer.
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.
};
/// Key classes (atom_key_class()).
/// Flags, bc same char can mean different things per mode (CTRL-T: tag-jump vs i_CTRL-T indent).
enum {
kKeySynthetic = 1 << 0, ///< Not a user keystroke (K_EVENT, K_IGNORE, K_COMMAND, K_LUA).
kKeyPayload = 1 << 1, ///< Interactively-typed payload (/, ?, :, !).
kKeyScrollMove = 1 << 2, ///< Scroll may move cursor (C-D/…): viewport-dependent, unreplayable.
kKeyScrollView = 1 << 3, ///< Viewport-only scroll (C-Y,wheel): cursor stays, unless 'scrolloff'.
kKeyJump = 1 << 4, ///< Moves to absolute pos from primary cursor's shared nav state
///< (jumplist C-O/I, CTRL-T, "g;"): not followable.
kKeyMotion = 1 << 5, ///< Replayable special-key motion (arrows, <Home>, …).
kKeyInsFlush = 1 << 6, ///< Insert-mode cmd a literal preview cannot represent:
///< - deletions/indent-shifts (<Del>, CTRL-W, …) may edit text
///< outside the tracked region by per-cursor amounts;
///< - cursor-moves (start_arrow()) move the insertion point itself.
kKeyMouse = 1 << 7, ///< Mouse button press (<LeftMouse>, …). Drag/release/move are the
///< press's continuation: no class, invisible to capture.
};

View File

@@ -5,6 +5,7 @@
#include <stdint.h>
#include "nvim/api/private/defs.h"
#include "nvim/types_defs.h"
/// structure used to store one block of the stuff/redo/recording buffers
typedef struct buffblock {
@@ -13,19 +14,51 @@ typedef struct buffblock {
char b_str[1]; ///< contents (actually longer)
} buffblock_T;
/// header used for the stuff buffer and the redo buffer
/// Consumable byte queue (linked list of buffblock_T) of keys in typeahead encoding (K_SPECIAL
/// escaped). Appends (add_buff()) fill the spare space of the last block, allocating a new block
/// when full; reads consume from the front (read_readbuf()).
///
/// Note: Append-only key accumulation (RedoBuf, macro recording) uses StringBuilder instead.
typedef struct {
buffblock_T bh_first; ///< first (dummy) block of list
buffblock_T bh_first; ///< empty sentinel: bh_first.b_next holds the first content
buffblock_T *bh_curr; ///< buffblock for appending
size_t bh_index; ///< index for reading
size_t bh_index; ///< read position in the first block's b_str
size_t bh_space; ///< space in bh_curr for appending
bool bh_create_newblock; ///< create a new block?
} buffheader_T;
#define BUFFHEADER_INIT { { NULL, 0, { NUL } }, NULL, 0, 0, false }
/// Structured decomposition of a normal-mode command, used two ways:
/// - Capture (prep_redo()): the command appends its own bytes to the redo body; only `regname`
/// and `count` are functional (the `["x][count]` prefix), the rest is CmdAtom metadata.
/// - Reconstruction (atom_from_spec()): a command that never preps ("u", motions) has no body, so
/// atom_compose_keys() composes the whole keysequence from the spec.
typedef struct {
buffheader_T sr_redobuff;
buffheader_T sr_old_redobuff;
} save_redo_T;
long count; ///< Effective count (0 = none)
int regname; ///< Register (`"x` prefix; 0 = none)
int op; ///< Operator char ('d'; 0 = none)
int op_extra; ///< Second operator char ("g~" => '~'; 0 = none)
int motion_force; ///< Forced motion type ('v'/'V'/CTRL-V; 0 = none)
int cmd; ///< Command/motion char ('J', 'p', 'f', K_LEFT, …; 0 = none)
int cmd2; ///< Second char of a two-char command name ("gJ" => 'J'; 0 = none)
int arg; ///< Operand ("fx" => 'x', "ma" => 'a'; 0 = none)
} CmdSpec;
/// The last change: structured fields plus the command body, filled as the command executes
/// (redo_append_xx()). "." (start_redo()) composes the `["x][count]` prefix around the body.
typedef struct {
CmdSpec spec; ///< Structured command fields.
StringBuilder keys; ///< Cmd body. Perf: StringBuilder (not buffheader_T) => fewer allocs/copies.
} RedoBuf;
/// RedoBuf (dot-repeat) state: the pending change atom and the previous one.
/// Also the "save" shape for preserving it across user code (save_redobuff()).
typedef struct {
RedoBuf cur; ///< Pending change: "." replays it (start_redo()).
RedoBuf old; ///< Last-but-one change: an insert session's own prep moved the previous change
///< here (redo_new()); "i_CTRL-O ." replays it.
} RedoState;
/// Used for the typeahead buffer: typebuf.
typedef struct {
@@ -40,15 +73,23 @@ typedef struct {
int tb_change_cnt; ///< nr of time tb_buf was changed; never zero
} typebuf_T;
/// Char put back by vungetc() (`c == -1`: none), with the read state restored when it is re-read:
/// a later input event must not leak its mod_mask/mouse coords into the ungot key.
typedef struct {
int c; ///< -1: none
int mod_mask;
int mouse_grid;
int mouse_row;
int mouse_col;
bool stuffed; ///< `KeyStuffed` when ungotten.
} UngotKey;
/// Struct to hold the saved typeahead for save_typeahead().
typedef struct {
typebuf_T save_typebuf;
bool typebuf_valid; ///< true when save_typebuf valid
int old_char;
int old_mod_mask;
UngotKey ungot;
buffheader_T save_readbuf1;
buffheader_T save_readbuf2;
String save_inputbuf;
} tasave_T;
/// Values for "noremap" argument of ins_typebuf()

View File

@@ -218,16 +218,17 @@ static void insert_enter(InsertState *s)
}
if (s->cmdchar != NUL && restart_edit == 0) {
ResetRedobuff();
AppendNumberToRedobuff(s->count);
if (s->cmdchar == 'V' || s->cmdchar == 'v') {
// "gR" or "gr" command
AppendCharToRedobuff('g');
AppendCharToRedobuff((s->cmdchar == 'v') ? 'r' : 'R');
redo_new((CmdSpec){ .count = s->count, .cmd = 'g', .cmd2 = (s->cmdchar == 'v') ? 'r' : 'R' });
redo_append_char('g');
redo_append_char((s->cmdchar == 'v') ? 'r' : 'R');
} else {
AppendCharToRedobuff(s->cmdchar);
redo_new((CmdSpec){ .count = s->count, .cmd = s->cmdchar,
.cmd2 = (s->cmdchar == 'g') ? 'I' : NUL });
redo_append_char(s->cmdchar);
if (s->cmdchar == 'g') { // "gI" command
AppendCharToRedobuff('I');
redo_append_char('I');
} else if (s->cmdchar == 'r') { // "r<CR>" command
s->count = 1; // insert only one <CR>
}
@@ -279,7 +280,7 @@ static void insert_enter(InsertState *s)
if (restart_edit != 0 && stuff_empty()) {
// After a paste we consider text typed to be part of the insert for
// the pasted text. You can backspace over the pasted text too.
Ins.arrow_used = where_paste_started.lnum == 0;
Ins.moved = where_paste_started.lnum == 0 ? kInsJump : kInsNone;
restart_edit = 0;
// If the cursor was after the end-of-line before the CTRL-O and it is
@@ -304,7 +305,7 @@ static void insert_enter(InsertState *s)
}
ins_at_eol = false;
} else {
Ins.arrow_used = false;
Ins.moved = kInsNone;
}
// we are in insert mode now, don't need to start it anymore
@@ -337,10 +338,10 @@ static void insert_enter(InsertState *s)
// Get the current length of the redo buffer, those characters have to be
// skipped if we want to get to the inserted characters.
String inserted = get_inserted();
Ins.new_insert_skip = (int)inserted.size;
if (inserted.data != NULL) {
xfree(inserted.data);
String redo = redo_keys();
Ins.new_insert_skip = (int)redo.size;
if (redo.data != NULL) {
xfree(redo.data);
}
old_indent = 0;
@@ -389,7 +390,7 @@ static int insert_check(VimState *state)
Ins.revins_legal = 0;
}
if (Ins.arrow_used) { // don't repeat insert when arrow key used
if (Ins.moved != kInsNone) { // don't repeat insert when arrow key used
s->count = 0;
}
@@ -411,7 +412,7 @@ static int insert_check(VimState *state)
}
// set curwin->w_curswant for next K_DOWN or K_UP
if (!Ins.arrow_used) {
if (Ins.moved == kInsNone) {
curwin->w_set_curswant = true;
}
@@ -726,8 +727,6 @@ static int insert_execute(VimState *state, int key)
static int insert_handle_key(InsertState *s)
{
// The big switch to handle a character in insert mode.
// TODO(tarruda): This could look better if a lookup table is used.
// (similar to normal mode `nv_cmds[]`)
switch (s->c) {
case ESC: // End input mode
if (echeck_abbr(ESC + ABBR_OFF)) {
@@ -1227,7 +1226,7 @@ normalchar:
ins_char(s->c);
}
}
AppendToRedobuffLit(str, -1);
redo_append_lit(str, -1);
}
xfree(str);
s->c = NUL;
@@ -1324,7 +1323,7 @@ static void insert_handle_key_post(InsertState *s)
}
// If the cursor was moved we didn't just insert a space
if (Ins.arrow_used) {
if (Ins.moved != kInsNone) {
s->inserted_space = false;
}
@@ -1395,11 +1394,6 @@ bool edit(int cmdchar, bool startln, int count)
return s->c == Ctrl_O;
}
bool ins_need_undo_get(void)
{
return Ins.need_undo;
}
/// Redraw for Insert mode.
/// This is postponed until getting the next character to make '$' in the 'cpo'
/// option work correctly.
@@ -1505,7 +1499,7 @@ static void ins_ctrl_v(void)
edit_putchar('^', true);
did_putchar = true;
}
AppendToRedobuff(CTRL_V_STR);
redo_append_str(S_LEN(CTRL_V_STR));
add_to_showcmd_c(Ctrl_V);
@@ -1596,7 +1590,7 @@ char *prompt_text(void)
return buf_prompt_text(curbuf);
}
// Prepare for prompt mode: Make sure the last line has the prompt text.
// Prepare for prompt mode (buftype=prompt): Make sure the last line has the prompt text.
// Move the cursor to this line.
static void init_prompt(int cmdchar_todo)
{
@@ -1674,7 +1668,7 @@ static void set_insstart(linenr_T lnum, colnr_T col)
Ins.start_orig = Ins.start;
Ins.start_textlen = Ins.start.col;
Ins.start_blank_vcol = MAXCOL;
Ins.arrow_used = false;
Ins.moved = kInsNone;
}
// Undo the previous edit_putchar().
@@ -1922,7 +1916,7 @@ static void insert_special(int c, int allow_modmask, int ctrlv)
}
p[len - 1] = NUL;
ins_str(p, (size_t)(len - 1));
AppendToRedobuffLit(p, -1);
redo_append_lit(p, -1);
ctrlv = false;
}
}
@@ -2112,7 +2106,7 @@ void insertchar(int c, int flags, int second_indent)
i = 0;
}
if (buf[i] != NUL) {
AppendToRedobuffLit(buf + i, -1);
redo_append_lit(buf + i, -1);
}
} else {
int cc;
@@ -2123,13 +2117,13 @@ void insertchar(int c, int flags, int second_indent)
utf_char2bytes(c, buf);
buf[cc] = NUL;
ins_char_bytes(buf, (size_t)cc);
AppendCharToRedobuff(c);
redo_append_char(c);
} else {
ins_char(c);
if (flags & INSCHAR_CTRLV) {
redo_literal(c);
} else {
AppendCharToRedobuff(c);
redo_append_char(c);
}
}
}
@@ -2144,47 +2138,62 @@ static void redo_literal(int c)
// three digits.
if (ascii_isdigit(c)) {
vim_snprintf(buf, sizeof(buf), "%03d", c);
AppendToRedobuff(buf);
redo_append_str(buf, -1);
} else {
AppendCharToRedobuff(c);
redo_append_char(c);
}
}
/// start_arrow() is called when an arrow key is used in insert mode.
/// Called when the cursor moves in insert-mode without editing text (arrow keys, <Home>, mouse).
/// For undo/redo it resembles hitting the <ESC> key.
///
/// @param end_insert_pos can be NULL
void start_arrow(pos_T *end_insert_pos)
/// @param end_insert_pos Can be NULL
/// @param end_change End undoable change
/// @param move_key Cursor-move key, captured in the CmdAtom (replayed at mc-cascade).
/// NUL for non-captured jumps (mouse, <PageUp>, <C-Home>); then the next
/// edit restarts a new insert-session (stop_arrow()).
void start_arrow(pos_T *end_insert_pos, bool end_change, int move_key)
{
start_arrow_common(end_insert_pos, true);
}
/// Like start_arrow() but with end_change argument.
/// Will prepare for redo of CTRL-G U if "end_change" is false.
///
/// @param end_insert_pos can be NULL
/// @param end_change end undoable change
static void start_arrow_with_change(pos_T *end_insert_pos, bool end_change)
{
start_arrow_common(end_insert_pos, end_change);
if (!end_change) {
AppendCharToRedobuff(Ctrl_G);
AppendCharToRedobuff('U');
if (Ins.moved == kInsNone && end_change) { // something has been inserted
stop_insert(end_insert_pos, false, false); // this stops the current insert
}
}
/// @param end_insert_pos can be NULL
/// @param end_change end undoable change
static void start_arrow_common(pos_T *end_insert_pos, bool end_change)
{
if (!Ins.arrow_used && end_change) { // something has been inserted
AppendToRedobuff(ESC_STR);
stop_insert(end_insert_pos, false, false);
Ins.arrow_used = true; // This means we stopped the current insert.
if (Ins.moved != kInsJump) {
if (move_key == NUL) {
redo_append_str(S_LEN(ESC_STR));
Ins.moved = kInsJump;
} else {
if (!IS_SPECIAL(move_key)) {
// CTRL-G j/k: the only captured cursor-moves typed as plain chars.
redo_append_char(Ctrl_G);
}
redo_append_char(move_key);
if (end_change) {
Ins.moved = kInsArrow;
}
}
}
check_spell_redraw();
}
/// Like start_arrow(); if `end_change` is false (CTRL-G U pending), also captures the CTRL-G U
/// keys in the atom, ahead of "move_key": replay re-executes the whole "<C-g>U<Left>".
///
/// (Capturing them here, before start_arrow(), cannot corrupt stop_insert()'s last_insert
/// harvest: they append only when "end_change" is false, and stop_insert() runs only when it
/// is true.)
///
/// @param end_insert_pos Can be NULL
/// @param end_change End undoable change
/// @param move_key @see start_arrow()
static void start_arrow_with_change(pos_T *end_insert_pos, bool end_change, int move_key)
{
if (!end_change && Ins.moved != kInsJump) {
redo_append_char(Ctrl_G);
redo_append_char('U');
}
start_arrow(end_insert_pos, end_change, move_key);
}
// If we skipped highlighting word at cursor, do it now.
// It may be skipped again, thus reset spell_redraw_lnum first.
static void check_spell_redraw(void)
@@ -2198,11 +2207,12 @@ static void check_spell_redraw(void)
}
// stop_arrow() is called before a change is made in insert mode.
// If an arrow key has been used, start a new insertion.
//
// After a cursor-move (`Ins.moved`), the change starts a new INSERTION (see `Ins.start`).
// Returns FAIL if undo is impossible, shouldn't insert then.
int stop_arrow(void)
{
if (Ins.arrow_used) {
if (Ins.moved != kInsNone) {
Ins.start = curwin->w_cursor; // new insertion starts here
if (Ins.start.col > Ins.start_orig.col && !Ins.need_undo) {
// Don't update the original insert position when moved to the
@@ -2211,8 +2221,9 @@ int stop_arrow(void)
}
Ins.start_textlen = linetabsize_str(get_cursor_line_ptr());
const bool jumped = Ins.moved == kInsJump;
if (u_save_cursor() == OK) {
Ins.arrow_used = false;
Ins.moved = kInsNone;
Ins.need_undo = false;
}
Ins.ai_col = 0;
@@ -2220,9 +2231,19 @@ int stop_arrow(void)
orig_line_count = curbuf->b_ml.ml_line_count;
vr_lines_changed = 1;
}
ResetRedobuff();
AppendToRedobuff("1i"); // Pretend we start an insertion.
Ins.new_insert_skip = 2;
if (jumped) {
// Non-captured cursor-move (mouse, <PageUp>, …): restart the capture as a "1i" insertion.
// The count is a spec field (not body bytes), so "[count]." replaces it ("3i…").
redo_new((CmdSpec){ .count = 1, .cmd = 'i' });
redo_append_char('i');
Ins.new_insert_skip = 2;
} else {
// Cursor-move was captured (start_arrow()): the atom mc-cascade will replay it.
// Only `last_insert` (the ". register, i_CTRL-A) restarts here, like Vim.
String redo = redo_keys();
Ins.new_insert_skip = (int)redo.size;
xfree(redo.data);
}
} else if (Ins.need_undo) {
if (u_save_cursor() == OK) {
// A command or event may have moved the cursor before the next
@@ -2241,7 +2262,7 @@ int stop_arrow(void)
// Always open fold at the cursor line when inserting something.
foldOpenCursor();
return Ins.arrow_used || Ins.need_undo ? FAIL : OK;
return Ins.moved != kInsNone || Ins.need_undo ? FAIL : OK;
}
/// Do a few things to stop inserting.
@@ -2258,17 +2279,17 @@ static void stop_insert(pos_T *end_insert_pos, int esc, int nomove)
// Save the inserted text for later redo with ^@ and CTRL-A.
// Don't do it when "restart_edit" was set and nothing was inserted,
// otherwise CTRL-O w and then <Left> will clear "last_insert".
String inserted = get_inserted();
int added = inserted.data == NULL ? 0 : (int)inserted.size - Ins.new_insert_skip;
String redo = redo_keys();
int added = redo.data == NULL ? 0 : (int)redo.size - Ins.new_insert_skip;
if (Ins.did_restart_edit == 0 || added > 0) {
xfree(last_insert.data);
last_insert = inserted; // structure copy
last_insert = redo; // structure copy
last_insert_skip = added < 0 ? 0 : Ins.new_insert_skip;
} else {
xfree(inserted.data);
xfree(redo.data);
}
if (!Ins.arrow_used && end_insert_pos != NULL) {
if (Ins.moved == kInsNone && end_insert_pos != NULL) {
int cc;
// Auto-format now. It may seem strange to do this when stopping an
// insertion (or moving the cursor), but it's required when appending
@@ -2313,9 +2334,9 @@ static void stop_insert(pos_T *end_insert_pos, int esc, int nomove)
// Do this when ESC was used or moving the cursor up/down.
// Check for the old position still being valid, just in case the text
// got changed unexpectedly.
if (!nomove && Ins.did_ai && (esc || (vim_strchr(p_cpo, kCpoIndent) == NULL
&& curwin->w_cursor.lnum !=
end_insert_pos->lnum))
if (!nomove && Ins.did_ai
&& (esc || (vim_strchr(p_cpo, kCpoIndent) == NULL
&& curwin->w_cursor.lnum != end_insert_pos->lnum))
&& end_insert_pos->lnum <= curbuf->b_ml.ml_line_count) {
pos_T tpos = curwin->w_cursor;
colnr_T prev_col = end_insert_pos->col;
@@ -2735,7 +2756,7 @@ static bool echeck_abbr(int c)
{
// Don't check for abbreviation in paste mode, when disabled and just
// after moving around with cursor keys.
if (p_paste || no_abbr || Ins.arrow_used) {
if (p_paste || no_abbr || Ins.moved != kInsNone) {
return false;
}
@@ -2956,15 +2977,15 @@ static void ins_reg(void)
if (literally == Ctrl_O || literally == Ctrl_P) {
// Append the command to the redo buffer.
AppendCharToRedobuff(Ctrl_R);
AppendCharToRedobuff(literally);
AppendCharToRedobuff(regname);
redo_append_char(Ctrl_R);
redo_append_char(literally);
redo_append_char(regname);
do_put(regname, NULL, BACKWARD, 1,
(literally == Ctrl_P ? PUT_FIXINDENT : 0) | PUT_CURSEND);
} else if (reg->y_size > 1 && is_literal_register(regname)) {
AppendCharToRedobuff(Ctrl_R);
AppendCharToRedobuff(regname);
redo_append_char(Ctrl_R);
redo_append_char(regname);
do_put(regname, NULL, BACKWARD, 1, PUT_CURSEND);
} else if (insert_reg(regname, NULL, !!literally) == FAIL) {
vim_beep(kOptBoFlagRegister);
@@ -3089,10 +3110,10 @@ static bool ins_esc(int *count, int cmdchar, bool nomove)
RedrawingDisabled--;
disabled_redraw = false;
}
if (!Ins.arrow_used) {
if (Ins.moved == kInsNone) {
// Don't append the ESC for "r<CR>" and "grx".
if (cmdchar != 'r' && cmdchar != 'v') {
AppendToRedobuff(ESC_STR);
redo_append_str(S_LEN(ESC_STR));
}
// Repeating insert may take a long time. Check for
@@ -3121,6 +3142,10 @@ static bool ins_esc(int *count, int cmdchar, bool nomove)
}
stop_insert(&curwin->w_cursor, true, nomove);
undisplay_dollar();
} else if (Ins.moved == kInsArrow && cmdchar != 'r' && cmdchar != 'v') {
// The session ended just after a cursor-move: close the atom, so the whole session (including
// cursor-move) replays.
redo_append_str(S_LEN(ESC_STR));
}
if (cmdchar != 'r' && cmdchar != 'v') {
@@ -3261,7 +3286,7 @@ static void ins_insert(int replaceState)
State = replaceState | (State & MODE_LANGMAP);
}
may_trigger_modechanged();
AppendCharToRedobuff(K_INS);
redo_append_char(K_INS);
showmode();
ui_cursor_shape(); // may show different cursor shape
}
@@ -3294,7 +3319,7 @@ static void ins_shift(int c, int lastc)
if (stop_arrow() == FAIL) {
return;
}
AppendCharToRedobuff(c);
redo_append_char(c);
// 0^D and ^^D: remove all indent.
if (c == Ctrl_D && (lastc == '0' || lastc == '^')
@@ -3349,7 +3374,7 @@ static void ins_del(void)
Ins.did_si = false;
Ins.can_si = false;
Ins.can_si_back = false;
AppendCharToRedobuff(K_DEL);
redo_append_char(K_DEL);
}
/// Handle Backspace, delete-word and delete-line in Insert mode.
@@ -3376,10 +3401,10 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
|| (!Ins.revins_on
&& ((curwin->w_cursor.lnum == 1 && curwin->w_cursor.col == 0)
|| (!can_bs(BS_START)
&& ((Ins.arrow_used && !bt_prompt(curbuf))
&& ((Ins.moved != kInsNone && !bt_prompt(curbuf))
|| (curwin->w_cursor.lnum == Ins.start_orig.lnum
&& curwin->w_cursor.col <= Ins.start_orig.col)))
|| (!can_bs(BS_INDENT) && !Ins.arrow_used && Ins.ai_col > 0
|| (!can_bs(BS_INDENT) && Ins.moved == kInsNone && Ins.ai_col > 0
&& curwin->w_cursor.col <= Ins.ai_col)
|| (!can_bs(BS_EOL) && curwin->w_cursor.col == 0)))) {
vim_beep(kOptBoFlagBackspace);
@@ -3518,7 +3543,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
&& curwin->w_cursor.col > 0
&& (*(get_cursor_pos_ptr() - 1) == TAB
|| (*(get_cursor_pos_ptr() - 1) == ' '
&& (!*inserted_space_p || Ins.arrow_used)))))) {
&& (!*inserted_space_p || Ins.moved != kInsNone)))))) {
*inserted_space_p = false;
bool const use_ts = !curwin->w_p_list || curwin->w_p_lcs_chars.tab1;
@@ -3669,7 +3694,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p)
// It's a little strange to put backspaces into the redo
// buffer, but it makes auto-indent a lot easier to deal
// with.
AppendCharToRedobuff(c);
redo_append_char(c);
// If deleted before the insertion point, adjust it
if (curwin->w_cursor.lnum == Ins.start_orig.lnum
@@ -3707,10 +3732,7 @@ static void ins_left(void)
undisplay_dollar();
pos_T tpos = curwin->w_cursor;
if (oneleft() == OK) {
start_arrow_with_change(&tpos, end_change);
if (!end_change) {
AppendCharToRedobuff(K_LEFT);
}
start_arrow_with_change(&tpos, end_change, K_LEFT);
// If exit reversed string, position is fixed
if (Ins.revins_scol != -1 && (int)curwin->w_cursor.col >= Ins.revins_scol) {
Ins.revins_legal++;
@@ -3719,7 +3741,7 @@ static void ins_left(void)
} else if (vim_strchr(p_ww, '[') != NULL && curwin->w_cursor.lnum > 1) {
// if 'whichwrap' set for cursor in insert mode may go to previous line.
// always break undo when moving upwards/downwards, else undo may break
start_arrow(&tpos);
start_arrow(&tpos, true, K_LEFT);
curwin->w_cursor.lnum--;
coladvance(curwin, MAXCOL);
curwin->w_set_curswant = true; // so we stay at the end
@@ -3742,7 +3764,7 @@ static void ins_home(int c)
curwin->w_cursor.col = 0;
curwin->w_cursor.coladd = 0;
curwin->w_curswant = 0;
start_arrow(&tpos);
start_arrow(&tpos, true, c == K_C_HOME ? NUL : K_HOME);
}
static void ins_end(int c)
@@ -3758,7 +3780,7 @@ static void ins_end(int c)
coladvance(curwin, MAXCOL);
curwin->w_curswant = MAXCOL;
start_arrow(&tpos);
start_arrow(&tpos, true, c == K_C_END ? NUL : K_END);
}
static void ins_s_left(void)
@@ -3769,10 +3791,7 @@ static void ins_s_left(void)
}
undisplay_dollar();
if (curwin->w_cursor.lnum > 1 || curwin->w_cursor.col > 0) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_S_LEFT);
}
start_arrow_with_change(&curwin->w_cursor, end_change, K_S_LEFT);
bck_word(1, false, false);
curwin->w_set_curswant = true;
} else {
@@ -3790,10 +3809,7 @@ static void ins_right(void)
}
undisplay_dollar();
if (gchar_cursor() != NUL || virtual_active(curwin)) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_RIGHT);
}
start_arrow_with_change(&curwin->w_cursor, end_change, K_RIGHT);
curwin->w_set_curswant = true;
if (virtual_active(curwin)) {
oneright();
@@ -3809,7 +3825,7 @@ static void ins_right(void)
&& curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count) {
// if 'whichwrap' set for cursor in insert mode, may move the
// cursor to the next line
start_arrow(&curwin->w_cursor);
start_arrow(&curwin->w_cursor, true, K_RIGHT);
curwin->w_set_curswant = true;
curwin->w_cursor.lnum++;
curwin->w_cursor.col = 0;
@@ -3828,10 +3844,7 @@ static void ins_s_right(void)
undisplay_dollar();
if (curwin->w_cursor.lnum < curbuf->b_ml.ml_line_count
|| gchar_cursor() != NUL) {
start_arrow_with_change(&curwin->w_cursor, end_change);
if (!end_change) {
AppendCharToRedobuff(K_S_RIGHT);
}
start_arrow_with_change(&curwin->w_cursor, end_change, K_S_RIGHT);
fwd_word(1, false, 0);
curwin->w_set_curswant = true;
} else {
@@ -3856,7 +3869,7 @@ static void ins_up(bool startcol)
|| old_topfill != curwin->w_topfill) {
redraw_later(curwin, UPD_VALID);
}
start_arrow(&tpos);
start_arrow(&tpos, true, startcol ? 'k' : K_UP);
Ins.can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
@@ -3870,7 +3883,7 @@ static void ins_pageup(void)
if (mod_mask & MOD_MASK_CTRL) {
// <C-PageUp>: tab page back
if (first_tabpage->tp_next != NULL) {
start_arrow(&curwin->w_cursor);
start_arrow(&curwin->w_cursor, true, NUL);
goto_tabpage(-1);
}
return;
@@ -3878,7 +3891,7 @@ static void ins_pageup(void)
pos_T tpos = curwin->w_cursor;
if (pagescroll(BACKWARD, 1, false) == OK) {
start_arrow(&tpos);
start_arrow(&tpos, true, NUL);
Ins.can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
@@ -3901,7 +3914,7 @@ static void ins_down(bool startcol)
|| old_topfill != curwin->w_topfill) {
redraw_later(curwin, UPD_VALID);
}
start_arrow(&tpos);
start_arrow(&tpos, true, startcol ? 'j' : K_DOWN);
Ins.can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
@@ -3915,7 +3928,7 @@ static void ins_pagedown(void)
if (mod_mask & MOD_MASK_CTRL) {
// <C-PageDown>: tab page forward
if (first_tabpage->tp_next != NULL) {
start_arrow(&curwin->w_cursor);
start_arrow(&curwin->w_cursor, true, NUL);
goto_tabpage(0);
}
return;
@@ -3923,7 +3936,7 @@ static void ins_pagedown(void)
pos_T tpos = curwin->w_cursor;
if (pagescroll(FORWARD, 1, false) == OK) {
start_arrow(&tpos);
start_arrow(&tpos, true, NUL);
Ins.can_cindent = true;
} else {
vim_beep(kOptBoFlagCursor);
@@ -3974,7 +3987,7 @@ static bool ins_tab(void)
Ins.did_si = false;
Ins.can_si = false;
Ins.can_si_back = false;
AppendToRedobuff("\t");
redo_append_str(S_LEN("\t"));
if (p_sta && ind) { // insert tab in indent, use 'shiftwidth'
temp = get_sw_value(curbuf);
@@ -4189,7 +4202,7 @@ bool ins_eol(int c)
curwin->w_cursor.col += get_cursor_pos_len();
}
AppendToRedobuff(NL_STR);
redo_append_str(S_LEN(NL_STR));
bool i = open_line(FORWARD,
has_format_option(kFoRetComs) ? OPENLINE_DO_COM : 0,
old_indent, NULL);
@@ -4260,7 +4273,7 @@ static int ins_digraph(void)
edit_unputchar();
}
if (cc != ESC) {
AppendToRedobuff(CTRL_V_STR);
redo_append_str(S_LEN(CTRL_V_STR));
c = digraph_get(c, cc, true);
clear_showcmd();
return c;
@@ -4323,7 +4336,7 @@ static int ins_ctrl_ey(int tc)
// wasn't set. Digits, 'o' and 'x' are special after a
// CTRL-V, don't use it for these.
if (c < 256 && !isalnum(c)) {
AppendToRedobuff(CTRL_V_STR);
redo_append_str(S_LEN(CTRL_V_STR));
}
OptInt tw_save = curbuf->b_p_tw;
curbuf->b_p_tw = -1;

View File

@@ -5,23 +5,28 @@
#include "nvim/pos_defs.h"
#include "nvim/types_defs.h"
/// Change-delimiting cursor-move in insert-mode (start_arrow()): undo, Ins.start and last_insert
/// (the ". register) restart at next edit (stop_arrow()); typed keys sync undo (may_sync_undo()).
typedef enum {
kInsNone = 0, ///< No cursor-move since last edit: contiguous insert-session.
kInsArrow, ///< Cursor-move captured in the atom.
kInsJump, ///< Non-replayable jump (mouse, <PageUp>, …): atom terminated (<Esc>).
} InsArrow;
/// Insert-mode session state: the in-progress insert session, as one global "group" (Ins), so the
/// insert session can be saved/restored as a whole around nested edit() mini-sessions (e.g.
/// a future multicursor live-mirror).
/// insert session can be saved/restored as a whole around nested edit() sessions.
typedef struct {
pos_T start; ///< Where the latest insert/append mode started
pos_T start_orig; ///< Where the latest insert/append mode started. In contrast to
///< "start", this won't be reset by certain keys and is needed for
///< op_insert(), to detect correctly where inserting by the user
///< started.
pos_T start; ///< Start of current INSERTION: from here to cursor is the unit that
///< undo, '[ and ". treat as one. Reanchored after cursor-move, C-G u.
pos_T start_orig; ///< Where insert by the user started (for op_insert): follows `start`
///< until insertion starts right of it; <bs> before it pulls back.
colnr_T start_textlen; ///< length of line when insert started
colnr_T start_blank_vcol; ///< vcol for first inserted blank
bool arrow_used; ///< Normally false, set to true after hitting a cursor key in insert
///< mode. Used by vgetorpeek() to decide when to call u_sync().
InsArrow moved; ///< Cursor moved since the last edit; see InsArrow.
bool stop_insert_mode; ///< for ":stopinsert"
bool can_cindent; ///< may do cindenting on this line
bool need_undo; ///< call u_save() before inserting a char. Set when edit() is
///< called; after that arrow_used is used.
///< called; after that `moved` is used.
bool did_ai; ///< Makes auto-indent work right on lines where only a <CR> or
///< <Esc> is typed: set when an auto-indent is done, reset when any
///< other editing is done on the line. If an <Esc> or <CR> is

View File

@@ -2781,7 +2781,7 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval)
}
// only format when something was inserted
if (!Ins.arrow_used && !ins_need_undo_get() && c != Ctrl_E) {
if (Ins.moved == kInsNone && !Ins.need_undo && c != Ctrl_E) {
insertchar(NUL, 0, -1);
}
@@ -3012,10 +3012,10 @@ static void ins_compl_fixRedoBufForLeader(char *ptr_arg)
}
// Add backspace characters for each remaining character in original text
for (p += len; *p != NUL; MB_PTR_ADV(p)) {
AppendCharToRedobuff(K_BS);
redo_append_char(K_BS);
}
}
AppendToRedobuffLit(ptr + len, -1);
redo_append_lit(ptr + len, -1);
}
/// Loops through the list of windows, loaded-buffers or non-loaded-buffers
@@ -6494,7 +6494,7 @@ static void spell_back_to_badword(void)
pos_T tpos = curwin->w_cursor;
spell_bad_len = spell_move_to(curwin, BACKWARD, SMT_ALL, true, NULL);
if (curwin->w_cursor.col != tpos.col) {
start_arrow(&tpos);
start_arrow(&tpos, true, NUL);
}
}

View File

@@ -4225,6 +4225,12 @@ int incl(pos_T *lp)
return r;
}
/// Decrements position `lp` by one character (multibyte-aware), crossing line boundaries as
/// necessary. Resets `lp.coladd`. Uses curbuf.
///
/// @return 1 when crossing to the previous line.
/// -1 when at the start of the file.
/// 0 otherwise.
int dec(pos_T *lp)
{
lp->coladd = 0;

View File

@@ -25,6 +25,7 @@
#include "nvim/globals.h"
#include "nvim/highlight.h"
#include "nvim/highlight_group.h"
#include "nvim/input_cmdatom.h"
#include "nvim/insexpand.h"
#include "nvim/lua/executor.h"
#include "nvim/main.h"
@@ -33,6 +34,7 @@
#include "nvim/memfile.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/normal.h"
#include "nvim/option_vars.h"
#include "nvim/sign.h"
#include "nvim/state_defs.h"
@@ -964,8 +966,7 @@ void free_all_mem(void)
// Clear registers.
clear_registers();
ResetRedobuff();
ResetRedobuff();
redo_free_all();
// highlight info
free_highlight();
@@ -983,6 +984,7 @@ void free_all_mem(void)
channel_free_all_mem();
eval_clear();
api_extmark_free_all_mem();
atom_free_all();
map_destroy(int, &buffer_handles);
map_destroy(int, &window_handles);

View File

@@ -1538,10 +1538,8 @@ void execute_menu(const exarg_T *eap, vimmenu_T *menu, int mode_idx)
save_state_T save_state;
ex_normal_busy++;
if (save_current_state(&save_state)) {
exec_normal_cmd(menu->strings[idx], menu->noremap[idx],
menu->silent[idx]);
}
save_current_state(&save_state);
exec_normal_cmd(menu->strings[idx], menu->noremap[idx], menu->silent[idx]);
restore_current_state(&save_state);
ex_normal_busy--;
} else {

View File

@@ -1552,7 +1552,7 @@ void wait_return(int redraw)
} else if (vim_strchr("\r\n ", c) == NULL && c != Ctrl_C && c != 'q') {
// Put the character back in the typeahead buffer. Don't use the
// stuff buffer, because lmaps wouldn't work.
requeue_key(vgetc_char, vgetc_mod_mask,
requeue_key(vgetc_char, vgetc_mod_mask, 0,
// Recording was suppressed around safe_vgetc() above.
false);
do_redraw = true; // need a redraw even though there is typeahead

View File

@@ -22,6 +22,7 @@
#include "nvim/grid.h"
#include "nvim/grid_defs.h"
#include "nvim/input.h"
#include "nvim/input_cmdatom.h"
#include "nvim/insert.h"
#include "nvim/keycodes.h"
#include "nvim/macros_defs.h"
@@ -518,9 +519,9 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
(fixindent ? PUT_FIXINDENT : 0) | PUT_CURSEND);
// Repeat it with CTRL-R CTRL-O r or CTRL-R CTRL-P r
AppendCharToRedobuff(Ctrl_R);
AppendCharToRedobuff(fixindent ? Ctrl_P : Ctrl_O);
AppendCharToRedobuff(regname == 0 ? '"' : regname);
redo_append_char(Ctrl_R);
redo_append_char(fixindent ? Ctrl_P : Ctrl_O);
redo_append_char(regname == 0 ? '"' : regname);
}
}
return false;
@@ -865,7 +866,8 @@ bool do_mouse(oparg_T *oap, int c, int dir, int count, bool fixindent)
c1 = (dir == FORWARD) ? 'p' : 'P';
c2 = NUL;
}
prep_redo(regname, count, NUL, c1, NUL, c2, NUL);
prep_redo(NULL, 0, false,
(CmdSpec){ .regname = regname, .count = count, .cmd = c1, .cmd2 = c2 });
// Remember where the paste started, so in edit() Ins.start can be set to this position
if (restart_edit != 0) {
@@ -1023,7 +1025,7 @@ void ins_mouse(int c)
curbuf->b_prompt_insert = 'A';
}
}
start_arrow(curwin == old_curwin ? &tpos : NULL);
start_arrow(curwin == old_curwin ? &tpos : NULL, true, NUL);
if (curwin != new_curwin && win_valid(new_curwin)) {
curwin = new_curwin;
curbuf = curwin->w_buffer;
@@ -1137,7 +1139,7 @@ void ins_mousescroll(int dir)
curbuf = curwin->w_buffer;
if (!equalpos(curwin->w_cursor, orig_cursor)) {
start_arrow(&orig_cursor);
start_arrow(&orig_cursor, true, NUL);
set_can_cindent(true);
}
}

View File

@@ -548,6 +548,7 @@ void rpc_free(Channel *channel)
/// Closes a channel after receiving fatal error, and logs a message.
static void chan_close_on_err(Channel *channel, char *msg, int loglevel)
{
logmsg(loglevel, "RPC: ", NULL, -1, true, "%s", msg);
for (size_t i = 0; i < kv_size(channel->rpc.call_stack); i++) {
ChannelCallFrame *frame = kv_A(channel->rpc.call_stack, i);
if (frame->returned) {

View File

@@ -14,7 +14,10 @@
#include <string.h>
#include <time.h>
#include "nvim/api/buffer.h"
#include "nvim/api/extmark.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/autocmd_defs.h"
@@ -22,6 +25,7 @@
#include "nvim/buffer_defs.h"
#include "nvim/change.h"
#include "nvim/charset.h"
#include "nvim/clipboard.h"
#include "nvim/cmdhist.h"
#include "nvim/cursor.h"
#include "nvim/decoration.h"
@@ -29,13 +33,16 @@
#include "nvim/digraph.h"
#include "nvim/drawscreen.h"
#include "nvim/errors.h"
#include "nvim/eval.h"
#include "nvim/eval/buffer.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/vars.h"
#include "nvim/ex_cmds.h"
#include "nvim/ex_cmds2.h"
#include "nvim/ex_docmd.h"
#include "nvim/ex_eval.h"
#include "nvim/ex_getln.h"
#include "nvim/extmark.h"
#include "nvim/file_search.h"
#include "nvim/fileio.h"
#include "nvim/fold.h"
@@ -45,15 +52,19 @@
#include "nvim/help.h"
#include "nvim/highlight.h"
#include "nvim/highlight_defs.h"
#include "nvim/highlight_group.h"
#include "nvim/indent_c.h"
#include "nvim/input.h"
#include "nvim/input_cmdatom.h"
#include "nvim/insert.h"
#include "nvim/insexpand.h"
#include "nvim/keycodes.h"
#include "nvim/lua/executor.h"
#include "nvim/macros_defs.h"
#include "nvim/mapping.h"
#include "nvim/mark.h"
#include "nvim/mark_defs.h"
#include "nvim/marktree.h"
#include "nvim/math.h"
#include "nvim/mbyte.h"
#include "nvim/mbyte_defs.h"
@@ -100,10 +111,14 @@ typedef struct {
bool ctrl_w;
bool need_flushbuf;
bool set_prevcount;
bool previous_got_int; // `got_int` was true
bool toplevel; // top-level normal mode
oparg_T oa; // operator arguments
cmdarg_T ca; // command arguments
bool previous_got_int; ///< `got_int` was true.
bool toplevel; ///< This is a (poorly-named) _behavior_ opt-in, not a state indicator.
///< It enables "full interactive-command treatment":
///< - count prep, v:count publication.
///< - scrollbind/cursorbind syncing after the command.
///< - callers pair it with readbuf1_empty() to exclude stuffed keys.
oparg_T oa; ///< Operator arguments.
cmdarg_T ca; ///< Command arguments.
int mapped_len;
int old_mapped_len;
int idx;
@@ -147,6 +162,8 @@ typedef void (*nv_func_T)(cmdarg_T *cap);
#define NV_RL 0x80 // 'rightleft' modifies command
#define NV_KEEPREG 0x100 // don't clear regname
#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)
// Generally speaking, every Normal mode command should either clear any
// pending operator (with *clearop*()), or set the motion type variable
@@ -201,12 +218,12 @@ static const struct nv_cmd {
{ Ctrl__, nv_error, 0, 0 },
{ ' ', nv_right, 0, 0 },
{ '!', nv_operator, 0, 0 },
{ '"', nv_regname, NV_NCH_NOP|NV_KEEPREG, 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_optrans, 0, 0 },
{ '\'', nv_gomark, NV_NCH_ALW, true },
{ '\'', nv_gomark, NV_NCH_ALW|NV_NCH_ARG, true },
{ '(', nv_brace, 0, BACKWARD },
{ ')', nv_brace, 0, FORWARD },
{ '*', nv_ident, 0, 0 },
@@ -231,7 +248,7 @@ static const struct nv_cmd {
{ '=', nv_operator, 0, 0 },
{ '>', nv_operator, NV_RL, 0 },
{ '?', nv_search, 0, false },
{ '@', nv_at, NV_NCH_NOP, false },
{ '@', nv_at, NV_NCH_NOP|NV_NCH_ARG, false },
{ 'A', nv_edit, 0, 0 },
{ 'B', nv_bck_word, 0, 1 },
{ 'C', nv_abbrev, NV_KEEPREG, 0 },
@@ -262,7 +279,7 @@ static const struct nv_cmd {
{ ']', nv_brackets, NV_NCH_ALW, FORWARD },
{ '^', nv_beginline, 0, BL_WHITE | BL_FIX },
{ '_', nv_lineop, 0, 0 },
{ '`', nv_gomark, NV_NCH_ALW, false },
{ '`', nv_gomark, NV_NCH_ALW|NV_NCH_ARG, false },
{ 'a', nv_edit, NV_NCH, 0 },
{ 'b', nv_bck_word, 0, 0 },
{ 'c', nv_operator, 0, 0 },
@@ -275,11 +292,11 @@ static const struct nv_cmd {
{ 'j', nv_down, 0, false },
{ 'k', nv_up, 0, false },
{ 'l', nv_right, NV_RL, 0 },
{ 'm', nv_mark, NV_NCH_NOP, 0 },
{ 'm', nv_mark, NV_NCH_NOP|NV_NCH_ARG, 0 },
{ 'n', nv_next, 0, 0 },
{ 'o', nv_open, 0, 0 },
{ 'p', nv_put, 0, 0 },
{ 'q', nv_q, NV_NCH, 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 },
@@ -406,6 +423,14 @@ void init_normal_cmds(void)
nv_max_linear = i - 1;
}
/// True if a command's second char (cmdarg_T.nchar) is a typed operand ("fx", "ma") rather than
/// the second char of its name ("gJ", "iw").
bool nv_nchar_is_arg(int cmdchar)
{
int idx = find_command(cmdchar);
return idx >= 0 && (nv_cmds[idx].cmd_flags & (NV_LANG|NV_NCH_ARG)) != 0;
}
/// Search for a command in the commands table.
///
/// @return -1 for invalid command.
@@ -500,9 +525,7 @@ bool op_pending(void)
&& current_oap->regname == NUL);
}
/// Normal state entry point: the main loop. Called on startup, never returns.
///
/// This used to be called main_loop() on main.c
/// Normal state entry point: the main loop. Never returns.
void normal_enter(void)
{
NormalState state;
@@ -1018,7 +1041,6 @@ normal_end:
}
checkpcmark(); // check if we moved since setting pcmark
xfree(s->ca.searchbuf);
mb_check_adjust_col(curwin); // #6203
@@ -1063,6 +1085,18 @@ 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++;
CmdBaseline atom_old;
atom_cmd_start(&atom_old);
NormalState *s = (NormalState *)state;
s->command_finished = false;
s->ctrl_w = false; // got CTRL-W command
@@ -1092,7 +1126,7 @@ static int normal_execute(VimState *state, int key)
// When "restart_edit" is set fake a "d"elete command, Insert mode will restart automatically.
// Insert the typed character in the typeahead buffer, so that it can
// be mapped in Insert mode. Required for ":lmap" to work.
requeue_key(vgetc_char, vgetc_mod_mask, true);
requeue_key(vgetc_char, vgetc_mod_mask, 0, true);
if (restart_edit != 0) {
s->c = 'd';
@@ -1133,12 +1167,11 @@ static int normal_execute(VimState *state, int key)
// Always remember the count. It will be set to zero (on the next call,
// above) when there is no pending operator.
// When called from main(), save the count for use by the "count" built-in
// variable.
// When called from toplevel, save the count for use by v:count.
s->ca.opcount = s->ca.count0;
s->ca.count1 = (s->ca.count0 == 0 ? 1 : s->ca.count0);
// Only set v:count when called from main() and not a stuffed command.
// Only set v:count when called from toplevel and not a stuffed command.
// Do set it for redo.
if (s->toplevel && readbuf1_empty()) {
set_vcount(s->ca.count0, s->ca.count1, s->set_prevcount);
@@ -1236,6 +1269,9 @@ static int normal_execute(VimState *state, int key)
finish:
normal_finish_command(s);
atom_cmd_end(&s->ca, &atom_old, depth == 1);
xfree(s->ca.searchbuf);
depth--;
return 1;
}
@@ -1699,51 +1735,11 @@ size_t find_ident_at_pos(win_T *wp, linenr_T lnum, colnr_T startcol, char **text
/// Prepare for redo of a normal command.
static void prep_redo_cmd(cmdarg_T *cap)
{
prep_redo(cap->oap->regname, cap->count0,
NUL, cap->cmdchar, NUL, NUL, NUL);
if (cap->nchar_len > 0) {
AppendToRedobuff(cap->nchar_composing);
} else {
AppendCharToRedobuff(cap->nchar);
}
}
/// Prepare for redo of any command.
/// Note that only the last argument can be a multi-byte char.
void prep_redo(int regname, int num, int cmd1, int cmd2, int cmd3, int cmd4, int cmd5)
{
prep_redo_num2(regname, num, cmd1, cmd2, 0, cmd3, cmd4, cmd5);
}
/// Prepare for redo of any command with extra count after "cmd2".
void prep_redo_num2(int regname, int num1, int cmd1, int cmd2, int num2, int cmd3, int cmd4,
int cmd5)
{
ResetRedobuff();
if (regname != 0) { // yank from specified buffer
AppendCharToRedobuff('"');
AppendCharToRedobuff(regname);
}
if (num1 != 0) {
AppendNumberToRedobuff(num1);
}
if (cmd1 != NUL) {
AppendCharToRedobuff(cmd1);
}
if (cmd2 != NUL) {
AppendCharToRedobuff(cmd2);
}
if (num2 != 0) {
AppendNumberToRedobuff(num2);
}
if (cmd3 != NUL) {
AppendCharToRedobuff(cmd3);
}
if (cmd4 != NUL) {
AppendCharToRedobuff(cmd4);
}
if (cmd5 != NUL) {
AppendCharToRedobuff(cmd5);
// Composing chars: the operand's byte form is the composed string, not the single char.
bool composing = cap->nchar_len > 0;
prep_redo(NULL, 0, composing, atom_cmd_spec(cap));
if (composing) {
redo_append_str(cap->nchar_composing, -1);
}
}
@@ -1759,7 +1755,7 @@ static bool checkclearop(oparg_T *oap)
return true;
}
/// Check for operator or Visual active. Clear active operator.
/// Checks for operator or Visual active, and clears active operator.
///
/// Beep and return true if an operator or Visual was active.
static bool checkclearopq(oparg_T *oap)
@@ -1925,7 +1921,9 @@ bool add_to_showcmd(int c)
0
};
if (!p_sc || msg_silent != 0 || ex_normal_busy) {
// Not for stuffed (replay): "." executes as atomic unit; displaying the keys would churn
// 'showcmd' mid-replay, redrawing transient states (e.g. selection of a replayed visual op).
if (!p_sc || msg_silent != 0 || ex_normal_busy || KeyStuffed) {
return false;
}
@@ -2019,7 +2017,8 @@ void showcmd_update_clear_state(void)
showcmd_is_clear = (showcmd_buf[0] == NUL);
}
static void display_showcmd(void)
/// Displays 'showcmd' info, and a ("2×") hint if multicursor is active.
void display_showcmd(void)
{
showcmd_update_clear_state();
@@ -2066,8 +2065,10 @@ static void display_showcmd(void)
int len = 0;
if (!showcmd_is_clear) {
len = grid_line_puts(sc_col, showcmd_buf, -1, HL_ATTR(HLF_MSG));
len += grid_line_puts(sc_col + len, showcmd_buf, -1, HL_ATTR(HLF_MSG));
}
// Clamp so the padding arithmetic below stays in bounds.
len = MIN(len, (int)SHOWCMD_COLS);
// clear the rest of an old message by outputting up to SHOWCMD_COLS spaces
grid_line_puts(sc_col + len, (char *)" " + len, -1, HL_ATTR(HLF_MSG));
@@ -3111,12 +3112,16 @@ static void nv_regreplay(cmdarg_T *cap)
return;
}
if (reg_recorded != 0) {
// The macro's commands are captured as one "@x"-labeled atom (see
// atom_macro_start()).
atom_macro_start(reg_recorded);
}
while (cap->count1-- && !got_int) {
if (do_execreg(reg_recorded, false, false, false) == false) {
clearopbeep(cap->oap);
break;
}
line_breakcheck();
}
}
@@ -4579,15 +4584,19 @@ static void nv_replace(cmdarg_T *cap)
// Give 'r' to edit(), to get the redo command right.
invoke_edit(cap, true, 'r', false);
} else {
prep_redo(cap->oap->regname, cap->count1, NUL, 'r', NUL, had_ctrl_v, 0);
prep_redo(NULL, 0, true, (CmdSpec){ .regname = cap->oap->regname, .count = cap->count1,
.cmd = 'r', .arg = cap->nchar });
if (had_ctrl_v != NUL) {
redo_append_char(had_ctrl_v);
}
curbuf->b_op_start = curwin->w_cursor;
const int old_State = State;
if (cap->nchar_len > 0) {
AppendToRedobuff(cap->nchar_composing);
redo_append_str(cap->nchar_composing, -1);
} else {
AppendCharToRedobuff(cap->nchar);
redo_append_char(cap->nchar);
}
// This is slow, but it handles replacing a single-byte with a
@@ -5637,11 +5646,17 @@ static void nv_g_cmd(cmdarg_T *cap)
nv_gd(oap, cap->nchar, cap->count0);
break;
// g<LeftMouse>: jump to mouse-clicked tag, like "CTRL-]".
case K_LEFTMOUSE:
if (do_mouse(oap, cap->nchar, BACKWARD, cap->count1, 0)) {
stuffcharReadbuff(Ctrl_RSB);
}
break;
// g<*Mouse> : <C-*mouse>
case K_MIDDLEMOUSE:
case K_MIDDLEDRAG:
case K_MIDDLERELEASE:
case K_LEFTMOUSE:
case K_LEFTDRAG:
case K_LEFTRELEASE:
case K_MOUSEMOVE:
@@ -5754,7 +5769,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.
if (start_redo(cap->count0, restart_edit != 0 && !Ins.arrow_used) == false) {
if (start_redo(cap->count0, restart_edit != 0 && Ins.moved == kInsNone) == false) {
clearopbeep(cap->oap);
}
}
@@ -5833,7 +5848,7 @@ static void nv_operator(cmdarg_T *cap)
return;
}
if (op_type == cap->oap->op_type) { // double operator works on lines
if (op_type == cap->oap->op_type) { // double operator ("dd") works on lines
nv_lineop(cap);
} else if (!checkclearop(cap->oap)) {
cap->oap->start = curwin->w_cursor;
@@ -6274,13 +6289,17 @@ static void invoke_edit(cmdarg_T *cap, int repl, int cmd, int startln)
if (cap->cmdchar != 'O' && cap->cmdchar != 'o') {
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
}
if (edit(cmd, startln, cap->count1)) {
InsSession session = atom_ins_start(cmd, cap->count1, kVInsNone, false);
bool busy = edit(cmd, startln, cap->count1);
if (busy) {
cap->retval |= CA_COMMAND_BUSY;
}
if (restart_edit == 0) {
restart_edit = restart_edit_save;
}
atom_ins_end(&session, busy);
}
/// "a" or "i" while an operator is pending or in Visual mode: object motion.
@@ -6417,6 +6436,7 @@ static void nv_at(cmdarg_T *cap)
return;
}
}
atom_macro_start(cap->nchar);
while (cap->count1-- && !got_int) {
if (do_execreg(cap->nchar, false, false, false) == false) {
clearopbeep(cap->oap);
@@ -6458,8 +6478,7 @@ static void nv_join(cmdarg_T *cap)
cap->count0 = curbuf->b_ml.ml_line_count - curwin->w_cursor.lnum + 1;
}
prep_redo(cap->oap->regname, cap->count0,
NUL, cap->cmdchar, NUL, NUL, cap->nchar);
prep_redo_cmd(cap);
do_join((size_t)cap->count0, cap->nchar == NUL, true, true, true);
}
@@ -6514,7 +6533,15 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent)
|| ((cap->cmdchar == 'g' || cap->cmdchar == 'z')
&& cap->nchar == 'P')) ? BACKWARD : FORWARD;
}
prep_redo_cmd(cap);
bool vatom_prepped = false;
if (Visual.active) {
// Visual-mode put: complete the visual atom ("viw" + "p").
vatom_prepped = atom_visual_end((CmdSpec){ .regname = cap->oap->regname, .count = cap->count0,
.cmd = cap->cmdchar, .cmd2 = cap->nchar }, true);
}
if (!vatom_prepped) {
prep_redo_cmd(cap);
}
if (cap->cmdchar == 'g') {
flags |= PUT_CURSEND;
} else if (cap->cmdchar == 'z') {
@@ -6552,8 +6579,10 @@ static void nv_put_opt(cmdarg_T *cap, bool fix_indent)
cap->nchar = NUL;
cap->oap->regname = keep_registers ? '_' : NUL;
msg_silent++;
atom_suppress(true); // internal op: the put atom already cascades
nv_operator(cap);
do_pending_operator(cap, 0, false);
atom_suppress(false);
empty = (curbuf->b_ml.ml_flags & ML_EMPTY);
msg_silent--;

View File

@@ -66,15 +66,12 @@ enum {
CA_NO_ADJ_OP_END = 2, ///< don't adjust operator end
};
/// A Visual selection's mode and extent (line/column span, not absolute positions), so it can be
/// re-applied starting at the cursor: for "gv" reselect (`Visual.resel`) and Visual-operator redo
/// (`redo_VIsual`).
/// A Visual selection's mode and extent (line/column span, not absolute positions), so an
/// equal-sized region can be re-applied starting at the cursor: {count}v reselect.
typedef struct {
int mode; ///< 'v', 'V', or Ctrl-V
linenr_T line_count; ///< number of lines
colnr_T vcol; ///< number of cols or end column (MAXCOL: to end of line)
int count; ///< count for the Visual operator
int arg; ///< extra argument
} VisualExtent;
/// Visual/Select mode state, as one global "group" (Visual). Previously these were bare EXTERN
@@ -88,8 +85,7 @@ typedef struct {
int restart_select; ///< Restart Select mode when next cmd finished.
int reselect; ///< Restart the selection after a Select-mode mapping or menu.
int mode; ///< Type of Visual mode: 'v', 'V', Ctrl-V.
bool redo_busy; ///< True when redoing Visual.
VisualExtent resel; ///< Previous Visual area, for reselection ("gv"); seeds operator-redo.
VisualExtent resel; ///< Previous Visual area's extent, for {count}v reselect.
} VisualState;
/// Replacement for nchar used by nv_replace().

View File

@@ -43,6 +43,7 @@
#include "nvim/indent.h"
#include "nvim/indent_c.h"
#include "nvim/input.h"
#include "nvim/input_cmdatom.h"
#include "nvim/input_defs.h"
#include "nvim/insert.h"
#include "nvim/keycodes.h"
@@ -108,7 +109,7 @@ static const char opchars[][3] = {
{ 'r', NUL, OPF_CHANGE }, // OP_REPLACE
{ 'I', NUL, OPF_CHANGE }, // OP_INSERT
{ 'A', NUL, OPF_CHANGE }, // OP_APPEND
{ 'z', 'f', 0 }, // OP_FOLD
{ 'z', 'f', 0 }, // OP_FOLD
{ 'z', 'o', OPF_LINES }, // OP_FOLDOPEN
{ 'z', 'O', OPF_LINES }, // OP_FOLDOPENREC
{ 'z', 'c', OPF_LINES }, // OP_FOLDCLOSE
@@ -170,6 +171,13 @@ int op_is_change(int op)
return opchars[op][2] & OPF_CHANGE;
}
/// Whether operator `op` builds a redo: text-changing operators do, ":" and the fold operators do
/// not. Yank with the 'y' flag in 'cpoptions'.
bool op_redoable(int op, bool redo_yank)
{
return op == OP_YANK ? redo_yank : op_is_change(op);
}
/// Get first operator command character.
///
/// @return 'g' or 'z' if there is another command character.
@@ -3189,7 +3197,7 @@ static void op_function(const oparg_T *oap)
/// Calculate start/end virtual columns for operating in block mode.
///
/// @param initial when true: adjust position for 'selectmode'
static void get_op_vcol(oparg_T *oap, colnr_T redo_VIsual_vcol, bool initial)
static void get_op_vcol(oparg_T *oap, bool initial)
{
colnr_T start;
colnr_T end;
@@ -3205,18 +3213,16 @@ static void get_op_vcol(oparg_T *oap, colnr_T redo_VIsual_vcol, bool initial)
mark_mb_adjustpos(curwin->w_buffer, &oap->end);
getvvcol(curwin, &(oap->start), &oap->start_vcol, NULL, &oap->end_vcol, 0);
if (!Visual.redo_busy) {
getvvcol(curwin, &(oap->end), &start, NULL, &end, 0);
getvvcol(curwin, &(oap->end), &start, NULL, &end, 0);
oap->start_vcol = MIN(oap->start_vcol, start);
if (end > oap->end_vcol) {
if (initial && *p_sel == 'e'
&& start >= 1
&& start - 1 >= oap->end_vcol) {
oap->end_vcol = start - 1;
} else {
oap->end_vcol = end;
}
oap->start_vcol = MIN(oap->start_vcol, start);
if (end > oap->end_vcol) {
if (initial && *p_sel == 'e'
&& start >= 1
&& start - 1 >= oap->end_vcol) {
oap->end_vcol = start - 1;
} else {
oap->end_vcol = end;
}
}
@@ -3229,8 +3235,6 @@ static void get_op_vcol(oparg_T *oap, colnr_T redo_VIsual_vcol, bool initial)
getvvcol(curwin, &curwin->w_cursor, NULL, NULL, &end, 0);
oap->end_vcol = MAX(oap->end_vcol, end);
}
} else if (Visual.redo_busy) {
oap->end_vcol = oap->start_vcol + redo_VIsual_vcol - 1;
}
// Correct oap->end.col and oap->start.col to be the
@@ -3252,6 +3256,24 @@ static bool is_ex_cmdchar(cmdarg_T *cap)
return cap->cmdchar == ':' || cap->cmdchar == K_COMMAND;
}
/// How an Insert-entering operator (OP_CHANGE/OP_INSERT/OP_APPEND) was entered from Visual mode:
/// decides the session's redo/publish handling (atom_ins_start()).
static VisualIns op_ins_visual(oparg_T *oap, cmdarg_T *cap)
{
if (!oap->is_VIsual) {
return kVInsNone;
}
if (is_ex_cmdchar(cap) || cap->cmdchar == K_LUA || oap->motion_force != NUL
|| (cap->cmdchar == 'g'
&& (cap->nchar == 'n' || cap->nchar == 'N' || cap->nchar == 'v'))) {
// The selection came from a self-selecting motion (gn/gN/gv, an omap running ":normal", a Lua
// motion) or a forced-motion operator: the redo replays the motion's own keys instead.
return kVInsOther;
}
// Unreplayable (void/absent) selection: the redo falls back to an equal-size reselect ("1v").
return atom_visual_replayable() ? kVInsKeys : kVInsOther;
}
/// Handle an operator after Visual mode or when the movement is finished.
/// "gui_yank" is true when yanking text for the clipboard.
void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
@@ -3259,9 +3281,6 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
oparg_T *oap = cap->oap;
int lbr_saved = curwin->w_p_lbr;
// The visual area is remembered for redo
static VisualExtent redo_VIsual = { NUL, 0, 0, 0, 0 };
pos_T old_cursor = curwin->w_cursor;
// If an operation is pending, handle it...
@@ -3301,75 +3320,46 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
Visual.reselect = false;
}
// Only redo yank when 'y' flag is in 'cpoptions'.
// Never redo "zf" (define fold).
if ((redo_yank || oap->op_type != OP_YANK)
atom_capture_op(oap, cap, redo_yank);
if (op_redoable(oap->op_type, redo_yank)
&& ((!Visual.active || oap->motion_force)
// Also redo Operator-pending Visual mode mappings.
|| ((is_ex_cmdchar(cap) || cap->cmdchar == K_LUA)
&& oap->op_type != OP_COLON))
&& cap->cmdchar != 'D'
&& oap->op_type != OP_FOLD
&& oap->op_type != OP_FOLDOPEN
&& oap->op_type != OP_FOLDOPENREC
&& oap->op_type != OP_FOLDCLOSE
&& oap->op_type != OP_FOLDCLOSEREC
&& oap->op_type != OP_FOLDDEL
&& oap->op_type != OP_FOLDDELREC) {
prep_redo(oap->regname, cap->count0,
get_op_char(oap->op_type), get_extra_op_char(oap->op_type),
oap->motion_force, cap->cmdchar, cap->nchar);
|| is_ex_cmdchar(cap) || cap->cmdchar == K_LUA)
&& cap->cmdchar != 'D') {
prep_redo(NULL, 0, false, (CmdSpec){
.regname = oap->regname, .count = cap->count0,
.op = get_op_char(oap->op_type), .op_extra = get_extra_op_char(oap->op_type),
.motion_force = oap->motion_force, .cmd = cap->cmdchar, .cmd2 = cap->nchar,
});
if (cap->cmdchar == '/' || cap->cmdchar == '?') { // was a search
// If 'cpoptions' does not contain 'r', insert the search
// pattern to really repeat the same command.
if (vim_strchr(p_cpo, kCpoRedo) == NULL) {
AppendToRedobuffLit(cap->searchbuf, -1);
redo_append_lit(cap->searchbuf, -1);
}
AppendToRedobuff(NL_STR);
redo_append_str(S_LEN(NL_STR));
} else if (is_ex_cmdchar(cap)) {
// do_cmdline() has stored the first typed line in
// "repeat_cmdline". When several lines are typed repeating
// won't be possible.
if (repeat_cmdline == NULL) {
ResetRedobuff();
redo_new((CmdSpec){ 0 });
} else {
if (cap->cmdchar == ':') {
AppendToRedobuffLit(repeat_cmdline, -1);
redo_append_lit(repeat_cmdline, -1);
} else {
AppendToRedobuffSpec(repeat_cmdline);
redo_append_spec(repeat_cmdline);
}
AppendToRedobuff(NL_STR);
redo_append_str(S_LEN(NL_STR));
XFREE_CLEAR(repeat_cmdline);
}
} else if (cap->cmdchar == K_LUA) {
AppendNumberToRedobuff(repeat_luaref);
AppendToRedobuff(NL_STR);
redo_append_num(repeat_luaref);
redo_append_str(S_LEN(NL_STR));
}
}
if (Visual.redo_busy) {
// Redo of an operation on a Visual area. Use the same size from
// redo_VIsual.line_count and redo_VIsual.vcol.
oap->start = curwin->w_cursor;
curwin->w_cursor.lnum += redo_VIsual.line_count - 1;
curwin->w_cursor.lnum = MIN(curwin->w_cursor.lnum, curbuf->b_ml.ml_line_count);
Visual.mode = redo_VIsual.mode;
if (redo_VIsual.vcol == MAXCOL || Visual.mode == 'v') {
if (Visual.mode == 'v') {
if (redo_VIsual.line_count <= 1) {
validate_virtcol(curwin);
curwin->w_curswant = curwin->w_virtcol + redo_VIsual.vcol - 1;
} else {
curwin->w_curswant = redo_VIsual.vcol;
}
} else {
curwin->w_curswant = MAXCOL;
}
coladvance(curwin, curwin->w_curswant);
}
cap->count0 = redo_VIsual.count;
cap->count1 = (cap->count0 == 0 ? 1 : cap->count0);
} else if (Visual.active) {
if (Visual.active) {
if (!gui_yank) {
// Save the current Visual area for '< and '> marks, and "gv"
curbuf->b_visual.vi_start = Visual.start;
@@ -3451,12 +3441,11 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
// Set "virtual_op" before resetting Visual.active.
virtual_op = virtual_active(curwin);
if (Visual.active || Visual.redo_busy) {
get_op_vcol(oap, redo_VIsual.vcol, true);
if (Visual.active) {
get_op_vcol(oap, true);
if (!Visual.redo_busy && !gui_yank) {
// Prepare to reselect and redo Visual: this is based on the
// size of the Visual text
if (!gui_yank) {
// Remember Visual text size, for {count}v reselect (:h visual-repeat).
Visual.resel.mode = Visual.mode;
if (curwin->w_curswant == MAXCOL) {
Visual.resel.vcol = MAXCOL;
@@ -3476,47 +3465,29 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
Visual.resel.line_count = oap->line_count;
}
// can't redo yank (unless 'y' is in 'cpoptions') and ":"
if ((redo_yank || oap->op_type != OP_YANK)
&& oap->op_type != OP_COLON
&& oap->op_type != OP_FOLD
&& oap->op_type != OP_FOLDOPEN
&& oap->op_type != OP_FOLDOPENREC
&& oap->op_type != OP_FOLDCLOSE
&& oap->op_type != OP_FOLDCLOSEREC
&& oap->op_type != OP_FOLDDEL
&& oap->op_type != OP_FOLDDELREC
&& oap->motion_force == NUL) {
// Prepare for redoing. Only use the nchar field for "r",
// otherwise it might be the second char of the operator.
if (op_redoable(oap->op_type, redo_yank) && oap->motion_force == NUL) {
// Prepare for redoing. "gn"/"gN"/"gv" motions select their own region: the
// redo replays the motion itself ("dgn" re-searches, "dgv" reselects).
if (cap->cmdchar == 'g' && (cap->nchar == 'n'
|| cap->nchar == 'N')) {
prep_redo(oap->regname, cap->count0,
get_op_char(oap->op_type), get_extra_op_char(oap->op_type),
oap->motion_force, cap->cmdchar, cap->nchar);
} else if (!is_ex_cmdchar(cap) && cap->cmdchar != K_LUA) {
int opchar = get_op_char(oap->op_type);
int extra_opchar = get_extra_op_char(oap->op_type);
int nchar = oap->op_type == OP_REPLACE ? cap->nchar : NUL;
// reverse what nv_replace() did
if (nchar == REPLACE_CR_NCHAR) {
nchar = CAR;
} else if (nchar == REPLACE_NL_NCHAR) {
nchar = NL;
}
if (opchar == 'g' && extra_opchar == '@') {
// also repeat the count for 'operatorfunc'
prep_redo_num2(oap->regname, 0, NUL, 'v', cap->count0, opchar, extra_opchar, nchar);
} else {
prep_redo(oap->regname, 0, NUL, 'v', opchar, extra_opchar, nchar);
}
}
if (!Visual.redo_busy) {
redo_VIsual = Visual.resel;
redo_VIsual.count = cap->count0;
redo_VIsual.arg = cap->arg;
|| cap->nchar == 'N'
|| cap->nchar == 'v')) {
prep_redo(NULL, 0, false, (CmdSpec){
.regname = oap->regname, .count = cap->count0,
.op = get_op_char(oap->op_type), .op_extra = get_extra_op_char(oap->op_type),
.motion_force = oap->motion_force, .cmd = cap->cmdchar, .cmd2 = cap->nchar,
});
} else if ((oap->op_type == OP_CHANGE || oap->op_type == OP_INSERT
|| oap->op_type == OP_APPEND)
&& !is_ex_cmdchar(cap) && cap->cmdchar != K_LUA) {
// Visual-entered Insert: redo body opens with the selection's captured keys; appends the
// op+text+<Esc>. Unreplayable (void) selection falls back to "1v" (fixed-size reselect).
// (Ex/Lua-motion selections were already prepped above, as the motion's keys.)
String v = atom_visual_span();
prep_redo(v.data != NULL ? v.data : "1v", v.data != NULL ? v.size : 2, false, (CmdSpec){
.regname = oap->regname,
.op = get_op_char(oap->op_type), .op_extra = get_extra_op_char(oap->op_type),
});
xfree(v.data);
}
}
@@ -3546,8 +3517,6 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
}
}
Visual.redo_busy = false;
// Switch Visual off now, so screen updating does
// not show inverted text when the screen is redrawn.
// With OP_YANK and sometimes with OP_COLON and OP_FILTER there is
@@ -3651,7 +3620,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
Visual.reselect = false; // don't reselect now
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
op_delete(oap);
// save cursor line for undo if it wasn't saved yet
@@ -3667,7 +3636,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
if (empty_region_error) {
if (!gui_yank) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
}
} else {
restore_lbr(lbr_saved);
@@ -3684,7 +3653,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
Visual.reselect = false; // don't reselect now
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
// This is a new edit command, not a restart. Need to
// remember it to make i_CTRL-O work with mappings for
@@ -3702,18 +3671,22 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
// trigger TextChangedI
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
if (op_change(oap)) { // will call edit()
InsSession session = atom_ins_start('c', cap->count0, op_ins_visual(oap, cap),
oap->is_VIsual && oap->motion_type == kMTBlockWise);
bool busy = op_change(oap); // will call edit()
if (busy) {
cap->retval |= CA_COMMAND_BUSY;
}
if (restart_edit == 0) {
restart_edit = restart_edit_save;
}
atom_ins_end(&session, busy);
}
break;
case OP_FILTER:
if (vim_strchr(p_cpo, kCpoFilter) != NULL) {
AppendToRedobuff("!\r"); // Use any last used !cmd.
redo_append_str(S_LEN("!\r")); // Use any last used !cmd.
} else {
bangredo = true; // do_bang() will put cmd in redo buffer.
}
@@ -3745,7 +3718,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
case OP_ROT13:
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
op_tilde(oap);
}
@@ -3768,26 +3741,21 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
op_format(oap, true); // use internal function
break;
case OP_FUNCTION: {
VisualExtent save_redo_VIsual = redo_VIsual;
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);
// Restore the info for redoing Visual mode, the function may
// invoke another operator and unintentionally change it.
redo_VIsual = save_redo_VIsual;
atom_opfunc_slice(false);
break;
}
case OP_INSERT:
case OP_APPEND:
Visual.reselect = false; // don't reselect now
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
// This is a new edit command, not a restart. Need to
// remember it to make i_CTRL-O work with mappings for
@@ -3801,7 +3769,10 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
// trigger TextChangedI
curbuf->b_last_changedtick_i = buf_get_changedtick(curbuf);
InsSession session = atom_ins_start('I', cap->count1, op_ins_visual(oap, cap),
oap->motion_type == kMTBlockWise);
op_insert(oap, cap->count1);
atom_ins_end(&session, false);
// Reset linebreak, so that formatting works correctly.
reset_lbr();
@@ -3822,7 +3793,7 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
Visual.reselect = false; // don't reselect now
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
// Restore linebreak, so that when the user edits it looks as before.
restore_lbr(lbr_saved);
@@ -3860,11 +3831,11 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank)
case OP_NR_SUB:
if (empty_region_error) {
vim_beep(kOptBoFlagOperator);
CancelRedo();
redo_cancel();
} else {
Visual.active = true;
restore_lbr(lbr_saved);
op_addsub(oap, (linenr_T)cap->count1, redo_VIsual.arg);
op_addsub(oap, (linenr_T)cap->count1, cap->arg);
Visual.active = false;
}
check_cursor_col(curwin);

View File

@@ -1,5 +1,6 @@
#pragma once
#include <lauxlib.h> // for LUA_NOREF
#include <stdbool.h>
#include <stddef.h>

View File

@@ -49,7 +49,6 @@
#include "nvim/ui.h"
#include "nvim/vim_defs.h"
#define NS_1_SECOND 1000000000U // 1 second, in nanoseconds
#define OUT_DATA_THRESHOLD 1024 * 10U // 10KB, "a few screenfuls" of data.
#define SHELL_SPECIAL "\t \"&'$;<>()\\|\n"
@@ -1049,10 +1048,10 @@ static bool out_data_decide_throttle(size_t size)
started = os_hrtime();
} else {
uint64_t since = os_hrtime() - started;
if (since < (visit * (NS_1_SECOND / 10))) {
if (since < (visit * (NS_PER_SEC / 10))) {
return true;
}
if (since > (3 * NS_1_SECOND)) {
if (since > (3 * NS_PER_SEC)) {
received = visit = 0;
return false;
}

View File

@@ -48,7 +48,7 @@ int64_t os_realtime(void)
ELOG("uv_clock_gettime failed: %d %s", error_number, uv_err_name(error_number));
return 0;
}
return ts.tv_sec * 1000000000L + ts.tv_nsec;
return ts.tv_sec * NS_PER_SEC + ts.tv_nsec;
}
/// Gets a millisecond-resolution, monotonically-increasing time relative to an

View File

@@ -2,3 +2,5 @@
#include <stdint.h>
typedef uint64_t Timestamp;
#define NS_PER_SEC 1000000000U ///< Nanoseconds per second.

View File

@@ -73,7 +73,7 @@ const char *profile_msg(proftime_T tm) FUNC_ATTR_WARN_UNUSED_RESULT
{
static char buf[50];
snprintf(buf, sizeof(buf), "%10.6lf",
(double)profile_signed(tm) / 1000000000.0);
(double)profile_signed(tm) / NS_PER_SEC);
return buf;
}

View File

@@ -171,7 +171,7 @@ int get_default_register_name(void)
return name;
}
/// Iterate over registers `regs`.
/// Iterate over registers `regs` (only NUM_SAVED_REGISTERS, not clipboard "*"/"+").
///
/// @param[in] iter Iterator. Pass NULL to start iteration.
/// @param[in] regs Registers list to be iterated.
@@ -206,7 +206,7 @@ const void *op_reg_iter(const void *const iter, const yankreg_T *const regs, cha
return NULL;
}
/// Iterate over global registers.
/// Iterate over global registers (only NUM_SAVED_REGISTERS, not clipboard "*"/"+").
///
/// @see op_register_iter
const void *op_global_reg_iter(const void *const iter, char *const name, yankreg_T *const reg,
@@ -216,6 +216,31 @@ const void *op_global_reg_iter(const void *const iter, char *const name, yankreg
return op_reg_iter(iter, y_regs, name, reg, is_unnamed);
}
/// Gets the most-recent timestamp (nanoseconds) of all registers, or 0 if all are empty. Every
/// register-write sets its timestamp, so this detects if any register changed (e.g. for caching).
///
/// @param clipboard Include clipboard registers ("*"/"+").
Timestamp reg_max_ts(bool clipboard)
{
Timestamp max_ts = 0;
const void *iter = NULL;
do {
char name = NUL;
yankreg_T reg;
bool is_unnamed = false;
iter = op_global_reg_iter(iter, &name, &reg, &is_unnamed);
if (name == NUL) {
break;
}
max_ts = MAX(max_ts, reg.timestamp);
} while (iter != NULL);
if (clipboard) {
max_ts = MAX(max_ts, get_y_register(op_reg_index('*'))->timestamp);
max_ts = MAX(max_ts, get_y_register(op_reg_index('+'))->timestamp);
}
return max_ts;
}
/// Get a number of non-empty registers
size_t op_reg_amount(void)
FUNC_ATTR_WARN_UNUSED_RESULT
@@ -424,7 +449,7 @@ static int stuff_yank(int regname, char *p)
reg->y_size = 1;
reg->y_type = kMTCharWise;
}
reg->timestamp = os_time();
reg->timestamp = (Timestamp)os_realtime();
return OK;
}
@@ -786,8 +811,8 @@ int insert_reg(int regname, yankreg_T *reg, bool literally_arg)
curwin->w_cursor = curpos;
}
AppendCharToRedobuff(Ctrl_R);
AppendCharToRedobuff(regname);
redo_append_char(Ctrl_R);
redo_append_char(regname);
do_put(regname, NULL, dir, 1, PUT_CURSEND);
} else {
stuffescaped(reg->y_array[i].data, literally);
@@ -1036,7 +1061,7 @@ void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append)
reg->y_width = 0;
reg->y_array = xcalloc(yanklines, sizeof(String));
reg->additional_data = NULL;
reg->timestamp = os_time();
reg->timestamp = (Timestamp)os_realtime();
size_t y_idx = 0; // index in y_array[]
linenr_T lnum = oap->start.lnum; // current line number
@@ -2649,7 +2674,7 @@ static void str_to_reg(yankreg_T *y_ptr, MotionType yank_type, const char *str,
y_ptr->y_type = yank_type;
y_ptr->y_size = lnum;
XFREE_CLEAR(y_ptr->additional_data);
y_ptr->timestamp = os_time();
y_ptr->timestamp = (Timestamp)os_realtime();
if (yank_type == kMTBlockWise) {
y_ptr->y_width = (blocklen == -1 ? (colnr_T)maxlen - 1 : blocklen);
} else {

View File

@@ -910,17 +910,18 @@ static void shada_read(FileDescriptor *const sd_reader, const int flags)
const bool get_old_files = (flags & (kShaDaGetOldfiles | kShaDaForceit)
&& (force || tv_list_len(oldfiles_list) == 0));
const bool want_marks = flags & kShaDaWantMarks;
const bool no_opt = flags & kShaDaNoOpt;
const unsigned srni_flags =
(unsigned)(
(flags & kShaDaWantInfo
? (kSDReadUndisableableData
| kSDReadRegisters
| kSDReadGlobalMarks
| (p_hi ? kSDReadHistory : 0)
| (find_shada_parameter('!') != NULL
| (p_hi && !(flags & kShaDaNoHistory) ? kSDReadHistory : 0)
| (no_opt || find_shada_parameter('!') != NULL
? kSDReadVariables
: 0)
| (find_shada_parameter('%') != NULL
| ((no_opt || find_shada_parameter('%') != NULL)
&& ARGCOUNT == 0
? kSDReadBufferList
: 0))
@@ -1042,32 +1043,36 @@ static void shada_read(FileDescriptor *const sd_reader, const int flags)
hms_insert(hms + cur_entry.data.history_item.histtype, cur_entry, true);
// Do not free shada entry: its allocated memory was saved above.
break;
case kSDItemRegister:
case kSDItemRegister: {
if (cur_entry.data.reg.type != kMTCharWise
&& cur_entry.data.reg.type != kMTLineWise
&& cur_entry.data.reg.type != kMTBlockWise) {
shada_free_shada_entry(&cur_entry);
break;
}
// reg are ns; shada stores seconds; Context/multicursor wants high-precision comparison.
const Timestamp reg_ts = (flags & kShaDaNanos)
? cur_entry.timestamp : cur_entry.timestamp * NS_PER_SEC;
if (!force) {
const yankreg_T *const reg = op_reg_get(cur_entry.data.reg.name);
if (reg == NULL || reg->timestamp >= cur_entry.timestamp) {
if (reg == NULL || reg->timestamp >= reg_ts) {
shada_free_shada_entry(&cur_entry);
break;
}
}
if (!op_reg_set(cur_entry.data.reg.name, (yankreg_T) {
.y_array = cur_entry.data.reg.contents,
.y_size = cur_entry.data.reg.contents_size,
.y_type = cur_entry.data.reg.type,
.y_width = (colnr_T)cur_entry.data.reg.width,
.timestamp = cur_entry.timestamp,
.additional_data = cur_entry.additional_data,
}, cur_entry.data.reg.is_unnamed)) {
.y_array = cur_entry.data.reg.contents,
.y_size = cur_entry.data.reg.contents_size,
.y_type = cur_entry.data.reg.type,
.y_width = (colnr_T)cur_entry.data.reg.width,
.timestamp = reg_ts,
.additional_data = cur_entry.additional_data,
}, cur_entry.data.reg.is_unnamed)) {
shada_free_shada_entry(&cur_entry);
}
// Do not free shada entry: its allocated memory was saved above.
break;
}
case kSDItemVariable:
var_set_global(cur_entry.data.global_var.name,
cur_entry.data.global_var.value);
@@ -1803,7 +1808,7 @@ static inline ShaDaWriteResult shada_read_when_writing(FileDescriptor *const sd_
}
if (wms->registers[idx].type == kSDItemMissing) {
const yankreg_T *const reg = op_reg_get(entry.data.reg.name);
if (reg != NULL && reg_empty(reg) && reg->timestamp >= entry.timestamp) {
if (reg != NULL && reg_empty(reg) && reg->timestamp / NS_PER_SEC >= entry.timestamp) {
shada_free_shada_entry(&entry);
break;
}
@@ -2108,7 +2113,9 @@ static inline void add_search_pattern(ShadaEntry *const ret_pse,
///
/// @param[in] wms The WriteMergerState used when writing.
/// @param[in] max_reg_lines The maximum number of register lines.
static inline void shada_initialize_registers(WriteMergerState *const wms, int max_reg_lines)
/// @param scale_ts Downscale the timestamp to seconds (shada legacy).
static inline void shada_initialize_registers(WriteMergerState *const wms, int max_reg_lines,
bool scale_ts)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_ALWAYS_INLINE
{
const void *reg_iter = NULL;
@@ -2127,7 +2134,7 @@ static inline void shada_initialize_registers(WriteMergerState *const wms, int m
wms->registers[op_reg_index(name)] = (ShadaEntry) {
.can_free_entry = false,
.type = kSDItemRegister,
.timestamp = reg.timestamp,
.timestamp = scale_ts ? reg.timestamp / NS_PER_SEC : reg.timestamp,
.data = {
.reg = {
.contents = reg.y_array,
@@ -2483,7 +2490,7 @@ static ShaDaWriteResult shada_write(FileDescriptor *const sd_writer,
// Initialize registers
if (dump_registers) {
shada_initialize_registers(wms, max_reg_lines);
shada_initialize_registers(wms, max_reg_lines, true);
}
// Initialize buffers
@@ -3609,14 +3616,17 @@ static inline size_t shada_init_jumps(ShadaEntry *jumps, Set(ptr_t) *const remov
}
/// Gets registers as a msgpack-encoded string, in shada format.
String shada_encode_regs(void)
///
/// @param scale_ts Downscale the timestamp to seconds (shada legacy).
/// @param since Only registers modified => this time (0: all). Nanoseconds, unless `scale_ts`.
String shada_encode_regs(bool scale_ts, Timestamp since)
FUNC_ATTR_NONNULL_ALL
{
WriteMergerState *const wms = xcalloc(1, sizeof(*wms));
shada_initialize_registers(wms, -1);
shada_initialize_registers(wms, -1, scale_ts);
PackerBuffer packer = packer_string_buffer();
for (size_t i = 0; i < ARRAY_SIZE(wms->registers); i++) {
if (wms->registers[i].type == kSDItemRegister) {
if (wms->registers[i].type == kSDItemRegister && wms->registers[i].timestamp >= since) {
if (kSDWriteFailed
== shada_pack_pfreed_entry(&packer, wms->registers[i], 0)) {
abort();

View File

@@ -1,6 +1,7 @@
#pragma once
#include "nvim/api/private/defs.h" // IWYU pragma: keep
#include "nvim/os/time_defs.h" // IWYU pragma: keep
/// Flags for shada_read_file and children
typedef enum {
@@ -9,6 +10,9 @@ typedef enum {
kShaDaForceit = 4, ///< Overwrite info already read
kShaDaGetOldfiles = 8, ///< Load v:oldfiles.
kShaDaMissingError = 16, ///< Error out when os_open returns -ENOENT.
kShaDaNanos = 32, ///< Timestamps are ns (Context), instead of seconds (shada legacy).
kShaDaNoHistory = 64, ///< Skip cmdline/search history merge (perf: O(history) per read).
kShaDaNoOpt = 128, ///< Ignore 'shada': only these flags decide what is read.
} ShaDaReadFileFlags;
#include "shada.h.generated.h"

View File

@@ -620,11 +620,10 @@ void spell_suggest(int count)
strcat(p, sug.su_badptr + stp->st_orglen);
// For redo we use a change-word command.
ResetRedobuff();
AppendToRedobuff("ciw");
AppendToRedobuffLit(p + c,
stp->st_wordlen + sug.su_badlen - stp->st_orglen);
AppendCharToRedobuff(ESC);
redo_new((CmdSpec){ 0 });
redo_append_str(S_LEN("ciw"));
redo_append_lit(p + c, stp->st_wordlen + sug.su_badlen - stp->st_orglen);
redo_append_char(ESC);
// "p" may be freed here
ml_replace(curwin->w_cursor.lnum, p, false);

View File

@@ -2316,7 +2316,7 @@ end:
return false;
}
requeue_key(vgetc_char, vgetc_mod_mask, true);
requeue_key(vgetc_char, vgetc_mod_mask, 0, true);
return true;
}

View File

@@ -107,6 +107,7 @@
#include "nvim/globals.h"
#include "nvim/highlight_defs.h"
#include "nvim/input.h"
#include "nvim/input_cmdatom.h"
#include "nvim/insert.h"
#include "nvim/macros_defs.h"
#include "nvim/mark.h"
@@ -1873,6 +1874,7 @@ static void u_doit(int startcount, bool quiet, bool do_buf_event)
if (!undo_allowed(curbuf)) {
return;
}
atom_op_global_set(); // multicursor: undo/redo must not cascade (global, not per-cursor).
u_newcount = 0;
u_oldcount = 0;

View File

@@ -0,0 +1,94 @@
-- Helpers shared by the atom-capture specs (mcursor_spec.lua, cmdatom_spec.lua).
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)
end
--- vim.keycode(): |key-notation| => the raw bytes of the CmdAtom event's keys/lhs.
function m.k(s)
return n.api.nvim_replace_termcodes(s, true, true, true)
end
--- Starts collecting CmdAtom event-data.
function m.atoms_start()
n.exec_lua([[
_G.atoms = {}
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
table.insert(_G.atoms, ev.data)
end,
})
]])
end
--- Gets the collected CmdAtom event-data (the events are deferred: drain
--- the event loop first).
function m.atoms()
n.poke_eventloop()
return n.exec_lua('return _G.atoms')
end
--- Gets the last collected CmdAtom event.
function m.atom_last()
local evs = m.atoms()
return evs[#evs]
end
--- Projects only the named fields of `ev` (a nil field stays absent, so `eq` still asserts
--- omission when the expected table lacks it).
function m.pick(ev, ...)
local r = {}
for _, f in ipairs({ ... }) do
r[f] = ev[f]
end
return r
end
--- Gets the last `count` collected CmdAtom events: bare `keys` strings by default, or
--- projections of the named `fields`.
function m.atoms_tail(count, ...)
local evs = m.atoms()
local fields = select('#', ...) > 0 and { ... } or nil
local tail = {}
for i = #evs - count + 1, #evs do
table.insert(tail, fields and m.pick(evs[i], unpack(fields)) or evs[i].keys)
end
return tail
end
--- Minimal vim-surround "ys": an <expr> mapping sets 'operatorfunc' and returns "g@"; the opfunc
--- reads the wrap char with getchar() and wraps the motion region (yank, modify register, paste
--- back).
m.minisurround_vim = [[
function! MiniSurroundSetup() abort
set operatorfunc=MiniSurround
return 'g@'
endfunction
function! MiniSurround(type) abort
let char = nr2char(getchar())
let save = getreg('"')
silent exe "norm! v`[o`]y"
call setreg('"', char .. getreg('"') .. char, 'v')
silent exe "norm! gvp`["
call setreg('"', save)
endfunction
nnoremap <expr> ys MiniSurroundSetup()
]]
--- 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 = [[
function! DelSurround() abort
call getchar()
" cursor is on the "("; delete it and its matching ")".
normal! mz%x`zx
endfunction
nnoremap <silent> ds :<C-U>call DelSurround()<CR>
]]
return m

View File

@@ -0,0 +1,866 @@
-- Tests for atom capture: the CmdAtom event, and dot-repeat of whole insert sessions.
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local t_atom = require('test.functional.editor.atom_testutil')
local describe, it, before_each = t.describe, t.it, t.before_each
local retry = t.retry
local clear = n.clear
local command = n.command
local exec = n.exec
local feed = n.feed
local fn = n.fn
local eq = t.eq
local api = n.api
local get_lines = t_atom.get_lines
local k = t_atom.k
local atoms_start = t_atom.atoms_start
local atoms = t_atom.atoms
local atoms_tail = t_atom.atoms_tail
local atom_last = t_atom.atom_last
local pick = t_atom.pick
describe('dot-repeat', function()
before_each(clear)
it('replays insert-mode cursor-moves (the whole session)', function()
fn.setline(1, { 'one', 'two' })
feed('iab<Left>c<Esc>')
eq({ 'acbone', 'two' }, get_lines())
-- The ". register and undo still restart at the cursor-move, like Vim.
eq('c', fn.getreg('.'))
feed('u')
eq({ 'abone', 'two' }, get_lines())
feed('<C-r>')
-- "." re-executes the whole session, cursor-move included.
feed('j0.')
eq({ 'acbone', 'acbtwo' }, get_lines())
-- An absolute jump (<C-Home>) still restarts the capture: "." replays
-- only the post-jump insert.
feed('ggAxy<C-Home>z<Esc>')
eq({ 'zacbonexy', 'acbtwo' }, get_lines())
eq('z', fn.getreg('.'))
feed('j0.')
eq({ 'zacbonexy', 'zacbtwo' }, get_lines())
-- Vim's repeat-only-the-tail behavior is a mapping away (documented in
-- vim_diff.txt): i_CTRL-O re-entry restarts the capture.
command('inoremap <Left> <C-o><Left>')
feed('ggiab<Left>c<Esc>')
eq({ 'acbzacbonexy', 'zacbtwo' }, get_lines())
feed('j0.')
eq({ 'acbzacbonexy', 'czacbtwo' }, get_lines())
end)
end)
describe('CmdAtom', function()
before_each(clear)
it('a counted mapped motion carries its count in the atom', function()
command('nnoremap j gj')
fn.setline(1, { 'a1', 'b2', 'c3', 'd4', 'e5' })
feed('gg')
atoms_start()
feed('3j')
eq(4, fn.line('.'))
local ev = atom_last()
eq(
{ type = 'motion', lhs = 'j', keys = '3gj', count = 3 },
pick(ev, 'type', 'lhs', 'keys', 'count')
)
-- The "," repeat recipe: replaying the KEYS verbatim repeats the count.
feed('gg')
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
eq(4, fn.line('.'))
-- Multi-command mapping: the pre-typed count lands in the FIRST folded
-- command; the folded atom itself has no single count.
command('nnoremap <F6> xw')
fn.setline(1, { 'abcdef ghi', 'jkl' })
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'))
end)
it('a Lua-callback mapping (the "]q" default) emits a mapping atom', 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.
n.exec_lua([[
vim.keymap.set('n', ']q', function()
vim.cmd({ cmd = 'cnext', count = vim.v.count1 })
end, { desc = ':cnext' })
]])
fn.setline(1, { 'aaa', 'bbb', 'ccc' })
local bufnr = api.nvim_get_current_buf()
fn.setqflist({ { bufnr = bufnr, lnum = 1 }, { bufnr = bufnr, lnum = 3 } })
command('cfirst')
atoms_start()
feed(']q')
eq(3, fn.line('.')) -- the mapping did run (:cnext)
eq(
{ type = 'mapping', lhs = ']q', keys = '', changed = false },
pick(atom_last(), 'type', 'lhs', 'keys', 'changed')
)
-- An empty-keys mapping that DOES edit still reports it: `changed` is
-- the only informative payload of a <Cmd>/Lua-callback edit.
n.exec_lua([[
vim.keymap.set('n', ',e', function()
vim.api.nvim_buf_set_lines(0, 0, 0, false, { 'NEW' })
end)
]])
feed(',e')
eq({ keys = '', changed = true }, pick(atom_last(), 'keys', 'changed'))
end)
it('motions, search, Ex emit without an edit', function()
-- Emission is not tied to editing: every user action publishes, so
-- plugins can observe all activity.
fn.setline(1, { 'alpha beta', 'gamma delta' })
feed('gg0')
atoms_start()
feed('w')
feed('3l')
feed('/gamma<CR>')
feed(':nohlsearch<CR>')
eq({
{ type = 'motion', keys = 'w' },
{ type = 'motion', keys = '3l' },
{ type = 'motion', keys = k('/gamma<NL>') },
{ type = 'ex', keys = k(':nohlsearch<NL>') },
}, atoms_tail(4, 'type', 'keys'))
-- ":" embeds its count as the range prefill, never as composed digits;
-- the count field carries it.
feed('gg')
feed('2:<CR>')
eq(2, fn.line('.'))
eq(
{ type = 'ex', keys = k(':.,.+1<NL>'), count = 2, cmd = ':' },
pick(atom_last(), 'type', 'keys', 'count', 'cmd')
)
eq({ 'alpha beta', 'gamma delta' }, get_lines()) -- nothing was edited
-- Scrolls and mouse presses also emit (type "scroll"/"mouse"): emit-only,
-- never cascaded.
feed('<C-e>')
feed('3<C-y>')
api.nvim_input_mouse('wheel', 'up', '', 0, 0, 0)
api.nvim_input_mouse('left', 'press', '', 0, 1, 2)
eq({
{ type = 'scroll', keys = k('<C-E>'), cascade = false },
{ type = 'scroll', keys = k('3<C-Y>'), cascade = false },
{ type = 'scroll', keys = k('<ScrollWheelUp>'), cascade = false },
{ type = 'mouse', keys = k('<LeftMouse>'), cascade = false },
}, atoms_tail(4, 'type', 'keys', 'cascade'))
eq(2, fn.line('.')) -- the click moved the cursor
-- <amatch>/match is the atom type, never path-expanded.
n.exec_lua([[
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
_G.last_match = ev.match
end,
})
]])
feed('0w')
n.poke_eventloop()
eq('motion', n.exec_lua('return _G.last_match'))
end)
it('captures counts and payload chars', function()
fn.setline(1, { 'abcd,ef' })
feed('gg0')
atoms_start()
feed('yw')
-- A yank emits but does not edit.
eq({ operator = 'y', changed = false }, pick(atom_last(), 'operator', 'changed'))
feed('3x')
eq({ 'd,ef' }, get_lines())
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', 'arg', 'motionforce', 'changed')
)
-- A visual atom carries the completing operator's fields, and decomposes
-- into its commands ("v", "f," and the operator).
local vis = atom_last() -- "vf,d"
eq({ type = 'visual', operator = 'd' }, pick(vis, 'type', 'operator'))
eq(
{
{ keys = 'v', cmd = 'v', changed = false },
{ keys = 'f,', cmd = 'f', arg = ',', changed = false },
{ keys = 'd', changed = true }, -- the completing operator did the edit
},
vim.tbl_map(function(c)
return pick(c, 'keys', 'cmd', 'arg', 'changed')
end, vis.atoms)
)
eq('d', vis.atoms[3].operator)
-- Operators with an interactively-typed search payload: the atom is the
-- redobuff, which includes the payload.
fn.setline(1, { 'aa END bb' })
feed('gg0')
feed('d/END<CR>')
eq({ 'END bb' }, get_lines())
eq({ k('d/END<NL>') }, atoms_tail(1))
eq(
{ operator = 'd', cmd = '/', changed = true },
pick(atom_last(), 'operator', 'cmd', 'changed')
)
-- An operand (mark or register name, target char) is its own field; the second char of a
-- two-char command NAME ("gJ") composes into `cmd`.
fn.setline(1, { 'one', 'two' })
feed('gg0magJ')
eq({ 'onetwo' }, get_lines()) -- "gJ": join without inserting a space
eq({ cmd = 'm', arg = 'a' }, pick(atoms()[#atoms() - 1], 'cmd', 'arg'))
eq({ cmd = 'gJ' }, pick(atom_last(), 'cmd', 'arg'))
local nrec = #atoms()
feed('qax') -- the recording register is an operand, not part of the name
feed('q')
eq({ cmd = 'q', arg = 'a' }, pick(atoms()[nrec + 1], 'cmd', 'arg'))
-- Forced motion type ("dvj") is a field.
fn.setline(1, { 'one', 'two' })
feed('gg0dvj')
eq(
{ operator = 'd', motionforce = 'v', cmd = 'j' },
pick(atom_last(), 'operator', 'motionforce', 'cmd')
)
end)
it('fires for typed input, never for programmatic sources', function()
atoms_start()
-- Drain deferred CmdAtom events, then return + clear the collected list.
local function take()
n.poke_eventloop()
return n.exec_lua([[local a = _G.atoms; _G.atoms = {}; return a]])
end
-- Fresh buffer + cursor via the API (no keys, so no motion atoms).
local function fresh()
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaaaaaaa', 'bbbbbbbb' })
api.nvim_win_set_cursor(0, { 1, 0 })
take()
end
-- Typed input emits: real keys (nvim_input, via feed())...
fresh()
feed('x')
eq(1, #take())
-- ...and nvim_feedkeys() with the "t" (typed) flag.
n.exec_lua([[vim.api.nvim_feedkeys('x', 't', false)]])
eq(1, #take())
-- A typed macro folds into exactly ONE atom, labeled "@q".
fresh()
feed('qqxq') -- recording is typed input
take()
feed('@q')
local evs = take()
eq(1, #evs)
eq('@q', evs[1].lhs)
-- Same for "Q" (replay the last recorded register).
feed('Q')
evs = take()
eq(1, #evs)
eq({ lhs = '@q', keys = 'dl' }, pick(evs[1], 'lhs', 'keys'))
-- Programmatic / replayed input must NOT leak any atom.
fresh()
api.nvim_buf_set_lines(0, 0, 1, true, { 'ZZZZ' }) -- API buffer edit
eq(0, #take())
api.nvim_buf_set_text(0, 0, 0, 0, 1, { 'Q' })
eq(0, #take())
command('normal! x') -- :normal!
eq(0, #take())
command('normal x') -- :normal (with mappings)
eq(0, #take())
n.exec_lua([[vim.cmd('normal! x')]])
eq(0, #take())
n.exec_lua([[vim.api.nvim_feedkeys('x', '', false)]]) -- feedkeys without "t"
eq(0, #take())
command('normal! @q') -- macro played programmatically, not typed
eq(0, #take())
n.exec_lua([[vim.api.nvim_feedkeys('@q', '', false)]])
eq(0, #take())
command('normal! yy') -- prep-exempt operator (a yank builds no redo)
eq(0, #take())
n.exec_lua([[vim.api.nvim_feedkeys('viwd', '', false)]]) -- Visual sequence
eq(0, #take())
-- A timer/scheduled API edit must NOT leak.
n.exec_lua([[
_G.done = false
vim.defer_fn(function()
vim.api.nvim_buf_set_lines(0, 0, 1, true, { 'TTTT' })
_G.done = true
end, 5)
]])
n.exec_lua('vim.wait(200, function() return _G.done end)')
eq(0, #take())
-- Sanity: typed input still emits after all the programmatic noise.
fresh()
feed('x')
eq(1, #take())
end)
it('visual atom keys re-execute on replay; unreplayable ops fall back to equal-size', 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' })
feed('gg0')
atoms_start()
feed('viwd') -- select the inner word and delete it
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.
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|).
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
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')
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
-- 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')
command('normal! Vjp')
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' })
feed('gg0vlr<C-V><CR>')
eq({ '\r\rcd', 'efgh' }, get_lines())
feed('j0.')
eq({ '\r\rcd', '\r\rgh' }, get_lines())
end)
it('a mapping can repeat the last visual atom', function()
-- User-defined Visual dot-repeat: capture a visual atom's resolved
-- `keys`, replay them verbatim from a mapping to RE-EXECUTE the
-- operation (not an equal-size reselect like builtin |.|).
n.exec_lua([[
vim.api.nvim_create_autocmd('CmdAtom', {
pattern = 'visual',
callback = function(ev)
_G.last_visual = ev.data.keys
end,
})
vim.keymap.set('n', ',', function()
-- Scheduled: runs after any pending CmdAtom event (fresh
-- `last_visual`), and emits no CmdAtom itself. See |CmdAtom|.
vim.schedule(function()
if _G.last_visual then
vim.api.nvim_feedkeys(_G.last_visual, 'n', false)
end
end)
end)
]])
--- 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
end
fn.setline(1, { 'foo bar', 'longword bar' })
feed('gg0')
atoms_start()
feed('viwd')
eq({ ' bar', 'longword bar' }, get_lines())
eq('visual', atom_last().type)
-- "iw" re-executes: it selects THAT line's (longer) word.
feed('j0,')
retry(nil, 1000, function()
eq({ ' bar', ' bar' }, get_lines())
end)
-- The scheduled replay is programmatic input: it emits no visual atom itself.
eq(1, nvisual())
-- A Visual change (insert session): the atom embeds the selection, the
-- operator, the inserted text, and <Esc>; the repeat re-executes it all.
api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar', 'longword bar' })
feed('gg0viwcX<Esc>')
eq({ 'X bar', 'longword bar' }, get_lines())
feed('j0,')
retry(nil, 1000, function()
eq({ 'X bar', 'X bar' }, get_lines())
end)
eq(2, nvisual())
end)
it('a mapping can restore equal-size visual dot-repeat', function()
-- Keep in sync with the example in runtime/doc/repeat.txt.
n.exec_lua([[
local vop ---@type string?
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
if ev.data.type == 'visual' then
local children = ev.data.atoms
vop = children and children[#children].keys or nil
elseif ev.data.changed then
vop = nil -- the last change is no longer the Visual one
end
end,
})
vim.keymap.set('n', '.', function()
vim.schedule(function()
vim.api.nvim_feedkeys(vop and ('1v' .. vop) or '.', 'n', false)
end)
end)
]])
fn.setline(1, { 'foo bar', 'longword bar' })
feed('gg0viwd')
eq({ ' bar', 'longword bar' }, get_lines())
-- Equal-size repeat: a 3-char region at the cursor, NOT that line's word.
feed('j0.')
retry(nil, 1000, function()
eq({ ' bar', 'gword bar' }, get_lines())
end)
-- A non-visual change falls back to the builtin |.|.
feed('gg0x')
eq({ 'bar', 'gword bar' }, get_lines())
feed('j0.')
retry(nil, 1000, function()
eq({ 'bar', 'word bar' }, get_lines())
end)
end)
it('one event per operation, for each kind of atom', 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 before = #atoms()
feed(keys)
local evs = atoms()
eq({ before + 1, k(expected) }, { #evs, evs[#evs].keys })
end
local lines = {}
for i = 1, 20 do
lines[i] = 'alpha beta gamma delta epsilon zeta'
end
fn.setline(1, lines)
feed('gg0')
atoms_start()
-- 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('dw', 'dw')
atom('"z2dw', '"z2dw')
atom('yy', 'yy')
atom('p', 'p')
atom('J', '2J')
atom('3J', '3J')
atom('r?', '1r?')
atom('~', '~')
atom('guiw', 'guiw')
atom('gUiw', 'gUiw')
atom('>>', '>>')
atom('dfa', 'dfa')
feed('ddk')
atom('P', 'P')
-- Insert sessions: one whole-session atom (entry + text + <Esc>).
atom('iXY<Esc>', '1iXY<Esc>')
atom('A!<Esc>', '1A!<Esc>')
atom('oNEW<Esc>', '1oNEW<Esc>')
atom('3iZ<Esc>', '3iZ<Esc>')
atom('cwWORD<Esc>', 'cwWORD<Esc>')
-- Visual: the full typed keysequence.
atom('viwd', 'viwd')
atom('Vd', 'Vd')
atom('<C-v>jd', '<C-V>jd')
-- Motions.
atom('w', 'w')
atom('3w', '3w')
atom('fb', 'fb')
atom('G', 'G')
atom('$', '$')
feed('gg0')
atom(']]', ']]')
-- Jumps: absolute/shared-state navigation, their own kind.
atom('ma', 'ma')
eq('command', atom_last().type) -- "m" sets state; it does not jump
atom('`a', '`a')
eq('jump', atom_last().type)
atom('<C-o>', '<C-O>')
eq('jump', atom_last().type)
-- Non-redoable commands: still emitted, as type "command".
atom('zz', 'zz')
eq('command', atom_last().type)
atom('u', 'u')
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
-- Payload commands: the interactively-typed cmdline completes the
-- keysequence (not a bare "/" or ":" prefix).
atom('/beta<CR>', '/beta<NL>')
eq({ type = 'motion', text = 'beta' }, pick(atom_last(), 'type', 'text'))
atom('?alpha<CR>', '?alpha<NL>')
atom('2/beta<CR>', '2/beta<NL>')
-- Ex commands: their own atom kind, the cmdline is the "text" payload.
atom(':set tw=42<CR>', ':set<Space>tw=42<NL>')
eq({ type = 'ex', text = 'set tw=42' }, pick(atom_last(), 'type', 'text'))
-- 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.
command('nnoremap ,D d')
command('nnoremap ,V v')
feed(',D')
eq({ lhs = ',D', pending = 'operator' }, pick(atom_last(), 'lhs', 'pending'))
atom('w', 'dw') -- the supplied motion completes the operation
feed(',V')
eq({ lhs = ',V', pending = 'visual' }, pick(atom_last(), 'lhs', 'pending'))
feed('<Esc>')
-- An ABORTED mapping emits nothing: an error discards its remaining
-- keys, and the composite with them.
command('nnoremap ,E :NoSuchCmd<CR>x')
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
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
-- Macro playback ("@q") emits its commands' atoms (capture-on-replay);
-- "@q" itself is a translation, never an atom.
local total = #atoms()
feed('qqxq')
feed('@q')
eq(total + 4, #atoms())
eq({ 'qq', 'dl', 'q', 'dl' }, atoms_tail(4))
-- The macro's atoms fold into one "@x"-labeled composite, like a mapping.
eq('@q', atom_last().lhs)
-- A typed scroll emits its own (emit-only) atom kind.
feed('<C-d>')
eq(total + 5, #atoms())
eq({ type = 'scroll', keys = k('<C-D>') }, pick(atom_last(), 'type', 'keys'))
-- An 'indentexpr' that runs ":normal" opens a nested command-frame mid-operator: "gq" still
-- pushes its own atom, after the edit.
exec([[
func Indent()
exe "normal! \<Ignore>"
return 0
endfunc
setlocal indentexpr=Indent() textwidth=20
]])
atom('Vgq', 'Vgq')
eq(true, atom_last().changed)
end)
it('captures non-edit operators (zfap) and fold/view commands', function()
fn.setline(1, { 'aa', 'aa', '' })
feed('gg0')
atoms_start()
feed('zfap')
eq({ 'zfap' }, atoms_tail(1))
eq('operator', atom_last().type)
eq(1, fn.foldclosed(1))
feed('za')
eq({ 'za' }, atoms_tail(1))
-- Neither an operator nor a motion: its own kind.
eq('command', atom_last().type)
eq(-1, fn.foldclosed(1))
end)
it('a single-command mapping keeps its own type and structure', function()
command('nnoremap ,d dw')
fn.setline(1, { 'one two aa bb cc dd' })
feed('gg0')
atoms_start()
feed(',d')
eq({ 'two aa bb cc dd' }, get_lines())
-- The event's structured fields mirror what is encoded in "keys"; a
-- mapping labels its atom with the typed LHS (a single-command
-- 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.
eq({
type = 'operator',
keys = 'dw',
operator = 'd',
cmd = 'w',
changed = true,
cascade = true, -- the edit is cascadable
lhs = ',d',
}, evs[#evs])
-- Count and register are captured; one event per occurrence.
feed('"z2dw')
feed('"z2dw')
evs = atoms()
eq({ keys = '"z2dw', count = 2, reg = 'z' }, pick(evs[#evs], 'keys', 'count', 'reg'))
eq(evs[#evs - 1], evs[#evs])
end)
it('a mapping with edits and motions folds into one atom', function()
-- Split the line at the cursor, ending at the EOL of the first half.
command('nnoremap gj i<c-j><esc>k$')
fn.setline(1, { 'aaa bbb' })
feed('gg04l')
atoms_start()
feed('gj')
eq({ 'aaa ', 'bbb' }, get_lines())
-- The mapping IS the atom: its commands (the insert session, k, $)
-- accumulate and fold into exactly ONE event, labeled with the typed
-- LHS; the resolved keys remain the (replayable) payload. The
-- mapping is never re-resolved.
local evs = atoms()
eq(1, #evs)
eq(
{ type = 'mapping', lhs = 'gj', keys = k('1i<NL><Esc>k$'), changed = true },
pick(evs[1], 'type', 'lhs', 'keys', 'changed')
)
-- The folded commands stay exposed, each with its own structure.
eq(
{
{ type = 'insert', keys = k('1i<NL><Esc>') },
{ type = 'motion', keys = 'k' },
{ type = 'motion', keys = '$' },
},
vim.tbl_map(function(c)
return pick(c, 'type', 'keys')
end, evs[1].atoms)
)
eq({ 'k', false }, { evs[1].atoms[2].cmd, evs[1].atoms[2].changed })
-- A scroll inside a mapping is NOT a subatom: the composite's keys must
-- stay replayable, so the scroll is elided.
fn.setline(1, { 'l1', 'l2', 'l3', 'l4', 'l5', 'l6' })
feed('3G')
command('nnoremap gk <C-e>j$')
feed('gk')
eq(4, fn.line('.'))
eq({ type = 'mapping', lhs = 'gk', keys = 'j$' }, pick(atom_last(), 'type', 'lhs', 'keys'))
-- A recursive mapping (:nmap gJ gj) does not nest: the inner mapping's commands flatten
-- into ONE composite labeled with the typed LHS, with the same resolved keys.
command('nmap gJ gj')
api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa bbb' })
feed('gg04l')
local before = #atoms()
feed('gJ')
eq({ 'aaa ', 'bbb' }, get_lines())
evs = atoms()
eq(before + 1, #evs)
eq(
{ type = 'mapping', lhs = 'gJ', keys = k('1i<NL><Esc>k$') },
pick(evs[#evs], 'type', 'lhs', 'keys')
)
end)
it('an insert session atom captures its text', function()
fn.setline(1, { 'aaa' })
feed('gg0')
atoms_start()
-- No event for the bare "i" command, and none per keystroke: ONE
-- whole-session event at <Esc>.
feed('i')
eq(0, #atoms())
feed('X')
eq(0, #atoms())
feed('Y')
eq(0, #atoms())
feed('<Esc>')
local evs = atoms()
eq(1, #evs)
-- `count` mirrors what the keys encode: insert keys always embed the count
-- ("1i…"), so it is 1 even untyped. "dw" omits its count.
eq(
{ type = 'insert', count = 1, text = 'XY', keys = k('1iXY<Esc>') },
pick(evs[1], 'type', 'count', 'text', 'keys')
)
-- Counted insert: entry cmd + text + <Esc> keys, with count and the
-- session's inserted text as fields.
feed('3iZ<Esc>')
eq({ type = 'insert', count = 3, text = 'Z' }, pick(atom_last(), 'type', 'count', 'text'))
end)
it("operatorfunc atom includes the getchar()'d payload", function()
n.exec(t_atom.minisurround_vim)
fn.setline(1, { 'alpha beta' })
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))
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).
n.exec(t_atom.delsurround_vim)
fn.setline(1, { 'a (one)' })
feed('gg0f(')
atoms_start()
feed('ds)') -- ")" is the getchar()'d payload
eq({ 'a one' }, get_lines())
local ev = atoms()[#atoms()]
eq('ds', ev.lhs)
t.matches(':call DelSurround%(%)', ev.keys)
end)
it('"," repeats the last motion atom', function()
-- Keep in sync with the example in runtime/doc/repeat.txt.
n.exec_lua([[
local last ---@type string?
vim.api.nvim_create_autocmd('CmdAtom', {
pattern = 'motion',
callback = function(ev)
last = ev.data.keys
end,
})
vim.keymap.set('n', ',', function()
-- CmdAtom delivery is deferred: schedule the replay AFTER any pending
-- event, so `last` is fresh even when "," immediately follows a
-- motion. A scheduled replay is programmatic input: it emits no
-- CmdAtom itself (no feedback loop).
vim.schedule(function()
if last then
vim.api.nvim_feedkeys(last, 'n', false) -- "n": already resolved
end
end)
end)
]])
local screen = Screen.new(30, 3)
fn.setline(1, { 'aa bb cc dd ee' })
feed('gg0')
feed('2w') -- last motion: "2w" (count included)
n.poke_eventloop() -- deliver the deferred CmdAtom before ","
screen:expect([[
aa bb ^cc dd ee |
{1:~ }|
|
]])
feed(',') -- repeated: "2w" again
screen:expect([[
aa bb cc dd ^ee |
{1:~ }|
|
]])
feed('0fb') -- last motion: "fb" (payload char included)
n.poke_eventloop()
screen:expect([[
aa ^bb cc dd ee |
{1:~ }|
|
]])
feed(',') -- repeated "fb": the second "b"
screen:expect([[
aa b^b cc dd ee |
{1:~ }|
|
]])
end)
it('activates a temporary mapping ("submode"), expired by the next unrelated atom', function()
-- Keep in sync with the example in runtime/doc/repeat.txt.
n.exec_lua([==[
local active = false
vim.api.nvim_create_autocmd('CmdAtom', {
callback = function(ev)
-- `cmd` is a key-notation name (unlike `keys`, which is raw bytes).
local resize = ev.data.cmd == '<C-W>+' or ev.data.cmd == '<C-W>-'
if resize then
-- Activate. Use of "+"/"-" resolves to the same cmd => re-activates.
vim.keymap.set('n', '+', '<C-w>+')
vim.keymap.set('n', '-', '<C-w>-')
active = true
elseif active then
vim.keymap.del('n', '+')
vim.keymap.del('n', '-')
active = false
end
end,
})
-- Also works if <c-w>+ was mapped to something else:
vim.cmd[[nnoremap <leader>+ <c-w>+]]
]==])
--- Waits for the deferred CmdAtom to (de)activate the temporary mappings.
local function wait_active(active)
eq(
true,
n.exec_lua(
[[
local active = ...
return vim.wait(1000, function()
return (vim.fn.maparg('+', 'n') ~= '') == active
end)
]],
active
)
)
end
fn.setline(1, { 'one', 'two' })
command('split')
local height = fn.winheight(0)
feed('<C-w>+')
wait_active(true) -- the deferred CmdAtom activated the mappings
eq(height + 1, fn.winheight(0))
feed('+') -- temporary mapping: resizes without the CTRL-W prefix
n.poke_eventloop()
eq(height + 2, fn.winheight(0))
feed('-') -- its own use emits the same resolved keys: stays active
n.poke_eventloop()
eq(height + 1, fn.winheight(0))
feed('j') -- any unrelated atom expires the mappings
wait_active(false)
feed('-') -- back to the builtin: a motion (up one line), not a resize
n.poke_eventloop()
eq(height + 1, fn.winheight(0))
eq(1, fn.line('.'))
-- A mapping whose atom RESOLVES to a resize also activates the submode.
feed('\\+')
n.poke_eventloop()
eq(height + 2, fn.winheight(0))
wait_active(true)
end)
end)

View File

@@ -55,67 +55,63 @@ describe('Visual mode and operator', function()
end)
it('simple change in Visual mode', function()
insert([[
apple banana cherry
-- Nvim: visual-repeat re-executes the visual operation, not a fixed-size reselect.
-- Exercise characterwise Visual mode plus operator, with count and repeat.
insert('apple banana cherry')
feed_command('/^apple')
feed('lvld.l3vd.')
expect('a ')
-- Same in linewise Visual mode.
feed_command('%delete _')
insert([[
line 1 line 1
line 2 line 2
line 3 line 3
line 4 line 4
line 5 line 5
line 6 line 6
line 6 line 6]])
feed_command('/^line 1')
feed('Vcnewline<esc>j.j2Vd.')
expect([[
newline
newline]])
-- Same in blockwise Visual mode.
feed_command('%delete _')
insert([[
xxxxxxxxxxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxx]])
-- Exercise characterwise Visual mode plus operator, with count and repeat.
feed_command('/^apple')
feed('lvld.l3vd.')
-- Same in linewise Visual mode.
feed_command('/^line 1')
feed('Vcnewline<esc>j.j2Vd.')
-- Same in blockwise Visual mode.
feed_command('/^xxxx')
feed('<c-v>jlc <esc>l.l2<c-v>c----<esc>l.')
-- Assert buffer contents.
expect([[
a y
newline
newline
--------x
--------x
xxxx--------x
xxxx--------x]])
--------
--------
xxxx--------
xxxx--------]])
end)
it('Visual mode mapping', function()
insert([[
KiwiRaspberryDateWatermelonPeach
JambuRambutanBananaTangerineMango]])
-- Nvim: visual-repeat re-executes the visual operation, not a fixed-size reselect.
-- Set up Visual mode mappings.
feed_command('vnoremap W /\\u/s-1<CR>')
feed_command('vnoremap iW :<C-U>call SelectInCaps()<CR>')
-- Do a simple change using the simple vmap, also with count and repeat.
feed_command([[call setline(1, 'KiwiRaspberryDateWatermelonPeach')]])
feed_command('/^Kiwi')
feed('vWcNo<esc>l.fD2vd.')
expect('NoNoberry')
-- Same, using the vmap that maps to an Ex command.
feed_command([[call setline(1, 'JambuRambutanBananaTangerineMango')]])
feed_command('/^Jambu')
feed('llviWc-<esc>l.l2vdl.')
-- Assert buffer contents.
expect([[
NoNoberryach
--ago]])
expect('--a')
end)
it('Operator-pending mode mapping', function()

View File

@@ -185,8 +185,8 @@ describe('listlbr', function()
1111-2222-1111-11-1111-2222-1111
Test 9: using redo after block visual mode
AaA
AaA
AAA
AAA
A
Test 10: using normal commands after block-visual

View File

@@ -119,6 +119,8 @@ describe('mapping', function()
command('imapclear')
command('set whichwrap=<,>,[,]')
feed('G3o<esc>2k')
-- Nvim: "." re-executes the whole insert session, including the (wrapping)
-- CTRL-G U <Right>, instead of only the text typed after it.
command(
[[:exe ":norm! iTest3: text with a (parenthesis here\<C-G>U\<Right>new line here\<esc>\<up>\<up>."]]
)
@@ -131,8 +133,8 @@ describe('mapping', function()
Test2: text wit a (here some more text [und undo])
new line here
Test3: text with a (parenthesis here
new line hereTest3: text with a (parenthesis here
new line here
]])
end)

View File

@@ -426,14 +426,14 @@ describe('commenting', function()
eq(get_lines(), { '# aa', '# aa', '# aa', '', ' aa', ' aa', 'aa' })
eq(get_cursor(), { 1, 0 })
-- Dot-repeat after first application in Visual mode should apply to the same
-- relative region
-- Dot-repeat after first application in Visual mode applies to the paragraph at cursor (not
-- a fixed-size region).
feed('.')
eq(get_lines(), example_lines)
set_cursor(3, 0)
feed('.')
eq(get_lines(), { 'aa', ' aa', ' # aa', ' #', ' # aa', ' aa', 'aa' })
eq(get_lines(), { '# aa', '# aa', '# aa', '', ' aa', ' aa', 'aa' })
end)
it("respects 'commentstring'", function()

View File

@@ -354,7 +354,9 @@ endfunc
" 2 2
func Test_visual_increment_14()
call setline(1, repeat(["1 1"], 2))
exec "norm! G\<C-V>k\<C-A>w."
" Nvim: "." re-executes the keysequence, so the replayed "k" needs a line
" above: repeat from the last line, like the original.
exec "norm! G\<C-V>k\<C-A>Gw."
call assert_equal(["2 2", "2 2"], getline(1, '$'))
call assert_equal([0, 1, 3, 0], getpos('.'))
endfunc
@@ -441,7 +443,9 @@ endfunc
func Test_visual_increment_18()
call setline(1, repeat(["0"], 4))
exec "norm! GV3kg\<C-A>"
exec "norm! .."
" Nvim: "." re-executes "V3kg<C-A>": replay from the last line, like the
" original.
exec "norm! G.G."
call assert_equal(["3", "6", "9", "12"], getline(1, '$'))
call assert_equal([0, 1, 1, 0], getpos('.'))
endfunc
@@ -684,9 +688,12 @@ endfunc
" Tab code, spaces and character-visual increment and redo
func Test_visual_increment_35()
call setline(1, ["\<TAB>123", " 123", "\<TAB>123", "\<TAB>123"])
" Nvim: "." re-executes "vjf3<C-A>"; the replayed "f3" fails (the numbers
" are now "124"), aborting the replay and (in :norm) flushing the rest.
exec "norm! ggvjf3\<C-A>..."
call assert_equal(["\<TAB>127", " 127", "\<TAB>123", "\<TAB>123"], getline(1, '$'))
call assert_equal([0, 1, 2, 0], getpos('.'))
exe "norm! \<Esc>"
call assert_equal(["\<TAB>124", " 124", "\<TAB>123", "\<TAB>123"], getline(1, '$'))
call assert_equal([0, 2, 9, 0], getpos('.'))
endfunc
" Tab code, spaces and blockwise-visual increment and redo
@@ -696,7 +703,9 @@ func Test_visual_increment_36()
call assert_equal([" 123", "\<TAB>556789"], getline(1, '$'))
call assert_equal([0, 1, 1, 0], getpos('.'))
exec "norm! ..."
" Nvim: "." re-executes "<C-V>kl<C-A>": replay from the last line, like the
" original.
exec "norm! G0.G0.G0."
call assert_equal([" 123", "\<TAB>856789"], getline(1, '$'))
call assert_equal([0, 1, 1, 0], getpos('.'))
endfunc

View File

@@ -268,11 +268,13 @@ endfunc
func Test_undo_after_block_visual()
call s:test_windows()
call setline(1, ["aaa", "aaa", "a"])
" Nvim: "." re-executes the captured keysequence ("<C-V>2jg~"), not an
" equal-size reselect (|visual-repeat|).
exe "norm! gg\<C-V>2j~e."
let lines = s:screen_lines([1, 3], winwidth(0))
let expect = [
\ "AaA ",
\ "AaA ",
\ "AAA ",
\ "AAA ",
\ "A ",
\ ]
call s:compare_lines(expect, lines)

View File

@@ -279,9 +279,11 @@ endfunc
func Test_break_undo()
set whichwrap=<,>,[,]
call feedkeys("G4o2k", "xt")
" Nvim: "." re-executes the whole insert session, including the (wrapping)
" CTRL-G U <Right>, instead of only the text typed after it.
exe ":norm! iTest3: text with a (parenthesis here\<C-G>U\<Right>new line here\<esc>\<up>\<up>."
call assert_equal('new line here', getline(line('$') - 3))
call assert_equal('Test3: text with a (parenthesis here', getline(line('$') - 2))
call assert_equal('Test3: text with a (parenthesis here', getline(line('$') - 3))
call assert_equal('new line hereTest3: text with a (parenthesis here', getline(line('$') - 2))
call assert_equal('new line here', getline(line('$') - 1))
set nomodified
endfunc

View File

@@ -535,9 +535,13 @@ func Test_normal09c_operatorfunc()
new
call setline(1, ['first', 'first', 'third', 'third', 'second'])
normal! 1GVjg@
normal! 5G.
normal! 3G.
call assert_equal(['_____', '_____', '_____', '_____', '______'], getline(1, '$'))
" Nvim: "." re-executes the captured keysequence ("Vjg@"), not an equal-size
" reselect. At the last line the replayed "j" fails, aborting the replay:
" nothing is applied (and the pending Visual mode is left active).
normal! 5G.
exe "normal! \<Esc>"
call assert_equal(['_____', '_____', '_____', '_____', 'second'], getline(1, '$'))
bwipe!
set operatorfunc=
endfunc

View File

@@ -505,8 +505,10 @@ func Test_visual_mode_op()
call setline(1, 'apple banana cherry')
call cursor(1, 1)
" Nvim: "." re-executes the captured keysequence (|visual-repeat|): the final
" "." replays "3vd", multiplying the previous (grown) selection again.
normal lvld.l3vd.
call assert_equal('a y', getline(1))
call assert_equal('a ', getline(1))
call setline(1, ['line 1 line 1', 'line 2 line 2', 'line 3 line 3',
\ 'line 4 line 4', 'line 5 line 5', 'line 6 line 6'])
@@ -518,10 +520,10 @@ func Test_visual_mode_op()
call setline(1, ['xxxxxxxxxxxxx', 'xxxxxxxxxxxxx', 'xxxxxxxxxxxxx',
\ 'xxxxxxxxxxxxx'])
exe "normal \<C-V>jlc \<Esc>l.l2\<C-V>c----\<Esc>l."
call assert_equal([' --------x',
\ ' --------x',
\ 'xxxx--------x',
\ 'xxxx--------x'], getline(1, '$'))
call assert_equal([' --------',
\ ' --------',
\ 'xxxx--------',
\ 'xxxx--------'], getline(1, '$'))
bwipe!
endfunc
@@ -545,15 +547,18 @@ func Test_visual_mode_maps()
vnoremap W /\u/s-1<CR>
vnoremap iW :<C-U>call SelectInCaps()<CR>
" Nvim: a selection extended by a search or Ex motion is not replayable:
" "." falls back to an equal-size reselect ("1v"), like Vim. But "." after
" "2vd" re-executes "2vd", multiplying the previous (grown) area again.
call setline(1, 'KiwiRaspberryDateWatermelonPeach')
call cursor(1, 1)
exe "normal vWcNo\<Esc>l.fD2vd."
call assert_equal('NoNoberryach', getline(1))
call assert_equal('NoNoberry', getline(1))
call setline(1, 'JambuRambutanBananaTangerineMango')
call cursor(1, 1)
exe "normal llviWc-\<Esc>l.l2vdl."
call assert_equal('--ago', getline(1))
call assert_equal('--a', getline(1))
vunmap W
vunmap iW