ViNELViNEL Is Not an Emulation Layer — Vim / Neovim modal editing for VS Code. (An ode to Wine and Vi.) What it isVim's modal editing — Normal / Visual / Insert modes, operators, text objects, dot-repeat, find-char, counts — built as a native state machine on top of VS Code, not a Vim emulation and not a Neovim bridge.
Everything below is count-aware — prefix a number ( ControlsModes & entering Insert
Motions (Normal & Visual)
Operators & edits (Normal)
Marks & registers
Macros
Macros are entirely ViNEL's own — no companion extension, ever. They record
literal keystrokes: ViNEL commands, the characters you type in Insert, and
the Insert-mode navigation keys you press (arrows, Backspace, Home/End). Replay
re-issues typed characters through VS Code's real typing pipeline, so
auto-indent, auto-closing pairs, auto-surround, and LSP edits all react on
replay exactly as they did while recording — e.g. selecting a word and typing
(How, without the always-on
The one place ViNEL touches
Blockwise Visual (
|
| Key | Action |
|---|---|
Ctrl-V |
Enter blockwise Visual (a columnar / rectangular selection) |
h j k l / arrows |
Grow / shrink the rectangle |
0 $ |
Left edge to column 0 · right edge to each line's end (ragged) |
I |
Insert at the block's left edge on every selected line at once |
A |
Append at the block's right edge (or line end after $) on every line |
d x · c · y |
Delete · change · yank the rectangle |
Blockwise selects the same columns across a range of lines — a tall box
instead of a run of text. Its signature move is block insert: make a column
selection down N lines, press I (or A), type once, Esc — and the text lands
on every line at that column. ViNEL does this with VS Code's own multi-cursor
(one cursor per row), so the broadcast typing is fully native.
Ctrl-Vnote: on Windows/LinuxCtrl-Vis normally Paste; ViNEL rebinds it to blockwise Visual while in Normal/Visual (matching vim). Paste on Mac isCmd+Vand is unaffected. To keepCtrl-Vas paste, remove/rebindvinel.enterVisualBlockin your keybindings.json (same mechanism as above).
Ex-commands (:)
Opens VS Code's input box; Enter runs, Escape cancels.
| Command | Action |
|---|---|
:w :wa |
Save · save all |
:q :q! :qa |
Close · close discarding · close all |
:wq :x |
Save and close |
:sp :vs |
Split down · split right |
:42 :$ |
Go to line 42 · last line |
:s/pat/rep/[g][i] |
Substitute on the current line (g = all, i = ignore case) |
:%s/pat/rep/g |
Substitute across the whole file |
:10,20s/… |
Substitute over a line range |
:'<,'>s/… |
Substitute over the Visual selection (: from Visual prefills this) |
:[range]d :[range]y |
Delete / yank a line range (:15,20y, :%d) |
:[range]> :[range]< |
Indent / outdent a line range |
:[range]j |
Join a line range |
:noh |
Clear the search highlight |
Ranges: % (whole file), n,m, . (current), $ (last), '<,'> (Visual
selection); no range = current line.
Substitution patterns are JavaScript RegExp (not full vim regex) — literals,
\d, groups, \1 backrefs, & = whole match all work; \v/\zs/\< do
not. One :%s is a single undo.
Text objects (after d/c/y, or in Visual)
| Key | Object |
|---|---|
iw aw / iW aW |
Inner / around word · WORD |
i" a" i' i` |
Inner / around a quoted string |
i( i) ib |
Inner / around () (ib alias) |
i{ i} iB |
Inner / around {} (iB alias) |
i[ i] · i< i> |
Inner / around [] · <> |
it at |
Inner / around an HTML/XML/JSX tag pair |
is as |
Inner / around a sentence |
ip ap |
Inner / around paragraph |
Use a instead of i for the "around" (delimiters-included) form: ci",
da(, yi{, vip, …
Surround (vim-surround)
| Key | Action |
|---|---|
ys{motion}{c} |
Surround the motion / text object with {c} — ysiw", ysa(, ys$) |
yss{c} |
Surround the whole line |
S{c} (in Visual) |
Surround the selection |
ds{c} |
Delete the surrounding {c} — ds", ds), dst (a tag) |
cs{old}{new} |
Change surrounding {old} → {new} — cs"', cs)] |
{c} is one of )( b · ][ · }{ B · >< · " ' ` · t (tag). The
"open" variants add inner spaces: ysiw( → ( word ), ysiw) → (word).
Tip: ys must be followed straight away by a motion/object (as in vim).
Visual mode
| Key | Action |
|---|---|
| motions | Extend the selection |
d c y x |
Act on the selection |
Delete / Backspace |
Delete the selection (QoL — faster than d) |
p |
Paste over the selection |
> < |
Indent / outdent |
J |
Join |
~ u U |
Toggle / lower / upper case |
o |
Jump to the other end of the selection |
i{obj} a{obj} |
Select a text object |
Editor & IDE (VS Code's own power, under Vim keys)
These route straight to VS Code's built-in commands, so you get full IDE features with Vim muscle memory. All are ordinary keybindings you can change — see Customizing keybindings.
| Key | Action |
|---|---|
Ctrl-O / Ctrl-I |
Jump back / forward through navigation history |
gd / gD |
Go to definition / declaration |
gr |
Peek references |
K |
Show hover docs |
gcc / gc (Visual) |
Toggle line comment (current line / selection) |
gt / gT |
Next / previous editor |
Ctrl-W h j k l |
Focus the split left / down / up / right |
Ctrl-W s / Ctrl-W v |
Split down / right |
Ctrl-W q / Ctrl-W w |
Close / cycle split |
za zo zc |
Toggle / open / close fold |
zR / zM |
Open all / close all folds |
== |
Re-indent the current line |
Customizing keybindings
Every key here is rebindable — nothing is locked. ViNEL contributes plain VS Code keybindings, so you override them the same way you'd override any built-in shortcut. The macro record and playback combos are the ones people most often want to change, so here's exactly how.
The two macro shortcuts (defaults)
| Action | Default key | Command id (this is what you rebind) |
|---|---|---|
| Start / stop recording | Cmd+F2 (mac) · Ctrl+F2 (Win/Linux) |
vinel.macroRecordToggle |
| Replay last macro | Cmd+F3 (mac) · Ctrl+F3 (Win/Linux) |
vinel.macroPlayLast |
Option A — the Keyboard Shortcuts UI (no JSON)
- Open the Command Palette (
Cmd/Ctrl+Shift+P) → Preferences: Open Keyboard Shortcuts. - In the search box type
ViNEL: Toggle macro recording(orViNEL: Replay last macro). - Double-click the row, press your desired key combo, hit Enter. Done — your binding automatically takes priority over the default.
Option B — edit keybindings.json directly
Command Palette → Preferences: Open Keyboard Shortcuts (JSON), then add:
[
// Pick any combos you like — these replace F2 / F3:
{ "key": "cmd+shift+2", "command": "vinel.macroRecordToggle", "when": "editorTextFocus" },
{ "key": "cmd+shift+3", "command": "vinel.macroPlayLast", "when": "editorTextFocus" },
// OPTIONAL — turn OFF the built-in F2 / F3 defaults (prefix the command with "-"):
{ "key": "cmd+f2", "command": "-vinel.macroRecordToggle" },
{ "key": "cmd+f3", "command": "-vinel.macroPlayLast" }
]
Notes:
- Keep
"when": "editorTextFocus"so the shortcuts work in every mode (that's what lets you start/stop recording mid-Insert). To restrict a shortcut to Normal mode only, use"when": "editorTextFocus && vinel.mode == 'normal'". - A numeric prefix for playback works in Normal mode regardless of the key
you choose: type the count, then fire your playback combo (
5→ replay 5×). - Your
keybindings.jsonalways wins over ViNEL's defaults, so you don't have to disable a default before rebinding — the-commandlines above are only if you want the old key to do nothing.
Rebinding anything else
The same recipe works for any ViNEL command — search "ViNEL" in the Keyboard
Shortcuts UI to see them all, or bind by command id in JSON. A few handy ids:
vinel.enterVisualBlock (blockwise Ctrl-V — rebind if you want Ctrl-V back
as Paste on Windows/Linux), vinel.exCommand (:), vinel.repeatChange (.).
Leader & custom mappings
A leader mapping is simple: press a leader key, then a short letter
sequence, to run any command — e.g. Space then f opens the file picker.
It's how Neovim users bind their most-used actions. ViNEL doesn't invent a
mapping language for this; you use native VS Code chord keybindings gated to
Normal mode, so they sync with the Keyboard Shortcuts UI like everything else.
Space is the conventional leader and ViNEL keeps it free (bare Space in
Normal does nothing), but any first key works.
Command Palette (Cmd/Ctrl+Shift+P) → Preferences: Open Keyboard Shortcuts
(JSON). Each entry reads as: "key" = your leader sequence · "command" = what
to run · "when" = vinel.mode == 'normal' (so it only fires in Normal):
[
// <leader> = Space
{ "key": "space f", "command": "workbench.action.quickOpen", "when": "editorTextFocus && vinel.mode == 'normal'" },
{ "key": "space w", "command": "workbench.action.files.save", "when": "editorTextFocus && vinel.mode == 'normal'" },
{ "key": "space g g", "command": "workbench.view.scm", "when": "editorTextFocus && vinel.mode == 'normal'" },
// a leader key can fire ViNEL's own commands too
{ "key": "space m", "command": "vinel.macroPlayLast", "when": "editorTextFocus && vinel.mode == 'normal'" }
]
Finding the command to bind
Every action in VS Code has a command id — the string you put in
"command". To find one:
- Open Keyboard Shortcuts (
Cmd/Ctrl+K Cmd/Ctrl+S). - Search in plain words — "save", "find files", "toggle terminal", "format document", or "ViNEL" for ViNEL's own.
- Right-click the matching row → Copy Command ID and paste it as
"command".
That's the whole trick — anything you can find in Keyboard Shortcuts, you can put
behind a leader key: built-in commands, other extensions' commands, or ViNEL's
(vinel.macroPlayLast, vinel.exCommand, …).
- Change your leader by swapping the first key (
,,\, …) in each entry. - Sequences are just multi-key chords —
space g g, pressed in order. - Your
keybindings.jsonoutranks ViNEL's defaults, so these take overSpacein Normal automatically (see Customizing keybindings).
Design note: text objects seek forward (across lines)
If the cursor isn't already inside/on a bracket or quote, di[ / ci" seek
forward through the document to the next one and act on its matched pair — so
they work from anywhere, not only when you're on top of the delimiter. An
enclosing pair always wins over a later one.
This is a deliberate choice. Strict vanilla Vim only acts on an enclosing
pair (and beeps otherwise); ViNEL matches the plugin-augmented Neovim
experience (targets.vim / mini.ai style) instead, because it's more useful in
practice. So from far above a lone bracket, di[ will travel down to that next
occurrence — by design.
Requires VSCodeVim disabled
If VSCodeVim is installed it claims the same keys through its own context, and which one fires becomes unpredictable. Disable or uninstall it:
code --uninstall-extension vscodevim.vim
Building
npm install
npm run compile
npm run package # produces a .vsix
code --install-extension vinel-0.0.1.vsix --force
Uninstalling & resetting
Uninstalling removes ViNEL and all of its commands and default keybindings
automatically. It contributes no settings and writes nothing to disk, so a
reinstall always starts clean — handy if anything ever gets into a weird state.
Your own keybindings.json (leader mappings, custom binds) is your file and is
left untouched; keep a copy (or track your VS Code config in git) so you can
restore it after a reinstall.
Not built yet
A few niceties remain: numbered / system-clipboard registers ("0, "+), the
( / ) sentence motions, and treesitter function/class objects (if/af).
Everything else documented above — modes, motions, operators, text objects,
marks & named registers, Ex-commands, macros, leader mappings, surround, and case
operators — is built and native, with no companion extension required.
Feedback & bug reports
- Bugs / feature requests: open an issue at https://github.com/andrew-booshartig/vinel-vscode/issues.
- On the Marketplace: the extension page's Q&A tab and Ratings & Review section both reach the publisher.
- Please include your OS, VS Code version, and steps to reproduce.
Support
If ViNEL saves you time, you can support development ☕ Buy me a coffee