zudo-codemirror-wisdom

Type to search...

to open search from anywhere

Vimrc Support

CreatedMar 29, 2026UpdatedApr 3, 2026Takeshi Takatsudo

Apply vimrc-style configuration to @replit/codemirror-vim using Vim.handleEx().

Vimrc Support

@replit/codemirror-vim can execute vimrc-style commands at runtime using the Vim.handleEx() method. This allows users to provide a block of vim configuration text, which your application parses and applies line by line.

Vim.handleEx()

Vim.handleEx() executes a single Ex command string as if the user had typed it in command-line mode.

import { getCM, Vim } from "@replit/codemirror-vim";

const cm = getCM(view)!;
Vim.handleEx(cm, "set number");
Vim.handleEx(cm, "imap jk <Esc>");

The first argument is the CodeMirror adapter object, obtained via getCM(view). The second argument is the raw Ex command string (without the leading :).

Getting the CodeMirror Adapter

getCM() takes an EditorView and returns the internal CodeMirror adapter that the vim layer operates on.

import { getCM } from "@replit/codemirror-vim";

const cm = getCM(view);

⚠️ Warning

getCM() may return undefined if the vim extension is not active on the given view. Check the return value before passing it to Vim.handleEx().

Parsing a Vimrc String

A vimrc file consists of one command per line. Blank lines and lines starting with " (double quote) are comments.

function parseVimrc(content: string): string[] {
  return content
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line.length > 0 && !line.startsWith('"'));
}

Applying Vimrc Commands

Iterate over the parsed commands and execute each one. Wrap in try/catch because not all vimrc commands may be supported.

import { getCM, Vim } from "@replit/codemirror-vim";

function applyVimrc(view: EditorView, vimrcContent: string) {
  const cm = getCM(view);
  if (!cm) return;

  for (const cmd of parseVimrc(vimrcContent)) {
    try {
      Vim.handleEx(cm, cmd);
    } catch (e) {
      console.warn("vimrc command failed:", cmd, e);
    }
  }
}

Example Vimrc Content

A typical vimrc string that a user might provide:

" Basic settings
set number

" Key mappings
imap jk <Esc>
nmap H ^
nmap L $
nnoremap j gj
nnoremap k gk

" Leader mappings
map <Space> <Leader>
nmap <Leader>w :w<CR>
nmap <Leader>q :q<CR>

Common Vimrc Settings

Line Numbers

set number
set nonumber

These work out of the box with @replit/codemirror-vim.

Line Wrapping

set wrap
set nowrap

📝 Note

set wrap and set nowrap require a custom option definition with Vim.defineOption(). See the Vim Options page for the implementation.

Insert Mode Escape Mapping

imap jk <Esc>

Map j/k to move by display lines (wrapped lines) instead of buffer lines.

nnoremap j gj
nnoremap k gk

Leader Key Configuration

map <Space> <Leader>
nmap <Leader>w :w<CR>
nmap <Leader>q :q<CR>
nmap <Leader>h :nohlsearch<CR>

Timing Considerations

Apply vimrc commands after the EditorView is created and the vim extension is initialized. If you apply commands too early, getCM(view) may return undefined.

const view = new EditorView({
  extensions: [vim(), basicSetup],
  parent: document.getElementById("editor")!,
});

// Apply vimrc after view creation
applyVimrc(view, userVimrcContent);

Loading Vimrc from User Input

A practical pattern is to let users paste or upload their vimrc, store it, and reapply it when the editor initializes.

function loadVimrc(): string | null {
  return localStorage.getItem("user-vimrc");
}

function saveVimrc(content: string) {
  localStorage.setItem("user-vimrc", content);
}

// On editor init
const vimrc = loadVimrc();
if (vimrc) {
  applyVimrc(view, vimrc);
}

File-Based + Settings-Based Vimrc Layering

In a desktop application, you may want to support two vimrc sources:

  1. File-based vimrc — A .vimrc file on disk, loaded via the backend (e.g., Tauri IPC)
  2. Settings-based vimrc — A vimrc string stored in the application settings UI

The layering strategy applies the file-based vimrc first, then the settings-based vimrc second. This way, settings-based overrides take precedence, and the user can customize behavior without editing the disk file.

import { getCM, Vim } from "@replit/codemirror-vim";

async function applyLayeredVimrc(
  view: EditorView,
  settingsVimrc: string,
  readVimrcFromDisk: () => Promise<string | null>,
) {
  const cm = getCM(view);
  if (!cm) return;

  const applyVimrc = (source: string, label: string) => {
    for (const cmd of parseVimrc(source)) {
      try {
        Vim.handleEx(cm, cmd);
      } catch (e) {
        console.warn(`${label} vimrc command failed: ${cmd}`, e);
      }
    }
  };

  // Layer 1: file-based vimrc (loaded from disk)
  const fileVimrc = await readVimrcFromDisk();
  if (fileVimrc) {
    applyVimrc(fileVimrc, "file-based");
  }

  // Layer 2: settings-based vimrc (overrides file-based)
  if (settingsVimrc) {
    applyVimrc(settingsVimrc, "settings");
  }
}

💡 Tip

This layering pattern mirrors how shell configuration works (/etc/profile then ~/.profile). The file-based vimrc provides machine-level defaults, while the settings-based vimrc allows per-application customization through the UI.

Handling Async Loading

Since file-based vimrc is loaded asynchronously (via file system APIs), the vimrc application happens after the EditorView is created. Use a cancellation guard to prevent applying vimrc to a destroyed editor:

let cancelled = false;

readVimrcFromDisk().then((vimrcContent) => {
  if (cancelled) return;
  const cm = getCM(view);
  if (!cm) return;

  if (vimrcContent) {
    applyVimrc(vimrcContent, "file-based");
  }
  if (settingsVimrc) {
    applyVimrc(settingsVimrc, "settings");
  }
});

// In cleanup:
cancelled = true;

This is important in React applications where the editor may be destroyed and recreated before the async vimrc read completes.

Revision History