In Neovim, the .
character repeats "the most recent action"; however, this is not always respected by plugin actions. Here we will explore how to build dot-repeat support directly into your plugin, bypassing the requirement of dependencies like repeat.vim.
When some buffer-modifying action is performed, Neovim implicitly remembers the operator (e.g. d
), motion (e.g. iw
), and some other miscellaneous information. When the dot-repeat command is called, Neovim repeats that operator-motion combination. For example, if we type ci"text<Esc>
, then we replace the inner contents of some double quotes with text
, i.e. "hello world"
→ "text"
. Dot-repeating from here will do the same, i.e. "more samples"
→ "text"
.
The trick is that this also applies to operatorfunc
, which is a special type of operator that calls a user-defined function whenever g@
is run in normal mode. Whenever the g@
operator is called with some motion, the [
and ]
marks are set to the beginning/end of that motion, and whatever function is stored in operatorfunc
gets called.
Note: Plugin-provided functions can be accessed via the v:lua
variable.
Make sure to omit parentheses when requiring the module, e.g.
vim.go.operatorfunc = "v:lua.require'nvim-surround'.normal_callback"
Then dot-repeating the action will use g@[motion]
, re-calling your function with the corresponding motion.
Note: If you're not concerned with the actual motion itself, I would
recommend calling the operatorfunc
with g@l
, as it keeps the cursor in-place
while calling the function.
Understanding this might be difficult, so let's take a look at some examples!
_G.my_count = 0
_G.main_func = function()
my_count = 0
vim.go.operatorfunc = "v:lua.callback"
return "g@l"
end
_G.callback = function()
my_count = my_count + 1
print("Count: " .. my_count)
end
vim.keymap.set("n", "<CR>", main_func, { expr = true })
In the above example, pressing <CR>
in normal mode will reset the counter and call the callback function, printing Count: 1
every time. However, dot-repeating the action will directly call the callback function, skipping the reset. This allows us to differentiate between manually calling the function and calling it by dot-repeating, which can be very useful.
Consider the following example that caches user input and uses it when dot-repeating, querying the user otherwise:
_G.my_name = nil
_G.main_func = function(name)
if not name then
my_name = nil
vim.go.operatorfunc = "v:lua.callback"
return "g@l"
end
print("Your name is: " .. my_name)
end
_G.callback = function()
if not my_name then
my_name = vim.fn.input("Enter your name: ")
end
main_func(my_name)
end
vim.keymap.set("n", "<CR>", main_func, { expr = true })
This is a slightly more complicated example that makes use of a "cache" variable, my_name
. The new control flow is now:
- User hits
<CR>
main_func
is called with no arguments- The cache is cleared
callback
is called- Since there is no cache, the user is queried for input
main_func
is re-called with the name, and it prints
- User hits
.
callback
is called directly- Since the cache hasn't been cleared, user input is skipped
main_func
is re-called with the name, and the old name is printed
Furthermore, if the motion doesn't matter or is known, e.g. g@l
, then we can actually call literally anything in between, including setting/calling other operatorfuncs, and restore dot-repeat capabilities after. All we need to do is reset operatorfunc
to the desired callback function, and then reset the motion. Consider this snippet from my plugin nvim-surround
. It first sets the most recently used action to g@l
(which calls a NOOP function), then sets operatorfunc
to the desired function.
If you liked this gist and/or found it helpful, leave it a ⭐ to let me know! Also feel free to leave any questions, suggestions, or mistakes in the comments below 💖
@chrisgrieser Since the dot-repeat command can reproduce
g@[motion]
commands, you would utilize that in order to dot-repeat any sort of motion. Thus you would work within the scope of youropfunc
in order to get the intended behavior. For example, ifopfunc=v:lua.callback
, then if you didg@iw
it would callcallback
with the appropriate marks set. Then if you moved the cursor and hit.
, it would call the callback function again, but this time with the marks set for the word that the cursor is sitting on.