InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

17-vim-mode

Shared from "Claude-Code" on Inkdown

Vim Mode System

Overview

Claude Code includes a Vim mode for efficient keyboard navigation within the terminal interface. It implements core Vim concepts: motions, operators, text objects, and modal editing.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                    VIM MODE ARCHITECTURE                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      MODAL EDITING                                   │   │
│  │                                                                      │   │
│  │     ┌──────────┐      ┌──────────┐      ┌──────────┐              │   │
│  │     │  NORMAL  │─────►│ INSERT   │◄────►│ REPLACE  │              │   │
│  │     │  (nav)   │◄─────│ (type)   │      │ (type)   │              │   │
│  │     └────┬─────┘      └──────────┘      └──────────┘              │   │
│  │          │                                                          │   │
│  │          │ Visual                                                   │   │
│  │          ▼                                                           │   │
│  │     ┌──────────┐                                                     │   │
│  │     │ VISUAL   │  (select)                                            │   │
│  │     │  MODE  │                                                     │   │
│  │     └──────────┘                                                     │   │
│  │                                                                      │   │
│  │  ESC: Return to NORMAL                                              │   │
│  │  i/I/a/A/o/O: Enter INSERT                                          │   │
│  │  v/V: Enter VISUAL                                                  │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                    MOTION SYSTEM                                     │   │
│  │                                                                      │   │
│  │  CHARACTER      WORD          LINE           DOCUMENT               │   │
│  │  h j k l        w b e         0 ^ $          gg G                   │   │
│  │                                                                      │   │
│  │  h: left        w: next word  0: start      gg: top                 │   │
│  │  j: down        b: prev word  ^: first char G: bottom               │   │
│  │  k: up          e: end word   $: end                              │   │
│  │  l: right                                                            │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                    OPERATOR SYSTEM                                   │   │
│  │                                                                      │   │
│  │  d: delete (cut)      dd: delete line                               │   │
│  │  y: yank (copy)      yy: yank line                                 │   │
│  │  c: change (delete + insert)  cc: change line                       │   │
│  │  p: paste            P: paste before                                │   │
│  │  >: indent           <: unindent                                    │   │
│  │                                                                      │   │
│  │  OPERATOR + MOTION = ACTION:                                        │   │
│  │  dw = delete word, d$ = delete to end, dgg = delete to top          │   │
│  │                                                                      │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Files

FilePurpose
vim/types.tsVim type definitions
vim/motions.tsMotion implementations
vim/operators.tsOperator implementations
vim/transitions.tsMode transitions
vim/textObjects.tsText object definitions
hooks/useVimMode.tsReact hook for Vim integration
components/BaseTextInput.tsxVim-enabled input component

Vim State

TypeScript
// vim/types.ts
export type VimMode =
  | 'normal'
  | 'insert'
  | 'replace'
  | 'visual'
  | 'visual-line'

export type VimState = {
  mode: VimMode

  // Cursor position
  cursor: {
    line: number
    col: number
  }

  // Visual selection
  visualStart?: {
    line: number
    col: number
  }

  // Command buffer (for multi-key commands)
  commandBuffer: string

  // Register contents
  registers: Map<string, string>
  unnamedRegister: string

  // Marks (bookmarks)
  marks: Map<string, { line: number; col: number }>

  // Last change
  lastChange?: {
    operation: string
    motion: string
  }

  // Count for numeric prefixes
  count: number
}

export function createInitialVimState(): VimState {
  return {
    mode: 'normal',
    cursor: { line: 0, col: 0 },
    commandBuffer: '',
    registers: new Map(),
    unnamedRegister: '',
    marks: new Map(),
    count: 0,
  }
}

Motions

Motion Interface
TypeScript
// vim/motions.ts
export type Motion = {
  name: string
  description: string

  // Execute motion from current position
  execute(state: VimState, text: string[]): CursorPosition

  // Is this motion linewise or characterwise?
  linewise: boolean
}

export type CursorPosition = {
  line: number
  col: number
}
Basic Motions
TypeScript
// Character motions
export const charMotions: Record<string, Motion> = {
  h: {
    name: 'left',
    description: 'Move cursor left',
    linewise: false,
    execute(state, text) {
      return {
        line: state.cursor.line,
        col: Math.max(0, state.cursor.col - 1),
      }
    },
  },

  j: {
    name: 'down',
    description: 'Move cursor down',
    linewise: true,
    execute(state, text) {
      return {
        line: Math.min(text.length - 1, state.cursor.line + 1),
        col: state.cursor.col,
      }
    },
  },

  k: {
    name: 'up',
    description: 'Move cursor up',
    linewise: true,
    execute(state, text) {
      return {
        line: Math.max(0, state.cursor.line - 1),
        col: state.cursor.col,
      }
    },
  },

  l: {
    name: 'right',
    description: 'Move cursor right',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line] || ''
      return {
        line: state.cursor.line,
        col: Math.min(line.length, state.cursor.col + 1),
      }
    },
  },
}

// Word motions
export const wordMotions: Record<string, Motion> = {
  w: {
    name: 'word-forward',
    description: 'Forward to start of next word',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line]
      const rest = line.slice(state.cursor.col)

      // Find next word start
      const match = rest.match(/\w+/)
      if (match && match.index !== undefined) {
        return {
          line: state.cursor.line,
          col: state.cursor.col + match.index + match[0].length,
        }
      }

      // No more words on this line, go to next line
      if (state.cursor.line < text.length - 1) {
        return { line: state.cursor.line + 1, col: 0 }
      }

      return state.cursor
    },
  },

  b: {
    name: 'word-backward',
    description: 'Backward to start of previous word',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line]
      const before = line.slice(0, state.cursor.col)

      // Find previous word
      const words = before.match(/\w+/g)
      if (words && words.length > 0) {
        const lastWord = words[words.length - 1]
        const idx = before.lastIndexOf(lastWord)
        return { line: state.cursor.line, col: idx }
      }

      // No more words, go to previous line
      if (state.cursor.line > 0) {
        const prevLine = text[state.cursor.line - 1]
        return { line: state.cursor.line - 1, col: prevLine.length }
      }

      return state.cursor
    },
  },

  e: {
    name: 'word-end',
    description: 'Forward to end of word',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line]
      const rest = line.slice(state.cursor.col + 1)

      const match = rest.match(/\w+/)
      if (match) {
        return {
          line: state.cursor.line,
          col: state.cursor.col + 1 + match[0].length - 1,
        }
      }

      return state.cursor
    },
  },
}

// Line motions
export const lineMotions: Record<string, Motion> = {
  '0': {
    name: 'line-start',
    description: 'Start of line',
    linewise: false,
    execute(state) {
      return { line: state.cursor.line, col: 0 }
    },
  },

  '^': {
    name: 'line-first-char',
    description: 'First non-blank character',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line]
      const match = line.match(/\S/)
      return {
        line: state.cursor.line,
        col: match ? match.index! : 0,
      }
    },
  },

  '$': {
    name: 'line-end',
    description: 'End of line',
    linewise: false,
    execute(state, text) {
      const line = text[state.cursor.line]
      return { line: state.cursor.line, col: line.length }
    },
  },

  gg: {
    name: 'document-start',
    description: 'Start of document',
    linewise: true,
    execute() {
      return { line: 0, col: 0 }
    },
  },

  G: {
    name: 'document-end',
    description: 'End of document',
    linewise: true,
    execute(state, text) {
      const lastLine = text.length - 1
      return { line: lastLine, col: text[lastLine].length }
    },
  },
}

Operators

Operator Interface
TypeScript
// vim/operators.ts
export type Operator = {
  name: string
  description: string

  // Execute operator on range
  execute(
    state: VimState,
    text: string[],
    range: Range
  ): { newText: string[]; newCursor: CursorPosition }
}

export type Range = {
  start: CursorPosition
  end: CursorPosition
  linewise: boolean
}
Built-in Operators
TypeScript
export const operators: Record<string, Operator> = {
  d: {
    name: 'delete',
    description: 'Delete text',
    execute(state, text, range) {
      const newText = [...text]

      if (range.linewise) {
        // Delete whole lines
        const start = Math.min(range.start.line, range.end.line)
        const end = Math.max(range.start.line, range.end.line)
        newText.splice(start, end - start + 1)

        // Save to register
        const deleted = text.slice(start, end + 1).join('\n')
        state.unnamedRegister = deleted

        return {
          newText,
          newCursor: { line: start, col: 0 },
        }
      } else {
        // Delete characters
        const line = newText[range.start.line]
        const before = line.slice(0, range.start.col)
        const after = line.slice(range.end.col)
        newText[range.start.line] = before + after

        // Save to register
        state.unnamedRegister = line.slice(range.start.col, range.end.col)

        return {
          newText,
          newCursor: range.start,
        }
      }
    },
  },

  y: {
    name: 'yank',
    description: 'Yank (copy) text',
    execute(state, text, range) {
      // Don't modify text, just copy to register
      if (range.linewise) {
        const start = Math.min(range.start.line, range.end.line)
        const end = Math.max(range.start.line, range.end.line)
        state.unnamedRegister = text.slice(start, end + 1).join('\n')
      } else {
        const line = text[range.start.line]
        state.unnamedRegister = line.slice(range.start.col, range.end.col)
      }

      return {
        newText: text,
        newCursor: range.start,
      }
    },
  },

  c: {
    name: 'change',
    description: 'Change (delete then insert)',
    execute(state, text, range) {
      // First delete
      const deleteResult = operators.d.execute(state, text, range)

      // Then switch to insert mode
      state.mode = 'insert'

      return deleteResult
    },
  },

  p: {
    name: 'put',
    description: 'Put (paste) text',
    execute(state, text, range) {
      const toPaste = state.unnamedRegister
      const newText = [...text]

      if (state.mode === 'normal') {
        // Paste after cursor
        const line = newText[range.start.line]
        const before = line.slice(0, range.start.col + 1)
        const after = line.slice(range.start.col + 1)
        newText[range.start.line] = before + toPaste + after

        return {
          newText,
          newCursor: {
            line: range.start.line,
            col: range.start.col + toPaste.length,
          },
        }
      }

      return { newText, newCursor: range.start }
    },
  },
}

Mode Transitions

TypeScript
// vim/transitions.ts
export function handleModeTransition(
  state: VimState,
  key: string
): VimState {
  switch (state.mode) {
    case 'normal':
      return handleNormalMode(state, key)

    case 'insert':
      return handleInsertMode(state, key)

    case 'visual':
      return handleVisualMode(state, key)

    default:
      return state
  }
}

function handleNormalMode(state: VimState, key: string): VimState {
  // ESC is no-op in normal
  if (key === 'Escape') return state

  // Enter insert mode
  if (key === 'i') {
    return { ...state, mode: 'insert' }
  }
  if (key === 'I') {
    // Insert at start of line
    return {
      ...state,
      mode: 'insert',
      cursor: { ...state.cursor, col: 0 },
    }
  }
  if (key === 'a') {
    // Append after cursor
    return {
      ...state,
      mode: 'insert',
      cursor: { ...state.cursor, col: state.cursor.col + 1 },
    }
  }
  if (key === 'A') {
    // Append at end of line
    return {
      ...state,
      mode: 'insert',
      cursor: { ...state.cursor, col: Infinity },
    }
  }

  // Enter visual mode
  if (key === 'v') {
    return {
      ...state,
      mode: 'visual',
      visualStart: state.cursor,
    }
  }

  // Operators
  if ('dyc'.includes(key)) {
    return {
      ...state,
      commandBuffer: key,
    }
  }

  // Motions
  const motion = findMotion(key)
  if (motion) {
    const newCursor = motion.execute(state, getCurrentText())
    return { ...state, cursor: newCursor, commandBuffer: '' }
  }

  return state
}

function handleInsertMode(state: VimState, key: string): VimState {
  // ESC returns to normal
  if (key === 'Escape') {
    return { ...state, mode: 'normal' }
  }

  // Regular keys insert text
  // (handled by input component)
  return state
}

function handleVisualMode(state: VimState, key: string): VimState {
  // ESC returns to normal
  if (key === 'Escape') {
    return {
      ...state,
      mode: 'normal',
      visualStart: undefined,
    }
  }

  // Motions extend selection
  const motion = findMotion(key)
  if (motion) {
    const newCursor = motion.execute(state, getCurrentText())
    return { ...state, cursor: newCursor }
  }

  // Operators act on selection
  if ('dy'.includes(key)) {
    const range = calculateVisualRange(state)
    const operator = operators[key]
    const result = operator.execute(state, getCurrentText(), range)

    return {
      ...state,
      mode: 'normal',
      visualStart: undefined,
    }
  }

  return state
}

React Integration

TypeScript
// hooks/useVimMode.ts
export function useVimMode(
  initialText: string,
  onChange: (text: string) => void
): VimModeState {
  const [vimState, setVimState] = useState(createInitialVimState())
  const [text, setText] = useState(initialText)

  const handleVimKey = useCallback((key: string) => {
    setVimState(prev => {
      const newState = handleModeTransition(prev, key)

      // If text changed, notify parent
      if (newState.lastChange) {
        const newText = applyVimChange(text, newState.lastChange)
        setText(newText)
        onChange(newText)
      }

      return newState
    })
  }, [text, onChange])

  return {
    vimState,
    text,
    handleVimKey,
    cursorPosition: vimState.cursor,
  }
}
Vim-Enabled Input
TypeScript
// components/VimTextInput.tsx
export function VimTextInput({
  value,
  onChange,
  vimMode = true,
}: VimTextInputProps) {
  const { vimState, handleVimKey, cursorPosition } = useVimMode(value, onChange)

  useInput((input, key) => {
    if (!vimMode) {
      // Normal typing
      onChange(value + input)
      return
    }

    if (vimState.mode === 'insert') {
      if (key.escape) {
        handleVimKey('Escape')
      } else {
        // Insert character
        onChange(insertAt(value, cursorPosition, input))
      }
    } else {
      // Normal mode - handle as Vim command
      handleVimKey(getVimKey(input, key))
    }
  })

  return (
    <Box>
      <Text>{value}</Text>
      {vimMode && (
        <Text dimColor>
          -- {vimState.mode.toUpperCase()} --
        </Text>
      )}
    </Box>
  )
}

Key Concepts

  1. Modal Editing: Different keys do different things in different modes
  2. Composability: Operators + Motions = Powerful commands
  3. Text Objects: iw (inner word), i" (inner quote), etc.
  4. Registers: Multiple clipboards for advanced operations
  5. Counts: 3dd = delete 3 lines

Debugging Vim

Bash
# Show current mode
:set showmode

# Verbose key logging
:set verbose=20

# Check vim state
claude /config vim