Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
A basic overview of how to manage dot-repeating in your Neovim plugin, as well as manipulate it to "force" what action is repeated.

Adding dot-repeat to your Neovim plugin

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.

The Basics

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".

Using operatorfunc

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"

_G.callback = function()
    my_count = my_count + 1
    print("Count: " .. my_count)

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"
    print("Your name is: " .. my_name)

_G.callback = function()
    if not my_name then
        my_name = vim.fn.input("Enter your name: ")

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.

Final Thoughts

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 💖

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment