Vimrc Support
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>
Navigation with Display Lines
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:
- File-based vimrc — A
.vimrcfile on disk, loaded via the backend (e.g., Tauri IPC) - 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.