a plugin for neovim

Where there was a shorter way.

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.

what you pressed
14 keys
j j j j j j j j j j j j j j
the shorter way
3 keys
1 4 j

14 against 3 is a sum anyone can check. That is the only kind of thing this plugin will say to you.

{ "vim-pro/fingers.nvim" } No configuration required. Then edit normally for a week and run :Fingers.
the whole interface

One buffer, when you ask for it.

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://you:Fingers
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.

The list at the bottom is the point.

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.

Counted honestly, or not at all.

  • Replayed macros don't count. A macro is one decision, however many keys it emits. Counting the replay would inflate exactly the mappings you automated.
  • Dot-repeats don't count. . replays the change, not your keystrokes. Press <leader>x then . and the count is 1: your fingers reached the mapping once.
  • Ambiguous prefixes wait to resolve. With both <leader>g and <leader>gs bound, typing <leader>gs credits only <leader>gs — the same rule vim itself uses via timeoutlen.
local by default

And complete that way.

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.

one file

Counts live in your data directory

A single JSON file, and nowhere else. Read it, diff it, delete it.

:lua= require("fingers.store").path()
-- stdpath("data")/fingers/counts.json
what it holds

Mapping names, counts, dates

It does not hold the keys you pressed, the text you typed, the files you opened, or anything about your projects.

deleting it

A complete uninstall of your history

Delete the file and your history is erased entirely; the plugin starts over without complaint. :Fingers reset does the same from inside the editor.

what it watches

Normal and visual. Never insert

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.

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. 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.

What it currently notices

Runs of a single key past run_threshold (five by default), where the alternative is named rather than gestured at:

jkhlxXwb

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.

install

Any plugin manager, no configuration.

-- 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.

Commands

commandwhat it does
:Fingersthe fingers buffer (q closes it)
:Fingers resetforget every count and finding, start over
:Fingers flushwrite to disk now (otherwise: on exit)
:checkhealth fingersexplains 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.

Configuration

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
known limits

Stated here rather than discovered later.

Late mappings are invisible for a session

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.

A hard crash loses the session

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.

Leader mappings only

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.

not built yet

Connecting to vim.pro.

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.

history

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.”

the population

“You're one of 4% who bind this; the rest use gd.”

a read of your editing

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.