Watches how you actually edit, and tells you — in one buffer, when you ask — where there was a shorter way. Your config says how you mean to edit; this measures how you actually do.
fingers.vim.pro · :h fingers
Everything is observed live. Nothing interrupts you: findings surface only in
the fingers buffer, which you open with :Fingers.
fingers.nvim how you actually edit
this week, one thing
j×14 in a row ×9 this week
try: {count}j, } , or <C-d>
what your editing looked like
j×14 in a row ×31 this wk 9 · last 22 ↘
moving down a line at a time
try: {count}j, } , or <C-d>
f{char} retyped for the same target ×8
repeating a find by spelling it out again
try: ; repeats the last f/t in one key (and , goes back)
<BS>×11 in insert mode ×6
erasing a character at a time
try: <Esc>, then db or daw — or <C-w> without leaving insert
drilled, then 2 of 5 reached your editing
visual-block drilled 9d ago ×9 since
macro drilled 9d ago ×2 since
increment drilled 1d ago not once since
global drilled 9d ago not watched — typed on the command line, which fingers never reads
phrases you keep typing a mapping nobody wrote yet
A;j ×23 a 2-key mapping would have saved 23 keys
techniques in the wild
. ×48 dot repeat
ciw ×12 change inner word
never sighted · 23 @@ ; , caw diw daw dap yiw ci( ci" …+13
the mouse
clicks ×132 · scroll ×460 this wk 49 · last 126 ↘
your mappings 4 of 6 pressed
<leader>ff 214 2026-07-26 find files
<leader>gs 89 2026-07-27 git status
<leader>ca 41 2026-07-26 code action · 1.6s pause before it
<leader>rn 12 2026-07-18 rename
never fired · 2
<leader>gd git diff
<leader>xx diagnostics list
beyond the leader — plugins & remaps 0 of 1 pressed
never fired · 1
ys add surround
counted locally; nothing has left this machine
The never-fired lists are the point. Everything else you already knew — except perhaps the pause: a mapping you stop in front of every time has a recall cost its fire-count can't see.
No account. No server. Nothing automatic ever touches the network, and
nothing is ever sent. Two calls exist, both explicit and both download-only:
:Fingers census and :Fingers practice, each a GET of a public page on
vim.pro (below).
Counts live in a single JSON file in your own data directory:
:lua= require("fingers.store").path()
That file holds mapping names, counts, dates, timings, and short normal-mode command phrases (the mining table). It does not hold insert-mode text, the files you opened, or anything about your projects — printable characters in insert mode are dropped at the watch layer's gate before any other module can see them, a rule the test suite enforces rather than documents. Read the file, diff it, delete it — deleting it erases your history completely, and the plugin will start over without complaint.
Any plugin manager, no configuration required.
-- lazy.nvim
{ "vim-pro/fingers.nvim" }
-- vim.pack
vim.pack.add({ "https://github.com/vim-pro/fingers.nvim" })Then edit normally for a week and run :Fingers.
| command | what it does |
|---|---|
:Fingers |
the fingers buffer (q closes it) |
:Fingers all |
the same, with the never-fired cap lifted |
:Fingers export |
the report as a markdown buffer, for sharing |
:Fingers census |
fetch the vim.pro mapping census (a public aggregate) |
:Fingers practice |
fetch your vim.pro practice record, to see what transferred |
:Fingers live |
per-session whispers after a finding — same command off |
:Fingers rescan |
re-read your mappings (after a lazy-loaded plugin lands) |
:Fingers reset |
forget every count and finding, start over |
:Fingers flush |
write to disk now (otherwise: on exit) |
scripts/fingers renders the findings half in a terminal, from the counts
file alone.
Lowercase :fingers works too. Neovim reserves lowercase command names, so
it's a command-line abbreviation rather than a command of its own: it expands
only when it's the whole line, and nothing built-in answers to that word
(:fin[ish] stops at finish). Set abbrev = false to leave it alone.
require("fingers").setup({
modes = { "n", "x" }, -- modes counted; insert passes only control keys
census = "all", -- count every typable mapping; "leader" narrows it
coach = true, -- false = mapping counts only, no live observer
techniques = true, -- tally native idioms (ciw, ., *, ``…) seen in the wild
mine = true, -- mine recurring phrases: the mapping you don't have yet
run_threshold = 5, -- repeats of one key before it's worth mentioning
weekly_note = false, -- opt-in: one notify line, at most weekly, findings only
site = "https://vim.pro", -- where census and practice read from
login = "", -- your vim.pro login, for :Fingers practice
abbrev = true, -- let `:fingers` expand to `:Fingers`
enabled = true, -- false loads the plugin without counting
})
require("fingers").stats() -- the numbers, for your own statusline:checkhealth fingers explains the usual problem, which is a lazy-loaded
config whose mappings didn't exist yet when counting started.
One rule governs every detector: the waste has to be arithmetic, not
aesthetic. "You pressed j fourteen times to move fourteen lines" is a fact,
and 14 keys against 3 is a sum anyone can check. "You use w more than f" is
taste, it's usually wrong, and it will never appear here — both are correct for
different jobs.
The current inventory, every entry naming its shorter form: runs of
j k h l x X w b n N and the arrow keys in normal mode; runs of <BS> and
arrows inside insert mode (seen through the control-keys-only gate); retyping
f{char} for the target ; already remembers; and v + one motion + operator
where operator + motion is a key shorter.
Some things are counted but never coached, because no honest arithmetic
applies: mouse clicks and scrolling (for anyone leaving the mouse behind, the
truest metric is that line shrinking on its own), and sightings of native
techniques like ciw and . — evidence of practice, not a homework list.
The phrases section is the census inverted: recurring normal-mode phrases with no name yet, each earning its place by (keys − 2) × count — what a two-key mapping would have saved. Phrases you already bound are excluded; noticing you never use them is the census's job.
There is no score, no streak, and no efficiency rating. A shorter way to do the thing you just did is coaching; a percentage is a game, and this isn't one.
It watches the key stream (vim.on_key) and matches it against the mappings
your editor reports. Your mappings are never wrapped or re-registered, so
nothing here can change what they do or break an expr or <Plug> mapping.
Things that deliberately do not count:
- Replayed macros. A macro is one decision, however many keys it emits. Counting the replay would inflate exactly the mappings you automated.
- Dot-repeats.
.replays the change, not your keystrokes, so repeating a mapping's effect doesn't credit the mapping again. Press<leader>xthen.and the count is 1: your fingers reached the mapping once. - Keys a mapping produced. If
<leader>xexpands tociwfoo<Esc>, theciwinside it is not a technique you typed — one decision, already counted. - Ambiguous prefixes, until they resolve. If you have both
<leader>gand<leader>gs, typing<leader>gscredits only<leader>gs— the same rule vim itself uses viatimeoutlen. - nvim's own stock mappings. The census is what you added. The baseline
is measured against a bare
nvim --cleanand subtracted, not guessed from metadata. - Insert-mode text, ever.
Your editor can say "you never press <leader>gd". Only a population can
finish the sentence. :Fingers census fetches the
vim.pro mapping census — one GET of a public aggregate,
nothing sent — and from then on the never-fired list carries facts like:
<leader>gd git diff · 3 of 5 census configs bind this
<leader>xx diagnostics list · nobody on the census binds it either
Delete or learn: the data is stated, the verdict stays yours. The result is cached in the counts file, so the annotation works offline until you refresh.
A drill teaches you a technique. Whether it stuck is a separate question, and a harder one to answer.
:Fingers practice reads your public vim.pro profile — which techniques you
drilled, when, and the key pattern each one looks like — and from then on
watches for those patterns in your real editing. The practice record comes
from the site; the keys are counted here:
drilled, then 2 of 5 reached your editing
visual-block drilled 9d ago ×9 since
increment drilled 1d ago not once since
global drilled 9d ago not watched — typed on the command line, which fingers never reads
Only sightings on or after the day you drilled count. One from before means you already knew it, and calling that transfer would take credit for the drill.
Download only. The pattern comes down, the match runs here. Nothing about
your editing is sent, and there is no code in the plugin that could send it.
Set login and run it:
require("fingers").setup({ login = "your-vim-pro-login" })What it can't watch, and says so. Techniques typed on the command line —
:g, :s, :norm, :sort — can never match, because fingers never reads
what you type after :. Those rows say not watched with the reason rather
than showing a zero, because silence from a blind spot is not evidence of
failure.
- A mapping installed after counting starts is invisible until the first
idle rescan (or
:Fingers rescan). Counting begins atVimEnter. - Buffer-local mappings (an LSP
on_attach, a filetype plugin) are not in the census —nvim_get_keymapreads the global table. - A mapping that overrides one of nvim's stock lhs (say, rebinding
gcc) is subtracted with the stock vocabulary. The count still accumulates; the row doesn't render yet. - Counts are held in memory and written on exit — atomically, and merged, so two instances no longer clobber each other; a hard crash still loses the current session rather than corrupting the file.
Two connections exist — the census cross-reference and practice transfer — and both are the same shape on purpose: an explicit command, a GET of a public page, nothing sent, cached locally. The site hands down what it knows and every join runs on your machine.
This plugin is useful with the network unplugged; that's the design, not a staging post. What's still missing is what an editor cannot know on its own:
- History. Your config's changelog lives on vim.pro, so a mapping can be dated: "added fourteen months ago, never fired" and "added Tuesday" are different advice.
- Your editing, read by the coach that already reads your config.
Both would need an account, and neither would change the rule above: nothing leaves without you asking, every time.
The direction of travel is fixed: vim.pro hands down what it knows, the plugin computes everything locally, and only aggregates you explicitly agree to are ever uploaded — never keys, never phrases, never text.
./scripts/test # every spec, headless
./scripts/test watch # just the matching onestests/integration_spec.lua drives a real child nvim over RPC and types at
it. It's there because a full unit suite once passed while the plugin counted
nothing at all: the matcher was right, the storage was right, and the key
handler read the wrong argument. Only real keystrokes catch that.
MIT