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_funcis called with no arguments- The cache is cleared
callbackis called- Since there is no cache, the user is queried for input
main_funcis re-called with the name, and it prints
- User hits
.callbackis called directly- Since the cache hasn't been cleared, user input is skipped
main_funcis 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 💖
Thank you for this example!
Could you also explain how to make text objects dot repeatable? The example works fine for normal mode, but I can't figure out how to get custom text objects being being repeated correctly.