Skip to main content

Overview

CodeMirror Vim allows you to extend Vim’s functionality by defining custom operators, actions, and motions. This enables you to create powerful text manipulation commands tailored to your needs.

Defining Custom Operators

Operators are commands that act on text objects or motions (like d for delete, c for change, or y for yank).

Vim.defineOperator()

Use Vim.defineOperator() to create a custom operator:
import { Vim, getCM } from "@replit/codemirror-vim";

Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) {
  // Operator implementation
  // Make changes and return new cursor position
  return newHead;
});

Operator Function Signature

type OperatorFn = (
  cm: CodeMirrorV,
  args: OperatorArgs,
  ranges: CM5RangeInterface[],
  oldAnchor: Pos,
  newHead?: Pos
) => Pos | void
Parameters:
  • cm - CodeMirror instance with Vim state
  • args - Operator arguments (repeat count, register, etc.)
  • ranges - Array of text ranges to operate on
  • oldAnchor - Previous cursor anchor position
  • newHead - New cursor head position
Returns: New cursor position (optional)

Hard Wrap Example

Here’s the complete hard wrap operator example from the README:
import { vim, Vim, getCM } from "@replit/codemirror-vim";
import { defaultKeymap } from "@replit/codemirror-vim";

// Add custom key to the default keymap
defaultKeymap.push({
  keys: 'gq',
  type: 'operator',
  operator: 'hardWrap'
});

// Define the hardWrap operator
Vim.defineOperator("hardWrap", function(cm, operatorArgs, ranges, oldAnchor, newHead) {
  // Get text width setting
  const textWidth = cm.getOption('textwidth') || 80;
  
  // Process each range
  for (let i = 0; i < ranges.length; i++) {
    const range = ranges[i];
    const from = range.anchor;
    const to = range.head;
    
    // Get text in range
    const text = cm.getRange(from, to);
    
    // Wrap the text
    const wrapped = wrapText(text, textWidth);
    
    // Replace the range
    cm.replaceRange(wrapped, from, to);
  }
  
  // Return new cursor position
  return newHead;
});

function wrapText(text, width) {
  const words = text.split(/\s+/);
  const lines = [];
  let currentLine = '';
  
  for (const word of words) {
    if (currentLine.length + word.length + 1 <= width) {
      currentLine += (currentLine ? ' ' : '') + word;
    } else {
      if (currentLine) lines.push(currentLine);
      currentLine = word;
    }
  }
  
  if (currentLine) lines.push(currentLine);
  return lines.join('\n');
}
After defining an operator, you need to add it to the keymap to bind it to specific keys.

Adding Operators to the Keymap

To make your operator accessible, add it to the defaultKeymap:
import { defaultKeymap } from "@replit/codemirror-vim";

// Add operator to keymap
defaultKeymap.push({
  keys: 'gq',           // Key combination
  type: 'operator',     // Command type
  operator: 'hardWrap'  // Operator name
});
Choose key combinations that don’t conflict with existing Vim commands, or use leader keys.

Defining Custom Actions

Actions are commands that perform a specific operation (like dd to delete a line or o to open a new line).

Vim.defineAction()

Vim.defineAction("myAction", function(cm, actionArgs, vim) {
  // Perform action
  const cursor = cm.getCursor();
  cm.replaceRange('Hello!', cursor);
});

Action Function Signature

type ActionFn = (
  cm: CodeMirrorV,
  actionArgs: ActionArgs,
  vim: vimState
) => void
Parameters:
  • cm - CodeMirror instance
  • actionArgs - Action arguments (repeat count, etc.)
  • vim - Current Vim state

Custom Action Examples

Vim.defineAction("insertTimestamp", function(cm) {
  const timestamp = new Date().toISOString();
  const cursor = cm.getCursor();
  cm.replaceRange(timestamp, cursor);
});

// Add to keymap
defaultKeymap.push({
  keys: '<Leader>t',
  type: 'action',
  action: 'insertTimestamp'
});

Defining Custom Motions

Motions are commands that move the cursor (like w for word, j for down, } for paragraph).

Vim.defineMotion()

Vim.defineMotion("myMotion", function(cm, head, motionArgs, vim, inputState) {
  // Calculate new position
  const newPos = { line: head.line + 1, ch: 0 };
  return newPos;
});

Motion Function Signature

type MotionFn = (
  cm: CodeMirrorV,
  head: Pos,
  motionArgs: MotionArgs,
  vim: vimState,
  inputState: InputStateInterface
) => Pos | [Pos, Pos] | null | undefined
Parameters:
  • cm - CodeMirror instance
  • head - Current cursor position
  • motionArgs - Motion arguments (repeat, forward, etc.)
  • vim - Vim state
  • inputState - Input state
Returns: New position, or range for text objects

Custom Motion Examples

Vim.defineMotion("nextBlankLine", function(cm, head, motionArgs) {
  const forward = motionArgs.forward !== false;
  const lineCount = cm.lineCount();
  let line = head.line;
  
  while (forward ? line < lineCount - 1 : line > 0) {
    line += forward ? 1 : -1;
    const lineText = cm.getLine(line);
    if (lineText.trim() === '') {
      return { line: line, ch: 0 };
    }
  }
  
  return head;
});

// Add to keymap
defaultKeymap.push({
  keys: ']b',
  type: 'motion',
  motion: 'nextBlankLine',
  motionArgs: { forward: true }
});

defaultKeymap.push({
  keys: '[b',
  type: 'motion',
  motion: 'nextBlankLine',
  motionArgs: { forward: false }
});

Operator Arguments

The OperatorArgs type contains useful information:
type OperatorArgs = {
  repeat?: number,           // Repeat count
  forward?: boolean,         // Direction
  linewise?: boolean,        // Line-wise operation
  fullLine?: boolean,        // Full line mode
  registerName?: string,     // Target register
  indentRight?: boolean,     // Indent direction
  toLower?: boolean,         // Case conversion
  shouldMoveCursor?: boolean,// Move cursor after
  keepCursor?: boolean       // Keep cursor position
}

Complete Example: Comment Operator

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

// Define comment operator
Vim.defineOperator("toggleComment", function(cm, operatorArgs, ranges) {
  for (let i = 0; i < ranges.length; i++) {
    const range = ranges[i];
    const startLine = Math.min(range.anchor.line, range.head.line);
    const endLine = Math.max(range.anchor.line, range.head.line);
    
    // Check if lines are commented
    let allCommented = true;
    for (let line = startLine; line <= endLine; line++) {
      const text = cm.getLine(line);
      if (!text.trim().startsWith('//')) {
        allCommented = false;
        break;
      }
    }
    
    // Toggle comments
    for (let line = startLine; line <= endLine; line++) {
      const text = cm.getLine(line);
      if (allCommented) {
        // Remove comment
        const newText = text.replace(/^\/\/\s?/, '');
        cm.replaceRange(newText, { line, ch: 0 }, { line, ch: text.length });
      } else {
        // Add comment
        cm.replaceRange('// ', { line, ch: 0 });
      }
    }
  }
});

// Add to keymap with 'gc' keys
defaultKeymap.push({
  keys: 'gc',
  type: 'operator',
  operator: 'toggleComment',
  isEdit: true
});
When modifying defaultKeymap, make sure to do so before initializing the vim extension in your editor.

Best Practices

1

Handle Edge Cases

Always check for boundary conditions and invalid input:
if (line < 0 || line >= cm.lineCount()) {
  return head; // Return original position
}
2

Respect Vim Conventions

Follow Vim’s behavior patterns for consistency:
  • Return new cursor position from operators
  • Handle repeat counts properly
  • Support visual mode when appropriate
3

Test Thoroughly

Test your custom commands in different scenarios:
  • With different repeat counts (e.g., 3gq)
  • In visual mode
  • With empty selections
  • At document boundaries

Build docs developers (and LLMs) love