AboutWritings

Dot-Repeat in Vim and Neovim

6 min read

Vim (and Neovim) is the most powerful text editors. And one of the most powerful and my personal favourite feature is . keymap. Also commonly known as dot-repeat.

Dot-Repeat

. allows you to repeat the last action for as many times you want in NORMAL mode. By default, dot-repeat only works for action that changes the content of the buffer like inserting, deleting, replacing text etc. For example, if you press d3w to delete 3 words; then press . to repeat the same action again.

You can also prefix . with a count to repeat the action exact number of times. For example, Press yy to copy the line; Then p to paste (this changes the buffer content); Lastly, press 10. to paste the line 10 times.


NOTE: The examples below focuses more on Neovim 0.7 + Lua but the same is applicable on Vim + vimscript.


Bring Your Own Dot

The native dot repeat is powerful but; Can we make our own dot repeat action? Yes, and We can do some cool stuff with it. Just like I did in Comment.nvim  which provides code comments keymap/action and allows you to repeat them using .

A simple dot repeat mapping looks like this

dot-repeat.lua
local counter = 0 function _G.__dot_repeat(motion) -- 4. if motion == nil then vim.o.operatorfunc = "v:lua.__dot_repeat" -- 3. return "g@" -- 2. end print("counter:", counter, "motion:", motion) counter = counter + 1 end vim.keymap.set("n", "gt", _G.__dot_repeat, { expr = true }) -- 1.

vimscript

dot-repeat.vim
let s:counter = 0 function DotRepeat(motion = v:null) " 4. if a:motion == v:null set operatorfunc=DotRepeat " 3. return 'g@' " 2. endif echo 'counter:' s:counter 'motion:' a:motion let s:counter += 1 endfunction nnoremap <expr> gt DotRepeat() " 1.

Let’s break this down

  1. Keymap is added using vim.keymap.set api with { expr = true } as options
  2. g@ is an operator that calls the function set by the operatorfunc
  3. vim.o.operatorfunc is where we set a function to be called by g@. Here we set it’s value to v:lua.__dot_repeat where v:lua is an interface to call any lua expression like __dot_repeat.
  4. __dot_repeat is a global function in lua and motion parameter is a string that denotes a motion

TIP: If you are a lua plugin author, you can set operatorfunc using this syntax "v:lua.require'my-plugin'.repeat_function"

How does it all work?

When you press gt it will execute __dot_repeat function with argument motion = nil. So we update the operatorfunc value and return g@ but because we specified { expr = true }, neovim will also execute g@ operator returned by the function.

After g@ is executed, you’ll enter Operator-pending-mode where neovim will wait for any motion wbhjkl or text-object iwa{i]at keys to be pressed and then executes the function set by the operatorfunc with the a string argument.

Finally, press . which executes the last [count]g@{motion}. You can see the counter incrementing in the command area.


For example, If you press gtk then the flow will look something like this:

gt | -> __dot_repeat(motion = nil) | -> operatorfunc = 'v:lua.__dot_repeat' -> return g@ | Operator-pending-mode | -> k | -> call operatorfunc | -> __dot_repeat(motion = 'line') | -> print(counter, motion) -> inc counter

For dot-repeat, it will look like

dot (.) | -> g@k | -> call operatorfunc | -> __dot_repeat(motion = 'line') | -> print(counter, motion) -> inc counter

Count Support

count is a number which get its value when you press any number keys i.e., 0-Infinity and using vim.v.count (starts from 0) or vim.v.count1 (starts from 1) we can read it.

In vimscript, use v:count and v:count1 variables

There are two ways that anyone would want to use count with dot-repeat

  1. {count}. - To . repeat the action count times
  2. {count}gt{motion} then . - Here count is a part of keymap and . is repating the keymap only 1 time

Fortunately, both of these cases are same and can be supported with a single function

count-repeat.lua
function _G.__dot_repeat(motion) if motion == nil then vim.o.operatorfunc = "v:lua.__dot_repeat" return "g@" end -- Print vim.v.count lines from the current cursor position local row = unpack(vim.api.nvim_win_get_cursor(0)) local lines = vim.api.nvim_buf_get_lines(0, row - 1, (row + vim.v.count) - 1, false) print(vim.inspect(lines)) end vim.keymap.set("n", "gt", _G.__dot_repeat, { expr = true })

vimscript

count-repeat.vim
function DotRepeat(motion = v:null) if a:motion == v:null set operatorfunc=DotRepeat return 'g@' endif " Prints v:count lines from the current cursor position let curpos = getcurpos() echom getline(curpos[1], (curpos[1] + v:count) - 1) endfunction nnoremap <expr> gt DotRepeat()

Pressing 10gtk will print 10 lines from the current cursor position. And pressing . will repeat the same as 10g@k.

NOTE: If you press 20. after 10gtk then value of vim.v.count will be 20 instead of 10

Using Motion

The value of motion argument could be one of line, char or block and we can check it to see which motion was used. And using '[ and '] marks we can get the precise range of the motion.

motion-repeat.lua
function _G.__dot_repeat(motion) if motion == nil then vim.o.operatorfunc = "v:lua.__dot_repeat" return "g@" end if motion == "char" then print("motion on the same line i.e., f{char} b{char} [count]w etc.") elseif motion == "line" then print("motion over multiple lines i.e., [count]k [count]j etc.") elseif motion == "block" then print("IDK when this happens") end local range = { starting = vim.api.nvim_buf_get_mark(0, "["), ending = vim.api.nvim_buf_get_mark(0, "]"), } print(vim.inspect(range)) end vim.keymap.set("n", "gt", _G.__dot_repeat, { expr = true })

vimscript

motion-repeat.vim
function DotRepeat(motion = v:null) if a:motion == v:null set operatorfunc=DotRepeat return 'g@' endif if a:motion == "char" echom "motion on the same line i.e., f{char} b{char} [count]w etc." elseif a:motion == "line" echom "motion over multiple lines i.e., [count]k [count]j etc." elseif a:motion == "block" echom "IDK when this happens" end let range = {} let range.starting = getpos("'[") let range.ending = getpos("']") echom range endfunction nnoremap <expr> gt DotRepeat()

Pressing gt10j (10j is the motion) will print the range of motion. And . will repeat the same as 10g@j

NOTE: nvim_buf_get_mark api only accepts the mark name, excluding the ' character

Visual Mode

We can use the same function in VISUAL mode with a little trick. IMO dot-repeat is not that useful in visual mode so I left it out.

visual-repeat.lua
function _G.__dot_repeat(motion) local is_visual = string.match(motion or '', "[vV]") -- 2. if not is_visual and motion == nil then vim.o.operatorfunc = "v:lua.__dot_repeat" return "g@" end if is_visual then print("VISUAL mode") else print("NORMAL mode") end local range = { -- 3. starting = vim.api.nvim_buf_get_mark(0, is_visual and "<" or "["), ending = vim.api.nvim_buf_get_mark(0, is_visual and ">" or "]"), } print(vim.inspect(range)) end vim.keymap.set("n", "gt", _G.__dot_repeat, { expr = true }) vim.keymap.set("x", "gt", "<ESC><CMD>lua _G.__dot_repeat(vim.fn.visualmode())<CR>") -- 1.

vimscript

visual-repeat.vim
function DotRepeat(motion = v:null) let is_visual = a:motion == 'V' && a:motion == 'v' " 2. if !is_visual && a:motion == v:null set operatorfunc=DotRepeat return 'g@' endif if is_visual echom "VISUAL mode" else echom "NORMAL mode" end let range = {} " 3. let range.starting = getpos(is_visual ? "'<" : "'[") let range.ending = getpos(is_visual ? "'>" : "']") echom range endfunction nnoremap <expr> gt DotRepeat() xnoremap gt <ESC><CMD>call DotRepeat(visualmode())<CR> " 1.
  1. We have to <ESC> first and only after that visual marks '< and '> are populated

    • vim.fn.visualmode() returns one of v, V or <CTRL-v>
  2. Checking motion for any VISUAL mode characters

  3. Using '< and '> marks to get the selection range

NOTE:

  1. I am using <ESC><CMD> instead of :<C-u> in the keymap to avoid triggering CmdLineEnter autocmd
  2. I am not handling VISUAL-BLOCK mode i.e., <CTRL-v> or dot-repeat. Maybe you could do it(?)

:help is available for most of the topic described in this post :)