fingers.nvim watches how you actually edit and tells you — in one buffer, when you ask — where there was a shorter way.
Your config is a claim about how you edit. This measures the claim, and the editing around it.
14 against 3 is a sum anyone can check. That is the only kind of thing this plugin will say to you.
:Fingers.
Everything is observed live and nothing interrupts you. There is no notification, no virtual text, no flash at the moment you press the key — being corrected mid-edit is how a coach becomes a thing you disable. It all waits in the fingers buffer, which q closes.
fingers.nvim how you actually edit what your editing looked like j×14 in a row ×6 moving down a line at a time try: {count}j, } , or <C-d> vwd ×3 selecting first, then operating try: dw does the same edit in one key fewer your mappings 9 of 16 pressed <leader>gs 142 2026-07-25 git status <leader>w 88 2026-07-25 write <leader>ff 31 2026-07-24 find files <leader>fg 24 2026-07-25 live grep <leader>e 19 2026-07-23 file explorer <leader>ca 11 2026-07-22 code action <leader>gb 6 2026-07-21 git blame <leader>rn 4 2026-07-18 rename symbol <leader>db 1 2026-07-11 toggle breakpoint never fired · 7 <leader>dc debug continue <leader>gL git log (graph) <leader>ld lsp diagnostics list <leader>sr search and replace <leader>tt terminal toggle <leader>xq quickfix toggle <leader>zz zen mode counted locally; nothing has left this machine
An illustration of the format, with made-up mappings — the
numbers are only ever your own. In your editor the buffer takes its colours
from your colorscheme: it links to Title, String,
Identifier, Number, DiagnosticWarn and
Comment with default = true, so it looks native
wherever you run it, and you can override any of it.
Everything else you already knew. You knew you press <leader>gs
a lot. You did not know that seven mappings you wrote, named, and
remembered writing have never once fired.
Mappings are read from the editor itself, via
nvim_get_keymap — never by parsing your config. Parsing would
be a guess: it cannot see a mapping a plugin made, a conditional branch,
or a vim.keymap.set behind a lazy load.
. replays the change,
not your keystrokes. Press <leader>x then .
and the count is 1: your fingers reached the mapping once.<leader>g and <leader>gs bound, typing
<leader>gs credits only <leader>gs — the
same rule vim itself uses via timeoutlen.No account. No server. No network calls — the plugin contains no code
that makes one. :checkhealth fingers states it as a check
rather than a promise.
A single JSON file, and nowhere else. Read it, diff it, delete it.
:lua= require("fingers.store").path() -- stdpath("data")/fingers/counts.json
It does not hold the keys you pressed, the text you typed, the files you opened, or anything about your projects.
Delete the file and your history is erased entirely; the plugin starts
over without complaint. :Fingers reset does the same from inside
the editor.
Insert mode is where text lives. The same goes for the contents of a
: or / line, which are never recorded.
It watches the key stream via 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.
“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. If a proposed detector can't state a smaller keystroke count for the
same outcome, it isn't a detector.
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.
Runs of a single key past run_threshold (five by default),
where the alternative is named rather than gestured at:
And selecting before operating: v,
one motion, then d, y or c — one key
more than the operator and motion for the identical edit. Only a single
motion counts, since a multi-motion selection isn't necessarily
expressible as one operator plus motion.
-- 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 reset | forget every count and finding, start over |
:Fingers flush | write to disk now (otherwise: on exit) |
:checkhealth fingers | explains the usual problem, which is a lazy-loaded config whose mappings didn't exist yet when counting started |
Lowercase :fingers works too — Neovim reserves
lowercase command names, so it's a command-line abbreviation for
:Fingers rather than a command of its own. It expands only when
it's the whole line, and nothing built-in answers to that word. Set
abbrev = false to leave the command line alone.
require("fingers").setup({ modes = { "n", "x" }, -- modes watched; never insert coach = true, -- false = mapping counts only, no live observer run_threshold = 5, -- repeats of one key before it's worth mentioning abbrev = true, -- let `:fingers` expand to `:Fingers` enabled = true, -- false loads the plugin without counting }) require("fingers").stats() -- the numbers, for your own statusline
A mapping installed after counting starts isn't counted until the next
session. Counting begins at VimEnter and rescans once on the
first CursorHold, which covers most lazy-loading, but not all
of it.
Counts are held in memory and written on exit, so a crash costs you the
current session rather than corrupting the file. :Fingers flush
writes immediately.
The census covers mappings that begin with your leader. They're
explicitly authored — nobody accidentally binds
<leader>gs — and they start with a known key, which is
what makes matching a typed sequence possible at all.
The plugin is useful without it — that's the design, not a staging post. When it exists it will be opt-in and strictly additive: the site knows things an editor cannot, and that asymmetry is the only reason to connect.
Your config's changelog lives on vim.pro, so a mapping can be dated: “you added this three weeks ago and have pressed it twice.”
“You're one of 4% who bind this; the rest use gd.”
Rather than a read of your config.
The direction of travel is fixed: vim.pro hands down what your config claims, the plugin computes the delta locally, and only what you agree to is ever uploaded. The local engine is the same one either way.