Dot-Repeat in Vim and Neovim
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 (opens in a new tab) which provides code comments keymap/action and allows you to repeat them using .
A simple dot repeat mapping looks like this
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
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.</div>Let's break this down
- Keymap is added using
vim.keymap.setapi with{ expr = true }as options g@is an operator that calls the function set by theoperatorfuncvim.o.operatorfuncis where we set a function to be called byg@. Here we set it's value tov:lua.__dot_repeatwherev:luais an interface to call any lua expression like__dot_repeat.__dot_repeatis a global function in lua andmotionparameter is a string that denotes a motion
TIP: If you are a lua plugin author, you can set
operatorfuncusing 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 counterFor dot-repeat, it will look like
dot (.)
|
-> g@k
|
-> call operatorfunc
|
-> __dot_repeat(motion = 'line')
|
-> print(counter, motion)
-> inc counterCount 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:countandv:count1variables
There are two ways that anyone would want to use count with dot-repeat
{count}.- To.repeat the actioncounttimes{count}gt{motion}then.- Herecountis 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
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
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()</div>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.countwill 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.
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
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()</div>Pressing gt10j (10j is the motion) will print the range of motion. And . will repeat the same as 10g@j
NOTE:
nvim_buf_get_markapi 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.
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
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.</div>-
We have to
<ESC>first and only after that visual marks'<and'>are populatedvim.fn.visualmode()returns one ofv,Vor<CTRL-v>
-
Checking
motionfor anyVISUALmode characters -
Using
'<and'>marks to get the selection range
NOTE:
- I am using
<ESC><CMD>instead of:<C-u>in the keymap to avoid triggeringCmdLineEnterautocmd- I am not handling
VISUAL-BLOCKmode i.e.,<CTRL-v>or dot-repeat. Maybe you could do it(?)
> `:help` is available for most of the topic described in this post :)