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.set
api with{ expr = true }
as options g@
is an operator that calls the function set by theoperatorfunc
vim.o.operatorfunc
is where we set a function to be called byg@
. Here we set it's value tov:lua.__dot_repeat
wherev:lua
is an interface to call any lua expression like__dot_repeat
.__dot_repeat
is a global function in lua andmotion
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
andv:count1
variables
There are two ways that anyone would want to use count
with dot-repeat
{count}.
- To.
repeat the actioncount
times{count}gt{motion}
then.
- Herecount
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
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.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.
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_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.
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
,V
or<CTRL-v>
-
Checking
motion
for anyVISUAL
mode characters -
Using
'<
and'>
marks to get the selection range
NOTE:
- I am using
<ESC><CMD>
instead of:<C-u>
in the keymap to avoid triggeringCmdLineEnter
autocmd- 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 :)