ai vim extension vscode 2026-07-25
ai vim extension vscode
The basics of writing a VS Code extension for Vim-style keybindings boil down to one hard problem: VS Code is an insert-mode editor by default. You cannot simply bind h/j/k/l in package.json because those keys must still type characters when the user is in insert mode.
Core Techniques
Microsoft’s official sample (vim-sample in the vscode-extension-samples repo) shows the recommended pattern:
-
Override the
typecommand
This intercepts every character the user types.- In normal / visual / operator-pending mode → handle the key yourself (motions, operators, etc.).
- In insert mode → forward it with
default:typeso VS Code behaves normally.
-
Use custom context keys (
setContext)
Publish your own keys such asvim.mode == 'Normal'orvim.inNormalMode.
Keybindings and menus can then usewhenclauses that only fire in the right mode. -
Change cursor style
editor.options.cursorStyle = TextEditorCursorStyle.Block(normal) vsLine(insert). -
Bind a few special keys via
contributes.keybindings
Escape, Ctrl+C, etc. These rarely produce printable characters, so static keybindings are fine.
Minimal Project Setup
npx --package yo --package generator-code -- yo code # Choose “New Extension (TypeScript)”
1. package.json (key parts)
{ "activationEvents": ["onStartupFinished"], "contributes": { "commands": [ { "command": "myvim.enterNormal", "title": "Enter Normal Mode" }, { "command": "myvim.enterInsert", "title": "Enter Insert Mode" } ], "keybindings": [ { "command": "myvim.enterNormal", "key": "escape", "when": "editorTextFocus && vim.mode == 'Insert'" }, { "command": "type", "key": "escape", "when": "editorTextFocus && vim.mode != 'Insert'", "args": { "text": "\u001b" } // optional: send Esc into your handler } ] } }
2. src/extension.ts – skeleton
import * as vscode from 'vscode';
type Mode = 'Normal' | 'Insert' | 'Visual';
let mode: Mode = 'Normal';
export function activate(context: vscode.ExtensionContext) {
// Publish the context key so keybindings can use it
setMode('Normal');
// The magic: intercept typing
context.subscriptions.push(
vscode.commands.registerCommand('type', (args: { text: string }) => {
if (mode === 'Insert') {
// Let VS Code do the normal insert
return vscode.commands.executeCommand('default:type', args);
}
// ---- Your Vim logic goes here ----
handleNormalModeKey(args.text);
})
);
// Mode switch helpers
context.subscriptions.push(
vscode.commands.registerCommand('myvim.enterNormal', () => setMode('Normal')),
vscode.commands.registerCommand('myvim.enterInsert', () => setMode('Insert'))
);
}
function setMode(newMode: Mode) {
mode = newMode;
vscode.commands.executeCommand('setContext', 'vim.mode', newMode);
const editor = vscode.window.activeTextEditor;
if (editor) {
editor.options.cursorStyle =
newMode === 'Insert'
? vscode.TextEditorCursorStyle.Line
: vscode.TextEditorCursorStyle.Block;
}
}
function handleNormalModeKey(key: string) {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
switch (key) {
case 'h':
vscode.commands.executeCommand('cursorLeft');
break;
case 'j':
vscode.commands.executeCommand('cursorDown');
break;
case 'k':
vscode.commands.executeCommand('cursorUp');
break;
case 'l':
vscode.commands.executeCommand('cursorRight');
break;
case 'i':
setMode('Insert');
break;
// add more motions / operators here
default:
// ignore unknown keys in normal mode
break;
}
}
Important Practical Notes
- Never put character-producing keys (
a,shift+a, etc.) intocontributes.keybindings. Keyboard layouts differ; thetypeoverride is the portable way. - For more complex keys (Ctrl combinations, etc.) you can still use static keybindings that call your own commands, then forward them into the same handler.
- Full Vim (operators + motions + counts + registers + macros + visual modes + …) is a large project. VSCodeVim and vscode-neovim are thousands of lines for a reason.
- The official sample implements a surprising amount of real Vim behavior and is the best starting point for deeper study: https://github.com/microsoft/vscode-extension-samples/tree/main/vim-sample
Recommended Learning Path
- Scaffold a hello-world extension and get the
typeoverride working. - Implement
h j k l+i/ Esc and mode-dependent cursor. - Add a simple operator (
d+ motion) and count prefix. - Study the official
vim-sampleand (if you want production quality) the architecture of VSCodeVim or vscode-neovim.
That is the entire essential foundation. Everything else is building a more complete state machine on top of the type override + context keys.