Overview
The @replit/codemirror-vim package provides a compatibility layer that allows you to use the familiar CodeMirror 5 Vim API with CodeMirror 6. This makes migration straightforward while taking advantage of CodeMirror 6’s improved architecture and performance.
The CM5 Vim API remains largely compatible with CM6 thanks to the getCM() compatibility adapter. Most existing code will work with minimal changes.
Key Architecture Changes
EditorView vs CodeMirror Instance
In CodeMirror 5, you worked directly with a CodeMirror instance. In CodeMirror 6, the main interface is EditorView:
CodeMirror 5:
import CodeMirror from 'codemirror';
import 'codemirror/keymap/vim';
const editor = CodeMirror(document.querySelector('#editor'), {
value: "Hello World",
keyMap: 'vim'
});
CodeMirror 6:
import { EditorView, basicSetup } from 'codemirror';
import { vim } from '@replit/codemirror-vim';
const view = new EditorView({
doc: "Hello World",
extensions: [
vim(), // Include vim before other keymaps
basicSetup,
],
parent: document.querySelector('#editor'),
});
Extension-Based Configuration
CodeMirror 6 uses an extension-based architecture instead of options:
- CM5: Configuration through options object
- CM6: Configuration through extensions array
The vim() extension must be included before other keymaps to ensure Vim bindings take precedence.
Using the CM5 Compatibility API
The getCM() function provides access to a compatibility layer that implements the CM5 Vim API on top of CM6:
import { Vim, getCM } from '@replit/codemirror-vim';
let cm = getCM(view);
// Use cm to access the CM5 API
Vim.exitInsertMode(cm);
Vim.handleKey(cm, "<Esc>");
The view.cm Property
Alternatively, you can access the compatibility layer directly through the view.cm property:
// Both approaches work identically
let cm = getCM(view);
let cm = view.cm;
// Then use the CM5 API
Vim.map("jj", "<Esc>", "insert");
Using getCM(view) is recommended over view.cm for type safety and clearer intent, though both work identically.
Common Migration Patterns
Defining Ex Commands
Ex commands work exactly the same way in CM6:
import { Vim } from '@replit/codemirror-vim';
Vim.defineEx('write', 'w', function() {
// Save the file
console.log('Saving file...');
});
Vim.defineEx('quit', 'q', function() {
// Close the editor
console.log('Closing editor...');
});
The Vim object is a global API that doesn’t require a cm instance for defining commands, maps, or operators.
Mapping Keys
Key mappings work identically to CM5:
import { Vim } from '@replit/codemirror-vim';
// Map in insert mode
Vim.map("jj", "<Esc>", "insert");
// Map in normal mode
Vim.map("Y", "y$");
// Map with custom action
Vim.map("<C-d>", "<C-d>zz"); // Center after half-page down
Unmapping Keys
Vim.unmap("jj", "insert");
Vim.unmap("Y");
Defining Custom Operators
Custom operators allow you to create new text object operations:
import { Vim } from '@replit/codemirror-vim';
// Add to the default keymap
defaultKeymap.push({
keys: 'gq',
type: 'operator',
operator: 'hardWrap'
});
Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) {
// Implement hard wrapping logic
const textWidth = cm.getOption('textwidth') || 80;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
cm.hardWrap({
from: range.head.line,
to: range.anchor.line,
column: textWidth
});
}
// Return new cursor position
return newHead;
});
What Changed
Extension Loading
The vim() extension must be loaded before other keymaps like basicSetup. Otherwise, default keybindings may conflict with Vim bindings.
Correct order:
new EditorView({
extensions: [
vim(), // Load first
basicSetup, // Load after
]
});
Incorrect order:
new EditorView({
extensions: [
basicSetup, // Will override Vim keys
vim(), // Too late
]
});
Selection Rendering
CodeMirror 6 requires explicit configuration for visual mode selection rendering:
import { EditorView, basicSetup } from 'codemirror';
import { vim } from '@replit/codemirror-vim';
import { drawSelection } from '@codemirror/view';
new EditorView({
extensions: [
vim(),
drawSelection(), // Required for visual mode
// ... other extensions
]
});
If you’re using basicSetup, the drawSelection plugin is already included. Only add it explicitly if you’re building a custom configuration.
Status Bar
The status bar is now opt-in through the status option:
import { vim } from '@replit/codemirror-vim';
new EditorView({
extensions: [
vim({ status: true }), // Enable status bar
]
});
The status bar displays:
- Current mode (NORMAL, INSERT, VISUAL, etc.)
- Pending keystrokes
- Command output
What Stayed the Same
The following APIs work identically to CM5:
Vim Global API
All global Vim configuration methods remain unchanged:
Vim.defineEx() - Define ex commands
Vim.map() - Map keys
Vim.unmap() - Unmap keys
Vim.defineOperator() - Define custom operators
Vim.defineMotion() - Define custom motions
Vim.defineAction() - Define custom actions
Vim.handleKey() - Programmatically trigger key events
Vim.exitInsertMode() - Exit insert mode
Vim.exitVisualMode() - Exit visual mode
CodeMirror Instance Methods
When using the compatibility layer (getCM(view)), these methods work as expected:
cm.getCursor() - Get cursor position
cm.setCursor() - Set cursor position
cm.getSelection() - Get selected text
cm.replaceSelection() - Replace selection
cm.getRange() - Get text range
cm.replaceRange() - Replace text range
cm.getValue() - Get entire document
cm.setValue() - Set entire document
Migration Checklist
Update Package Dependencies
Replace CodeMirror 5 dependencies with CodeMirror 6:npm uninstall codemirror
npm install codemirror @replit/codemirror-vim
Update Editor Initialization
Replace CM5 initialization with CM6 EditorView:import { EditorView, basicSetup } from 'codemirror';
import { vim } from '@replit/codemirror-vim';
const view = new EditorView({
doc: initialContent,
extensions: [vim(), basicSetup],
parent: document.querySelector('#editor'),
});
Update API Access
Replace direct CodeMirror access with getCM():import { Vim, getCM } from '@replit/codemirror-vim';
const cm = getCM(view);
Vim.handleKey(cm, "<Esc>");
Update Vim Configuration
Move Vim configuration to use the imported Vim object:// Ex commands
Vim.defineEx('write', 'w', saveFile);
// Key mappings
Vim.map("jj", "<Esc>", "insert");
Vim.map("Y", "y$");
// Custom operators
Vim.defineOperator("hardWrap", wrapFunction);
Test Visual Mode
Ensure drawSelection is available (included in basicSetup):import { drawSelection } from '@codemirror/view';
// Only needed if NOT using basicSetup
new EditorView({
extensions: [vim(), drawSelection()],
});
Verify Event Handlers
Update event handlers to use CM6’s event system if needed. The compatibility layer handles most events automatically.
Breaking Changes
Removed Methods
Some CM5 methods are not available in the compatibility layer:
cm.addKeyMap() / cm.removeKeyMap() - Use CM6 keymap extensions instead
cm.setOption() - Use CM6 configuration compartments and effects
cm.getOption() - Most options are accessible, but some may require CM6 state access
Behavior Differences
Read-only Mode: The readOnly option works differently in CM6. Use the EditorState.readOnly facet instead of the CM5 option.
CM5:
cm.setOption('readOnly', true);
CM6:
import { EditorState } from '@codemirror/state';
view.dispatch({
effects: StateEffect.appendConfig.of(EditorState.readOnly.of(true))
});
Example: Complete Migration
Before (CM5)
import CodeMirror from 'codemirror';
import 'codemirror/keymap/vim';
const editor = CodeMirror(document.getElementById('editor'), {
value: 'Hello World',
keyMap: 'vim',
lineNumbers: true
});
// Custom ex command
CodeMirror.Vim.defineEx('write', 'w', function(cm) {
saveFile(cm.getValue());
});
// Key mapping
CodeMirror.Vim.map('jj', '<Esc>', 'insert');
// Exit insert mode programmatically
CodeMirror.Vim.exitInsertMode(editor);
After (CM6)
import { EditorView, basicSetup } from 'codemirror';
import { vim, Vim, getCM } from '@replit/codemirror-vim';
const view = new EditorView({
doc: 'Hello World',
extensions: [
vim({ status: true }),
basicSetup,
],
parent: document.getElementById('editor'),
});
// Custom ex command (unchanged)
Vim.defineEx('write', 'w', function(cm) {
saveFile(view.state.doc.toString());
});
// Key mapping (unchanged)
Vim.map('jj', '<Esc>', 'insert');
// Exit insert mode programmatically
const cm = getCM(view);
Vim.exitInsertMode(cm);
Getting Help
If you encounter issues during migration:
- Check the API Reference for detailed method documentation
- Review the Configuration Guide for extension setup
- See Examples for common use cases
- Open an issue on GitHub for bugs or feature requests
Most CM5 Vim code will work with minimal changes. Start by updating imports and initialization, then test your existing configuration.