mirror of
https://github.com/neovim/neovim.git
synced 2026-08-25 00:21:55 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
==============================================================================
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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|,
|
||||
|
||||
@@ -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*
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
runtime/lua/vim/_meta/api_keysets.gen.lua
generated
1
runtime/lua/vim/_meta/api_keysets.gen.lua
generated
@@ -105,6 +105,7 @@ error('Cannot require a meta file')
|
||||
--- |'ChanClose'
|
||||
--- |'ChanInfo'
|
||||
--- |'ChanOpen'
|
||||
--- |'CmdAtom'
|
||||
--- |'CmdUndefined'
|
||||
--- |'CmdlineChanged'
|
||||
--- |'CmdlineEnter'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
1
runtime/lua/vim/_meta/options.gen.lua
generated
1
runtime/lua/vim/_meta/options.gen.lua
generated
@@ -2193,6 +2193,7 @@ vim.go.ei = vim.go.eventignore
|
||||
--- `ChanClose`,
|
||||
--- `ChanInfo`,
|
||||
--- `ChanOpen`,
|
||||
--- `CmdAtom`,
|
||||
--- `CmdUndefined`,
|
||||
--- `CmdlineChanged`,
|
||||
--- `CmdlineEnter`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
726
src/nvim/input.c
726
src/nvim/input.c
File diff suppressed because it is too large
Load Diff
1123
src/nvim/input_cmdatom.c
Normal file
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
30
src/nvim/input_cmdatom.h
Normal 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"
|
||||
81
src/nvim/input_cmdatom_defs.h
Normal file
81
src/nvim/input_cmdatom_defs.h
Normal 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.
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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--;
|
||||
|
||||
|
||||
@@ -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().
|
||||
|
||||
231
src/nvim/ops.c
231
src/nvim/ops.c
@@ -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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <lauxlib.h> // for LUA_NOREF
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
|
||||
#include <stdint.h>
|
||||
typedef uint64_t Timestamp;
|
||||
|
||||
#define NS_PER_SEC 1000000000U ///< Nanoseconds per second.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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, ®, &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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
94
test/functional/editor/atom_testutil.lua
Normal file
94
test/functional/editor/atom_testutil.lua
Normal 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
|
||||
866
test/functional/editor/cmdatom_spec.lua
Normal file
866
test/functional/editor/cmdatom_spec.lua
Normal 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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user