npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vimsplain

v0.2.0

Published

Parse and explain Vim keystroke sequences

Readme

vimsplain

npm version npm downloads CI license

A TypeScript parser and interpreter for Vim keystroke sequences. Translates raw Vim commands including basic motions, text objects, visual mode, search, and insert modes — into structured explanations or human-readable summaries. Perfect for building interactive learning tools, editor plugins, or keyboard shortcut analyzers.

Used to power the "Motion Log" feature of VimGym - an interactive Vim training game.

import { explainSequence, summarizeSequence } from "vimsplain";

// 1. Basic Motions & Operators
summarizeSequence("ddp");
// "delete line, then paste after cursor"

// 2. Text Objects & Visual Mode
explainSequence('da"');
// {
//   commands: [
//     { matched: 'da"', explanation: 'delete around ""' }
//   ],
//   remaining: ""
// }

explainSequence('vjd');
// {
//   commands: [
//     { matched: "v", explanation: "enter visual mode" },
//     { matched: "j", explanation: "move line down" },
//     { matched: "d", explanation: "delete selection" }
//   ],
//   remaining: ""
// }

// 3. Multi-Mode Sequences (Search, Operators, Insert Mode)
explainSequence('/target[Enter]cwnew[Esc]');
// {
//   commands: [
//     { matched: "/target", explanation: 'search forward for "target"' },
//     { matched: "cw",      explanation: 'change word forward' },
//     { matched: "new",     explanation: 'type "new"' },
//     { matched: "[Esc]",   explanation: 'exit insert mode' }
//   ],
//   remaining: ""
// }

Install

npm install vimsplain
# or
pnpm add vimsplain

API

explainSequence(input: string): ExplainResult

Parses a Vim keystroke sequence and returns structured explanations for each command.

Handles five parsing modes:

  • Normal mode — motions, operators, text objects
  • Insert mode — after i, a, cw, etc., accumulates typed text until [Esc]
  • Visual mode — after v, V, or [C-v], supports visual selection operators
  • Search mode — after / or ?, accumulates pattern until [Enter]
  • Ex mode — after :, accumulates command until [Enter]

Special key notation: Encode special keys as bracketed strings: [Esc], [Enter], [Backspace], [Delete], [Up], [Down], [Left], [Right], [C-r], [C-w], [C-o], [C-i].

Use the exported SPECIAL_KEYS constants to avoid typos:

import { SPECIAL_KEYS, explainSequence } from "vimsplain";

explainSequence(`ihello${SPECIAL_KEYS.ESCAPE}`);
// commands: [
//   { matched: "i",     explanation: "insert before cursor" },
//   { matched: "hello", explanation: 'type "hello"' },
//   { matched: "[Esc]", explanation: "exit insert mode" }
// ]

formatExplanation(result: ExplainResult): string

Formats an ExplainResult as a human-readable multi-line string.

import { explainSequence, formatExplanation } from "vimsplain";

const result = explainSequence("ggdG");
console.log(formatExplanation(result));
// gg: go to start of file
// dG: delete to end of file

summarizeSequence(input: string): string

Parses a sequence and returns a plain English summary joined by ", then ".

import { summarizeSequence } from "vimsplain";

summarizeSequence("yyp");
// "yank line, then paste after cursor"

Types

type ExplainedCommand = {
  matched: string;      // The matched keystroke(s)
  explanation: string;  // Human-readable explanation
};

type ExplainResult = {
  commands: ExplainedCommand[];
  remaining: string;    // Any unmatched trailing input
};

type CommandDefinition = {
  pattern: RegExp;
  description: string;
  isMotion: boolean;
};

Constants

import { SPECIAL_KEYS } from "vimsplain";

SPECIAL_KEYS.ESCAPE      // "[Esc]"
SPECIAL_KEYS.ENTER       // "[Enter]"
SPECIAL_KEYS.BACKSPACE   // "[Backspace]"
SPECIAL_KEYS.DELETE      // "[Delete]"
SPECIAL_KEYS.ARROW_UP    // "[Up]"
SPECIAL_KEYS.ARROW_DOWN  // "[Down]"
SPECIAL_KEYS.ARROW_LEFT  // "[Left]"
SPECIAL_KEYS.ARROW_RIGHT // "[Right]"
SPECIAL_KEYS.CTRL_R      // "[C-r]"
SPECIAL_KEYS.CTRL_W      // "[C-w]"
SPECIAL_KEYS.CTRL_O      // "[C-o]"
SPECIAL_KEYS.CTRL_I      // "[C-i]"

Supported Commands

| Keystroke | Description | |-----------|-------------| | N | move $1 chars right | | | move char right | | "_dd | delete line (discard) | | "_dNw | delete $1 word(s) (discard) | | "\+yy | yank line to system clipboard | | "\+p | paste from system clipboard after cursor | | "\+P | paste from system clipboard before cursor | | "(a)yy | yank line into register '$1' | | "(a)dd | delete line into register '$1' | | "(a)p | paste from register '$1' after cursor | | "(a)P | paste from register '$1' before cursor | | d$ | delete to end of line | | d0 | delete to start of line | | d^ | delete to first non-blank | | dgg | delete to start of file | | dG | delete to end of file | | dNw | delete $1 word(s) forward | | dNb | delete $1 word(s) backward | | dNe | delete to end of $1 word(s) | | dNj | delete $1 line(s) down | | dNk | delete $1 line(s) up | | df(.) | delete through '$1' | | dF(.) | delete back through '$1' | | dt(.) | delete till '$1' | | dT(.) | delete back till '$1' | | dd | delete line | | Ndd | delete $1 lines | | D | delete to end of line | | c$ | change to end of line | | c0 | change to start of line | | c^ | change to first non-blank | | cNw | change $1 word(s) forward | | cNb | change $1 word(s) backward | | cNe | change to end of $1 word(s) | | cf(.) | change through '$1' | | cF(.) | change back through '$1' | | ct(.) | change till '$1' | | cT(.) | change back till '$1' | | cc | change entire line | | C | change to end of line | | S | substitute entire line | | s | substitute character and enter insert mode | | y$ | yank to end of line | | y0 | yank to start of line | | y^ | yank to first non-blank | | yNw | yank $1 word(s) forward | | yf(.) | yank through '$1' | | yF(.) | yank back through '$1' | | yt(.) | yank till '$1' | | yT(.) | yank back till '$1' | | yy | yank line | | Y | yank line | | Nyy | yank $1 lines | | ciw | change inner word | | caw | change a word (with space) | | ci" | change inside "" | | ca" | change around "" | | ci' | change inside '' | | ca' | change around '' | | ci( | change inside () | | ci) | change inside () | | ca( | change around () | | ca) | change around () | | ci[ | change inside [] | | ci] | change inside [] | | ca[ | change around [] | | ca] | change around [] | | ci{ | change inside {} | | ci} | change inside {} | | ca{ | change around {} | | ca} | change around {} | | cit | change inside tag | | cat | change around tag | | diw | delete inner word | | daw | delete a word (with space) | | di" | delete inside "" | | da" | delete around "" | | di' | delete inside '' | | da' | delete around '' | | di( | delete inside () | | di) | delete inside () | | da( | delete around () | | da) | delete around () | | di[ | delete inside [] | | di] | delete inside [] | | da[ | delete around [] | | da] | delete around [] | | di{ | delete inside {} | | di} | delete inside {} | | da{ | delete around {} | | da} | delete around {} | | dit | delete inside tag | | dat | delete around tag | | yiw | yank inner word | | yaw | yank a word (with space) | | yi" | yank inside "" | | ya" | yank around "" | | yi' | yank inside '' | | ya' | yank around '' | | yi( | yank inside () | | yi) | yank inside () | | ya( | yank around () | | ya) | yank around () | | viw | select inner word | | vaw | select a word (with space) | | vi" | select inside "" | | va" | select around "" | | vi' | select inside '' | | va' | select around '' | | vi( | select inside () | | va( | select around () | | vi) | select inside () | | va) | select around () | | vi[ | select inside [] | | va[ | select around [] | | vi] | select inside [] | | va] | select around [] | | vi{ | select inside {} | | va{ | select around {} | | vi} | select inside {} | | va} | select around {} | | vit | select inside tag | | vat | select around tag | | ci< | change inside <> | | ci> | change inside <> | | ca< | change around <> | | ca> | change around <> | | di< | delete inside <> | | di> | delete inside <> | | da< | delete around <> | | da> | delete around <> | | yi< | yank inside <> | | yi> | yank inside <> | | ya< | yank around <> | | ya> | yank around <> | | vi< | select inside <> | | vi> | select inside <> | | va< | select around <> | | va> | select around <> | | ci`` | change inside `` | | ca| change around | | di`` | delete inside `` | | da| delete around | | yi`` | yank inside `` | | ya| yank around | | vi`` | select inside `` | | va| select around | | f(.) | find '$1' forward | | F(.) | find '$1' backward | | t(.) | till '$1' forward | | T(.) | till '$1' backward | | ; | repeat last f/t/F/T | | , | repeat last f/t/F/T reverse | | Nw | move $1 words forward | | w | move word forward | | NW | move $1 WORDS forward | | W | move WORD forward | | Nb | move $1 words backward | | b | move word backward | | NB | move $1 WORDS backward | | B | move WORD backward | | Ne | move to end of $1 words | | e | move to end of word | | NE | move to end of $1 WORDS | | E | move to end of WORD | | ge | move to end of previous word | | gE | move to end of previous WORD | | 0 | move to start of line | | $ | move to end of line | | ^ | move to first non-blank | | _ | move to first non-blank | | Nj | move $1 lines down | | j | move line down | | Nk | move $1 lines up | | k | move line up | | Nh | move $1 chars left | | h | move char left | | Nl | move $1 chars right | | l | move char right | | gg | go to start of file | | Ngg | go to line $1 | | G | go to end of file | | NG | go to line $1 | | { | move paragraph backward | | } | move paragraph forward | | ( | move sentence backward | | ) | move sentence forward | | i | insert before cursor | | I | insert at start of line | | a | append after cursor | | A | append at end of line | | o | open line below | | O | open line above | | Nx | delete $1 chars | | x | delete char under cursor | | X | delete char before cursor | | r(.) | replace with '$1' | | R | enter replace mode | | ~ | toggle case | | J | join lines | | gJ | join lines (no space) | | u | undo | | U | undo line | | [C-r] | redo | | p | paste after cursor | | P | paste before cursor | | \. | repeat last change | | v | enter visual mode | | V | enter visual line mode | | [C-v] | enter visual block mode | | m(.) | set mark '$1' | | '(.) | go to mark '$1' (line) | | ``(.)| go to mark '$1' (exact) | |q(a)| start recording macro '$1' | |q| stop recording macro | |@@| replay last macro | |@(a)| play macro '$1' | |n| next search match | |N| previous search match | |*| search word under cursor forward | |#| search word under cursor backward | |%| go to matching bracket | |zO| open all folds recursively | |zR| open all folds | |zM| close all folds | |zo| open fold | |zc| close fold | |za| toggle fold | |z=| suggest spelling corrections | |zg| add word to dictionary | |zw| mark word as incorrect | |]s| next misspelling | |[s| previous misspelling | |zz| center cursor line | |zt| scroll cursor to top | |zb| scroll cursor to bottom | |guNw| lowercase $1 word(s) | |gUNw| uppercase $1 word(s) | |guw| lowercase word | |gUw| uppercase word | |guu| lowercase line | |gUU| uppercase line | |g~~| toggle case line | |gcc| toggle comment line | |gcNw| toggle comment $1 word(s) forward | |gcNj| toggle comment $1 line(s) down | |gcNk| toggle comment $1 line(s) up | |gciw| toggle comment inner word | |gcaw| toggle comment a word | |gci(| toggle comment inside () | |gca(| toggle comment around () | |gc| toggle comment selection | |=ap| auto-indent paragraph | |=G| auto-indent to end of file | |=%| auto-indent to matching bracket | |=Nj| auto-indent $1 lines down | |>>| indent line | |<<| dedent line | |>Nj| indent $1 lines down | |<Nj| dedent $1 lines down | |[C-w]s| split window horizontally | |[C-w]v| split window vertically | |[C-w]h| move to window left | |[C-w]j| move to window below | |[C-w]k| move to window above | |[C-w]l| move to window right | |[C-w]q| close window | |[C-o]| jump back | |[C-i]| jump forward | |[Esc]| return to normal mode | |[Enter]| execute/confirm | |[Backspace]| delete char left | |[Delete]| delete char under cursor | |[Up]| move up | |[Down]| move down | |[Left]| move left | |[Right]| move right | |d` | delete char under cursor |

Visual Mode Operators

| Keystroke | Description | |-----------|-------------| | d | delete selection | | D | delete selection | | c | change selection | | C | change selection | | y | yank selection | | Y | yank selection | | x | delete selection | | X | delete selection | | s | change selection | | S | change selection | | ~ | toggle case of selection | | > | indent selection | | < | dedent selection | | = | auto-indent selection | | J | join selection | | p | paste over selection | | P | paste over selection | | gc | toggle comment selection | | gu | lowercase selection | | gU | uppercase selection | | g~ | toggle case of selection | | gq | format selection |

Contributing

Issues and PRs welcome. The command definitions live in src/handlers/normal.ts (as a NORMAL_COMMANDS array) and src/handlers/visual.ts (as VISUAL_OPERATORS). Adding new normal commands is a one-liner:

{ pattern: /^gf/, description: "go to file under cursor", isMotion: false }

After adding, removing, or renaming entries, regenerate the Supported Commands tables above:

pnpm gen:commands

This rewrites the tables between their respective <!-- ..._TABLE_START --> and <!-- ..._TABLE_END --> markers.

Publishing a new version

This package is published from the oller/vimgym monorepo using Changesets and GitHub Actions.

To release a new version:

1. Create a changeset after making your changes to packages/vimsplain/:

pnpm changeset

Select vimsplain, choose the bump type (patch / minor / major), and write a short summary. Commit the generated .changeset/*.md file alongside your code changes.

2. Open a PR and merge to main. The Changesets bot will comment indicating a version bump is pending.

3. The bot opens a "Version Packages" PR that bumps package.json, updates CHANGELOG.md, and removes the consumed changeset file.

4. Merge the "Version Packages" PR. GitHub Actions publishes to npm automatically via OIDC trusted publishing — no tokens required.

You only need to run pnpm changeset when the packages/vimsplain/ source changes. Changes to the vimgym app itself don't require a changeset.

Acknowledgements

Inspired by pafcu/vimsplain, a Python script that parses Vim keystroke sequences using Vim's official index.txt help file.

License

MIT