Skip to main content

Overview

The Vim.defineMotion() function creates custom Vim motions. Motions are commands that move the cursor and can be combined with operators. For example, in dw (delete word), w is the motion.

Signature

Vim.defineMotion(
  name: string,
  callback: (
    cm: CodeMirror,
    head: Pos,
    motionArgs: MotionArgs,
    vim: VimState,
    inputState: InputState
  ) => Pos | [Pos, Pos] | null | undefined
): void

Parameters

name
string
required
The name of the motion. This name is used to reference the motion in keymaps.
callback
function
required
The function that performs the cursor movement. Receives:
  • cm (CodeMirror) - The editor instance
  • head (Pos) - Current cursor position with properties:
    • line (number) - Line number (0-indexed)
    • ch (number) - Character position (0-indexed)
  • motionArgs (MotionArgs) - Motion arguments including:
    • repeat (number) - Repeat count
    • forward (boolean) - Direction of motion
    • linewise (boolean) - Whether motion is linewise
    • inclusive (boolean) - Whether motion includes end position
  • vim (VimState) - The Vim state object
  • inputState (InputState) - Current input state
Should return:
  • Pos - New cursor position
  • [Pos, Pos] - Selection range (for visual selections)
  • null or undefined - No movement

Return Value

return
void
This function does not return a value.

Examples

Jump to Next Function

Define a motion to jump to the next function definition:
import { Vim } from "@replit/codemirror-vim";

// Add to keymap
Vim.mapCommand({
  keys: ']f',
  type: 'motion',
  motion: 'jumpToNextFunction',
  motionArgs: { forward: true }
});

Vim.mapCommand({
  keys: '[f',
  type: 'motion',
  motion: 'jumpToNextFunction',
  motionArgs: { forward: false }
});

// Define the motion
Vim.defineMotion('jumpToNextFunction', function(cm, head, motionArgs) {
  const forward = motionArgs.forward;
  const repeat = motionArgs.repeat || 1;
  const functionRegex = /^\s*function\s+\w+|^\s*const\s+\w+\s*=\s*(?:function|\()/;
  
  let line = head.line;
  let found = 0;
  
  // Search for function definitions
  while (forward ? line < cm.lastLine() : line > cm.firstLine()) {
    line += forward ? 1 : -1;
    const text = cm.getLine(line);
    
    if (functionRegex.test(text)) {
      found++;
      if (found === repeat) {
        return { line: line, ch: 0 };
      }
    }
  }
  
  // No function found, return current position
  return head;
});

Jump to Matching Bracket

Define a motion to jump to the matching bracket:
Vim.mapCommand({
  keys: 'gm',
  type: 'motion',
  motion: 'jumpToMatchingBracket'
});

Vim.defineMotion('jumpToMatchingBracket', function(cm, head, motionArgs) {
  const cursor = { line: head.line, ch: head.ch };
  const matchingBracket = cm.findMatchingBracket(cursor);
  
  if (matchingBracket && matchingBracket.to) {
    return matchingBracket.to;
  }
  
  return head;
});

Jump to Next Blank Line

Define a motion to jump to the next blank line:
Vim.mapCommand({
  keys: ']b',
  type: 'motion',
  motion: 'jumpToBlankLine',
  motionArgs: { forward: true }
});

Vim.mapCommand({
  keys: '[b',
  type: 'motion',
  motion: 'jumpToBlankLine',
  motionArgs: { forward: false }
});

Vim.defineMotion('jumpToBlankLine', function(cm, head, motionArgs) {
  const forward = motionArgs.forward;
  const repeat = motionArgs.repeat || 1;
  let line = head.line;
  let found = 0;
  
  while (forward ? line < cm.lastLine() : line > cm.firstLine()) {
    line += forward ? 1 : -1;
    const text = cm.getLine(line);
    
    if (text.trim() === '') {
      found++;
      if (found === repeat) {
        return { line: line, ch: 0 };
      }
    }
  }
  
  return head;
});

Jump by Indentation Level

Define a motion to jump to the next line with the same indentation:
Vim.mapCommand({
  keys: ']i',
  type: 'motion',
  motion: 'jumpToSameIndent',
  motionArgs: { forward: true }
});

Vim.defineMotion('jumpToSameIndent', function(cm, head, motionArgs) {
  const currentLine = cm.getLine(head.line);
  const currentIndent = currentLine.match(/^\s*/)[0].length;
  const forward = motionArgs.forward;
  let line = head.line;
  
  while (forward ? line < cm.lastLine() : line > cm.firstLine()) {
    line += forward ? 1 : -1;
    const text = cm.getLine(line);
    const indent = text.match(/^\s*/)[0].length;
    
    if (text.trim() && indent === currentIndent) {
      return { line: line, ch: indent };
    }
  }
  
  return head;
});

Text Object Motion

Define a motion that returns a range for a text object:
Vim.mapCommand({
  keys: 'af',
  type: 'motion',
  motion: 'aroundFunction',
  motionArgs: { textObjectInner: false }
});

Vim.defineMotion('aroundFunction', function(cm, head, motionArgs) {
  // Find the start and end of the current function
  const functionStart = findFunctionStart(cm, head.line);
  const functionEnd = findFunctionEnd(cm, head.line);
  
  if (functionStart !== null && functionEnd !== null) {
    // Return a range [start, end]
    return [
      { line: functionStart, ch: 0 },
      { line: functionEnd, ch: cm.getLine(functionEnd).length }
    ];
  }
  
  return head;
});

function findFunctionStart(cm, line) {
  const regex = /^\s*function\s+\w+/;
  while (line >= cm.firstLine()) {
    if (regex.test(cm.getLine(line))) return line;
    line--;
  }
  return null;
}

function findFunctionEnd(cm, line) {
  // Simplified: find the matching closing brace
  // In practice, you'd need proper brace matching
  let depth = 0;
  while (line <= cm.lastLine()) {
    const text = cm.getLine(line);
    for (const char of text) {
      if (char === '{') depth++;
      if (char === '}') depth--;
      if (depth === 0 && text.includes('}')) return line;
    }
    line++;
  }
  return null;
}

Usage

After defining a motion, you can use it:
  • By itself: Move the cursor (e.g., ]f to jump to next function)
  • With operators: Combine with operators (e.g., d]f to delete to next function)
  • With counts: Prefix with a count (e.g., 3]f to jump 3 functions forward)
  • In visual mode: Extend selection using the motion

Return Values

  • Single Pos: Moves cursor to that position
  • [Pos, Pos]: Creates a selection from first to second position (useful for text objects)
  • null or undefined: No movement (motion failed)

Notes

  • Motions must be added to the keymap using Vim.mapCommand() before they can be used
  • The motionArgs.repeat contains the count prefixed before the motion
  • Set motionArgs.linewise = true for line-based motions
  • Set motionArgs.inclusive = true to include the end position in operations
  • Motions work with all built-in operators (d, c, y, etc.)

MotionArgs Interface

interface MotionArgs {
  repeat: number;           // Repeat count
  forward?: boolean;        // Direction of motion
  linewise?: boolean;       // Linewise motion
  inclusive?: boolean;      // Include end position
  wordEnd?: boolean;        // Move to word end
  bigWord?: boolean;        // Use WORD instead of word
  textObjectInner?: boolean; // Inner text object
  toJumplist?: boolean;     // Add to jump list
}

See Also

Build docs developers (and LLMs) love