ai vim extension vscode 2026-07-25

By Xah Lee. Date: .
xtodo

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:

  1. Override the type command
    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:type so VS Code behaves normally.
  2. Use custom context keys (setContext)
    Publish your own keys such as vim.mode == 'Normal' or vim.inNormalMode.
    Keybindings and menus can then use when clauses that only fire in the right mode.

  3. Change cursor style
    editor.options.cursorStyle = TextEditorCursorStyle.Block (normal) vs Line (insert).

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

Recommended Learning Path

  1. Scaffold a hello-world extension and get the type override working.
  2. Implement h j k l + i / Esc and mode-dependent cursor.
  3. Add a simple operator (d + motion) and count prefix.
  4. Study the official vim-sample and (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.