1.4 Vim 模式:终端里的完整编辑器

源码位置src/vim/(5 个文件,共约 540 行) 激活方式:输入框里按 Esc 键,或在设置中开启 vim 模式


一句话理解

Claude Code 的输入框内置了完整的 Vim 键位系统——不是简单的方向键支持,而是带状态机的完整 NORMAL / INSERT 模式切换、文本对象、操作符、计数前缀、. 重复、u 撤销。


状态机设计(VimState)

Vim 模式的核心是一个两层状态机:

VimState
  ├── { mode: 'INSERT', insertedText: string }
  │     ← 记录插入的文字,用于 . 重复
  │
  └── { mode: 'NORMAL', command: CommandState }
        │
        ├── { type: 'idle' }                        ← 默认等待键入
        ├── { type: 'count', digits: string }       ← 数字前缀(3dw)
        ├── { type: 'operator', op, count }         ← 等待动作(d_ 中的 d)
        ├── { type: 'operatorCount', op, count, digits } ← 操作符后的数字
        ├── { type: 'operatorFind', op, count, find }   ← df<char>
        ├── { type: 'operatorTextObj', op, count, scope } ← daw / diw
        ├── { type: 'find', find, count }           ← f<char> 跳转
        ├── { type: 'g', count }                    ← g + gg/G
        ├── { type: 'operatorG', op, count }        ← dg + G
        ├── { type: 'replace', count }              ← r<char>
        └── { type: 'indent', dir, count }          ← >> / <<

源码注释中直接写了完整状态图(src/vim/types.ts 第 1-26 行),是罕见的"类型即文档"设计。


支持的完整键位

移动(Motion)

按键功能
h j k l左下上右
w W下一个词首
e E当前词尾
b B上一个词首
0行首
$行尾
^行首非空白
g g第一行
G最后一行
f<x> F<x>向前/后查找字符
t<x> T<x>向前/后查找字符(停在前一位)

操作符(Operator)

按键操作
d删除(delete)
c修改(change → 切换到 INSERT)
y复制(yank)

操作符 + 动作组合

dd    → 删除整行
d$    → 删除到行尾
dw    → 删除一个词
daw   → 删除一个词(含空格,around)
diw   → 删除一个词(不含空格,inner)
d3w   → 删除 3 个词
3dd   → 删除 3 行
cc    → 修改整行
c$    → 修改到行尾
yy    → 复制整行

文本对象(Text Object)

范围括号引号
inner (i)iwi( i[ i{i' i" i`
around (a)awa( a[ a{a' a" a`

其他常用操作

按键功能
i I进入 INSERT(当前位置 / 行首)
a A进入 INSERT(后一位 / 行尾)
o O新增行(下方 / 上方)+ INSERT
x删除当前字符
r<x>替换当前字符
p P粘贴(后 / 前)
u撤销
> <缩进 / 反缩进
J合并下一行
~切换大小写
.重复上次修改操作

PersistentState:跨命令记忆

// src/vim/types.ts
type PersistentState = {
  register: string | null      // 复制寄存器(最近一次 yank/delete 的内容)
  lastFind: LastFind | null    // 最后一次 f/F/t/T 查找(用于 ; 和 , 重复)
  lastInsert: string | null    // 最后一次 INSERT 内容(用于 . 重复)
  lastOp: LastOp | null        // 最后一次操作(用于 . 重复)
}

这解释了为什么 .; , 能正确重复——所有需要记忆的状态都持久化在 PersistentState 里。


与 Ink 的集成

Vim 状态机纯函数式设计,不直接操作 DOM。Claude Code 的 Ink 组件捕获原始键盘事件,传递给 transition() 函数:

// src/vim/transitions.ts(核心入口)
  state: CommandState,
  key: string,
  ctx: TransitionContext,
): TransitionResult

// TransitionResult
type TransitionResult = {
  next?: CommandState    // 下一个状态(不传 = 回到 idle)
  execute?: () => void   // 要执行的操作(副作用)
}

设计原则:状态机本身无副作用,只返回"下一状态"和"要执行什么操作"。副作用(光标移动、文本删除)由调用方执行。


为什么是纯函数式?

Vim 状态机用纯函数实现的好处:

  1. 可测试:输入 (state, key) → 输出 (nextState, operation),天然可单元测试
  2. 无竞态:不持有可变状态,并发键入不会污染全局
  3. 可追溯:每次状态转换都是清晰的函数调用,调试时 stack trace 直接反映操作序列

下一步