Skip to content

Instantly share code, notes, and snippets.

@kylechui
Last active April 8, 2024 08:20
Show Gist options
  • Star 58 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kylechui/a5c1258cd2d86755f97b10fc921315c3 to your computer and use it in GitHub Desktop.
Save kylechui/a5c1258cd2d86755f97b10fc921315c3 to your computer and use it in GitHub Desktop.
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.

Examples

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.

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 💖

@kylechui
Copy link
Author

To be honest, I'm not actually 100% sure how omap works; I don't know if you need to use opfunc for this at all. I would have assumed that once you had the keymap set up, dot-repeating would "just work properly". The little blurb that I originally wrote was supposed to try and help with the problem of dot-repeating a custom action over some motion, rather than define a custom motion.

@chrisgrieser
Copy link

Meh, too bad. Thanks for the effort anyway! Hope I'll find info on it somewhere else

@xsh005
Copy link

xsh005 commented Jan 17, 2024

A wonderful gem! Thank you for sharing this :D

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