pi-vim-editor
Minimal Vim-style modal editing for Pi's prompt editor that composes with other editor extensions.
Package details
Install pi-vim-editor from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-vim-editor- Package
pi-vim-editor- Version
0.10.0- Published
- Nov 4, 2026
- Downloads
- 1,763/mo · 570/wk
- Author
- mavam
- License
- MIT
- Types
- extension
- Size
- 183.7 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
⌨️ pi-vim-editor
Edit prompts by sentence and paragraph, reshape blocks, and reflow prose— without replacing your editor. pi-vim-editor adds Vim-style INSERT and NORMAL modes by decorating the installed editor. Editor-extension compatibility depends on load order; see Usage.
🚀 Installation
pi install npm:pi-vim-editor
✨ Usage
The prompt starts in INSERT mode. Press Esc for NORMAL mode, where the
common Vim motions, operators, and text objects edit the prompt, and v, V,
or Ctrl-V start Visual mode. Press Esc again in NORMAL mode to interrupt
the agent, as usual. By default, the current mode shows in a corner of the
editor border.
Usually, load pi-vim-editor after extensions that customize the editor. For
framing extensions, order also affects Visual highlighting and H/M/L:
these are disabled if the editor pi-vim-editor decorates has already
rearranged Pi's rows. A framing extension can instead wrap the Vim-decorated
editor and show the Vim status itself. See
docs/integration.md.
⌨️ Key bindings
Counts work on motions and most edits, as in 3w, d3w, or 2dd. Keys Vim
doesn't use, such as Enter, Tab, arrows, and Ctrl or Alt shortcuts, keep
doing what they do in Pi. The exceptions in NORMAL mode are Ctrl-V, which
starts Visual block mode instead of pasting an image, Ctrl-R, which redoes,
and Ctrl-]. While a search query is open, its keys stay with search instead
of Pi (see Literal prompt search).
Modes and Esc
| Key | Action |
|---|---|
Esc / Ctrl-[ |
INSERT or REPLACE → NORMAL; closes autocomplete first if it's open |
Esc in NORMAL |
Cancels a pending command, otherwise interrupts the agent as usual |
i a / I A |
INSERT before / after the cursor, at the first non-blank / line end |
o / O |
Open a line below / above |
R |
REPLACE: typed characters overwrite; Backspace restores |
v / V / Ctrl-V |
Visual, Visual line, Visual block |
After you submit a prompt, the editor is back in INSERT mode.
Motions
| Key | Motion |
|---|---|
h j k l |
Left, down, up, right |
w b e / W B E |
Word / WORD start, back, end |
ge / gE |
Back to the end of the previous word / WORD |
0 ^ $ |
Line start, first non-blank, last character |
gj / gk |
Down / up one screen row through wrapped lines |
g0 g^ g$ |
Screen row start, first non-blank, last character |
H / M / L |
Top, middle, bottom visible row |
gg / G / {n}G |
First line, last line, line n |
f F t T + char |
To / till a character on the line |
; / , |
Repeat the last f F t T / in reverse |
% |
Jump to the matching bracket |
( / ) |
Back / forward to a sentence start |
{ / } |
Back / forward to an empty-line paragraph boundary |
Use 2} to move forward two paragraph boundaries or d} to delete up to the
next boundary. Only empty lines separate paragraphs for these motions;
space/tab-only lines do not. Consecutive empty lines form one separator run.
These motions do not recognize Markdown, nroff macros, or section markers.
Literal prompt search
In NORMAL or Visual mode, type /text and press Enter to search forward, or
?text to search backward. Search is literal and case-sensitive, not Vim
or JavaScript regex: .*, brackets, and backslashes match themselves. It
searches only the prompt, never the transcript or pasted-block contents or
markers. Matches respect grapheme boundaries.
Use n to repeat in the accepted direction and N in reverse; N doesn't
change the direction of later n. Counts work (2/text, 3n). Search wraps
with a notice. Enter on an empty query reuses the last accepted text in the
chosen / or ? direction. No match leaves the cursor in place with a notice.
The border shows the query unless indicator is none. Backspace deletes
its last grapheme; Esc or Ctrl-[ cancels without moving. Enter confirms,
never submits. Paste adds to the query, including line breaks; other
shortcuts and navigation keys are ignored until you confirm or cancel.
External cursor or text changes cancel the query. Search memory is private
to each editor and resets after submit.
Operator search (d/text, cn, and similar) isn't supported: it cancels
without editing. Instead, use v/text, Enter, then d, c, or y to act on
the inclusive selection. There is no regex, query history UI, or live preview.
Operators and text objects
d deletes, c changes, y yanks, gu lowercases, and gU uppercases.
gq and gw reflow prose into hard-wrapped lines.
Combine them with a motion (dw, c$, y3j, d%, d), d}), double them for whole
lines (dd, cc, yy, guu, gUU), or use a text object:
| Object | Inner / around |
|---|---|
| word / WORD | iw aw / iW aW |
| sentence | is as |
| paragraph (whole lines) | ip ap |
| quotes | i" a", i' a', i` a` |
| brackets | i( a(, i[ a[, i{ a{ (closing brackets work too) |
Sentence objects can span lines: cis changes the whole current sentence,
even from its middle, and das also takes adjacent separator whitespace.
Use d2as for two sentences. Quote and bracket objects stay on the current line.
With the cursor inside `src/old.ts`, type ci` to replace the path
while keeping the backticks.
Use cip to rewrite an instruction paragraph, dap to delete it with adjacent
blank lines, or yip to copy its whole lines. Empty and space/tab-only lines
separate paragraphs. ap prefers trailing blank lines, falling back to leading
ones at the prompt’s end; d2ap deletes two paragraphs. These are prose objects,
not Markdown heading, list, or fence objects.
Manual prose reflow
Use gwap to wrap the current paragraph and keep your place in the text, or
gqap to wrap it and move to its end. Both retain blank paragraph separators;
gwip and gqip leave those separators outside the formatting range.
gqq and gww format the current line; 3gqq and 3gwgw format three lines.
You can also use motions (gq}, gwj) or format a Visual selection with gq
or gw. Formatting always covers whole touched lines, preserves the register,
and supports one-step u and . repeat.
The default width is a fixed 79 display columns, configurable with
vimEditor.textWidth. Wrapping keeps the first line’s indentation, leaves
long words intact, and refuses ranges containing pasted blocks. It only runs
when requested: there is no automatic formatting while typing and no Markdown,
comment, or filetype parsing.
Edits, undo, and repeat
| Key | Action |
|---|---|
x / X |
Delete the character under / before the cursor |
s / S |
Substitute a character / the line |
r + char |
Replace the character under the cursor |
~ |
Toggle case and move right |
D / C / Y |
Delete / change to the line end, yank the line |
J |
Join with the next line |
>> / << |
Indent / outdent by two spaces |
p / P |
Put after / before the cursor |
u |
Undo a whole change, such as ciwfoo<Esc> |
Ctrl-R |
Redo what u undid |
. / {n}. |
Repeat the last change, including Visual changes and typed text |
Deletes, changes, and yanks share one register. Yanks also go to the system
clipboard. Prefix a command with "+ or "* to use the system clipboard
explicitly: "+dd cuts a line to it, and "+p puts it. Edits that would cut
into a pasted block ([paste #1 +24 lines]) are refused.
Visual modes
Motions and text objects (viw, vi(, vis, v2as, vip, v2ap) extend the
selection. vip and vap start whole-line paragraph selections.
| Key | Action |
|---|---|
o / O |
Jump to the other end / the other corner of a block |
d x / c s / y |
Delete / change / yank the selection |
p |
Replace the selection with the register (not in a block) |
r + char |
Replace every selected character |
~ / u / U |
Toggle case, lowercase, uppercase |
J / > / < |
Join, indent, outdent the selected lines |
gq / gw |
Reflow whole selected lines; gw keeps the active cursor’s place in the text |
$ |
Block: extend to the end of every line |
I / A |
Block: insert / append on every line |
Quitting Pi
| Keys | Action |
|---|---|
ZZ, :q, :qa |
Quit if the prompt is empty |
ZQ, :q!, :qa! |
Quit, dropping the prompt |
The full spec, including where it differs from Vim, is in docs/ux.md.
🆚 Comparison
How pi-vim-editor compares with other Vim extensions for Pi, based on their
documented features. This table describes current main; the published npm
release may not yet include its latest features.
| pi‑vim‑editor | pi‑vim | pi‑command‑deck | pi‑vim‑keys | pixit | pi‑vimmode | |
|---|---|---|---|---|---|---|
| Editor integration | Wraps the installed editor | Replaces it | Replaces it | Replaces it | Replaces it | Replaces it |
| Visual modes | v V Ctrl-V |
v V |
v V |
v V |
❌ | v V; block configurable |
| Operators | d c y gu gU |
d c y |
d c y gu gU g~ |
limited | d c y |
d c y gu gU g~ |
| Text objects | word, sentence, paragraph, quotes, brackets | word, quotes, brackets | word, paragraph | ❌ | word | word, quotes, brackets, paragraphs, Markdown, tags, error blocks |
| Paragraph motions | Navigation and operators | Navigation only | Navigation and operators | not documented | not documented | Navigation and operators |
| Prose reflow | gq / gw operators |
not documented | not documented | not documented | not documented | :reflow; gq line action |
| Counts | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
. repeat |
✅ | ✅ | ✅ | ❌ | ❌ | ⚠️ limited |
| Redo | Ctrl-R |
Ctrl-R |
Ctrl-R |
U |
Ctrl-R |
Ctrl-R |
Replace mode R |
✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
Search / ? n |
Literal, case-sensitive; also N |
❌ | ✅ | ❌ | ❌ | ✅ |
| Macros, named registers | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| System clipboard | both ways | both ways | yanks | ❌ | ❌ | both ways |
| Ex commands | :q |
Pi commands | Pi commands | ❌ | ❌ | Pi commands, editing |
| Cursor shape per mode | ✅ opt-in¹ | ✅ | ✅ | ✅ | ❌ | ✅ |
¹ Requires Pi's hardware cursor and a terminal that supports cursor shapes.
⚙️ Configuration
Set the manual prose wrapping width in Pi’s settings.json:
{ "vimEditor": { "textWidth": 72 } }
textWidth accepts a positive integer in display columns. Missing or invalid
values (including 0) use 79, regardless of terminal or editor width.
This is a fixed default, not Vim’s window-dependent textwidth=0 fallback.
To make plain p paste the system clipboard, and every delete and change
copy to it, as with Vim's clipboard=unnamedplus:
{ "vimEditor": { "clipboard": "unnamedplus" } }
Move the mode label to another corner in Pi's settings.json:
{ "vimEditor": { "indicator": "top-left" } }
Choose bottom-right (default), bottom-left, top-right, top-left, or
none. To hide the label entirely:
{ "vimEditor": { "indicator": "none" } }
With none, mode text, pending commands, and search queries are visible only
if another extension renders them from the
status capability.
Vim editing, Visual highlighting, and optional cursor shapes remain active.
Settings are read when a session starts, including for an existing editor.
To also show the mode through the cursor shape, enable both settings:
{
"showHardwareCursor": true,
"vimEditor": { "cursorShape": true }
}
cursorShape defaults to false. When enabled, INSERT uses a steady bar,
NORMAL and Visual modes a steady block, and REPLACE a steady underline. Pi
still controls cursor visibility; with its hardware cursor hidden, the
software cursor stays unchanged.
To pick the style per mode, set cursorShape to a map instead of true:
{
"showHardwareCursor": true,
"vimEditor": {
"cursorShape": {
"insert": "blinking-bar",
"normal": "blinking-block",
"replace": "blinking-underline"
}
}
}
The keys are insert, normal, visual (all Visual modes), and replace.
The styles are steady-bar, blinking-bar, steady-block,
blinking-block, steady-underline, blinking-underline, and reset for
the terminal's default. Missing or invalid entries keep their default style.
Pi merges maps across global and project settings, so a project map only
overrides the entries it names; {} doesn't reset inherited entries. Set
cursorShape to false to turn off a map inherited from your global
settings.
Cursor shapes require DECSCUSR support in your terminal and, if used, tmux
or another multiplexer. Disable cursorShape if your terminal ignores the
sequences. The extension resets the shape with DECSCUSR 0, the terminal's
default (a blinking block in xterm), when the prompt loses focus, Pi opens an external editor or suspends,
or the extension shuts down or reloads. It does not query or restore your
pre-Pi cursor shape.