在 IntelliJ IDEA 里复刻 Cursor 式内联审查:架构复盘-从放弃到拾起:如何用 LineStatusTracker 拯救一个烂掉的项目

目录


序言:放弃那一刻

2026年6月2日下午3点,我关掉了电脑。

72个Kotlin文件。60多个版本。将近两个月的时间。所有这一切,只是为了在IntelliJ IDEA里给AI改的代码加一层"审查UI"------绿底高亮改动行、红色标记删除线、两个按钮RejectKeep让用户一键接受或回滚。

看起来很简单。做起来地狱。

最绝望的不是持续失败,而是成功和失败在24小时内无缝切换。上午用户说"非常好",下午就bug重现。同一份zip包,不同的打开顺序,完全不同的结果。这不是代码质量问题,这是架构的本质缺陷在对我呐喊。

那一刻我想:算了,不弄了。以后提起这条路,能躲多远躲多远。

但三天后,一份分析报告彻底改变了我对这个项目的理解。


产品是什么(README 一句话)

Cursor 风格的 IntelliJ IDEA 内嵌代码审查插件 ------基于 LineStatusTracker,零侵入。

AI(Claude Code / CC GUI / Copilot / 终端改文件)写完代码后,你在编辑器里直接看到绿底新增、红删标移除,每个改动块旁 Reject Keep 一键决策,顶栏负责多文件导航与批量 Keep。不打开 Diff 窗,也能完成块级审查。


先看看效果

和cursor 的效果一样

第一章:反思之前,先看看现在发生了什么

所有AI工具改的代码,都能用这个插件

这个插件的初衷是为Claude Code的IDEA集成(CC GUI)设计的。但重新思考后我意识到一个关键事实:

插件监听的不是"谁在改代码",而是"磁盘上的文件变了"。

这意味着什么?

  • Claude Code在终端敲命令,改了一个Java文件 → 磁盘变了 → VFS事件触发 → 插件激活 → 你在IDEA里看到绿底和审查按钮
  • GitHub Copilot在编辑器补全代码 → 磁盘变了 → 同样的流程
  • 甚至你自己用Vim在terminal改文件 → 也能触发这套流程

只要是VFS(Virtual File System)检测到内容变化,我们就知道有改动了。不管改动来自哪个AI、哪个终端、哪次操作,它都会被捕捉。
#mermaid-svg-KGje0adZHXA8p18F{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KGje0adZHXA8p18F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KGje0adZHXA8p18F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KGje0adZHXA8p18F .error-icon{fill:#552222;}#mermaid-svg-KGje0adZHXA8p18F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KGje0adZHXA8p18F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KGje0adZHXA8p18F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KGje0adZHXA8p18F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KGje0adZHXA8p18F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KGje0adZHXA8p18F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KGje0adZHXA8p18F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KGje0adZHXA8p18F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KGje0adZHXA8p18F .marker.cross{stroke:#333333;}#mermaid-svg-KGje0adZHXA8p18F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KGje0adZHXA8p18F p{margin:0;}#mermaid-svg-KGje0adZHXA8p18F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KGje0adZHXA8p18F .cluster-label text{fill:#333;}#mermaid-svg-KGje0adZHXA8p18F .cluster-label span{color:#333;}#mermaid-svg-KGje0adZHXA8p18F .cluster-label span p{background-color:transparent;}#mermaid-svg-KGje0adZHXA8p18F .label text,#mermaid-svg-KGje0adZHXA8p18F span{fill:#333;color:#333;}#mermaid-svg-KGje0adZHXA8p18F .node rect,#mermaid-svg-KGje0adZHXA8p18F .node circle,#mermaid-svg-KGje0adZHXA8p18F .node ellipse,#mermaid-svg-KGje0adZHXA8p18F .node polygon,#mermaid-svg-KGje0adZHXA8p18F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KGje0adZHXA8p18F .rough-node .label text,#mermaid-svg-KGje0adZHXA8p18F .node .label text,#mermaid-svg-KGje0adZHXA8p18F .image-shape .label,#mermaid-svg-KGje0adZHXA8p18F .icon-shape .label{text-anchor:middle;}#mermaid-svg-KGje0adZHXA8p18F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KGje0adZHXA8p18F .rough-node .label,#mermaid-svg-KGje0adZHXA8p18F .node .label,#mermaid-svg-KGje0adZHXA8p18F .image-shape .label,#mermaid-svg-KGje0adZHXA8p18F .icon-shape .label{text-align:center;}#mermaid-svg-KGje0adZHXA8p18F .node.clickable{cursor:pointer;}#mermaid-svg-KGje0adZHXA8p18F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KGje0adZHXA8p18F .arrowheadPath{fill:#333333;}#mermaid-svg-KGje0adZHXA8p18F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KGje0adZHXA8p18F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KGje0adZHXA8p18F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KGje0adZHXA8p18F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KGje0adZHXA8p18F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KGje0adZHXA8p18F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KGje0adZHXA8p18F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KGje0adZHXA8p18F .cluster text{fill:#333;}#mermaid-svg-KGje0adZHXA8p18F .cluster span{color:#333;}#mermaid-svg-KGje0adZHXA8p18F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KGje0adZHXA8p18F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KGje0adZHXA8p18F rect.text{fill:none;stroke-width:0;}#mermaid-svg-KGje0adZHXA8p18F .icon-shape,#mermaid-svg-KGje0adZHXA8p18F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KGje0adZHXA8p18F .icon-shape p,#mermaid-svg-KGje0adZHXA8p18F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KGje0adZHXA8p18F .icon-shape .label rect,#mermaid-svg-KGje0adZHXA8p18F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KGje0adZHXA8p18F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KGje0adZHXA8p18F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KGje0adZHXA8p18F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写磁盘


AI / 终端 / Copilot
VFS 事件
文件在 VCS 内?
忽略
INACTIVE → ACTIVE
LST 异步重算 ranges
渲染绿底 / 红删 / 块按钮 / 顶栏

这是插件最精妙的地方:通用性来自于底层观察点的选择。 我们没有去硬编码"CC GUI的改动"、"Copilot的改动",而是监听了所有写盘事件。

但这个优势并没有救我

知道原理≠代码写得好。就像一个医生知道人体解剖学,却在手术中一团糟。

我的问题是:我在这个简单的观察点上,硬生生堆砌了5套互不相容的状态系统。


第二章:那份改变人生的分析报告

放弃的第三天,一份分析报告出现了。

开头就是一句话,像一巴掌:

"你的项目不是'代码写得不好',而是把一个本来该用'纯函数 + 单一职责'解决的问题,硬生生塞进了一套'多逻辑糅合 + 事件驱动 + 异步对齐'的系统。所以代码坏掉不是bug,是架构的必然产物。"

然后报告指向了一个我一直视若无睹的东西:LineStatusTracker(LST)

那条绿线背后,藏着什么

你有没有注意过IntelliJ IDEA编辑器左边那条竖着的彩色gutter?

  • 绿色 = 新增行(对比git HEAD)
  • 蓝色 = 修改行
  • 红色 = 删除行

你点一下它,会弹出一个对话框,显示这一行在git里长什么样,还有一个"Rollback"按钮。

这一切的幕后英雄就是 LineStatusTracker------IDEA官方提供的一个组件,它负责:

  1. 比较当前editor document和git HEAD
  2. 计算哪些行新增了、哪些删除了、哪些修改了
  3. 提供API让你回滚这些改动
  4. 当文件变了时自动重新计算

而我花了60多个版本自己造的东西,它全都提供了。

我重新发明了什么

看看我费了多少功夫:

我苦心经营60个版本 LST一个API搞定
IdeaReviewStore::hunks --- 自己存储改动块内容 Tracker::getRanges() --- 直接问它要
EffectiveBaseline --- 复杂的基线计算(插入、删除、各种边界case) Range对象自动计算好的坐标
Tracker.getVcsDocument() --- 自己追踪文档变化 Tracker.getVcsDocument() --- LST本身就提供
split_line() writeCommandAction() 复杂的回滚逻辑 tracker.rollbackChanges(range) --- 一行代码
replaceHunksForFile() 递归替换,处理各种冲突 文档变了?LST自动重算ranges,不用我干预

#mermaid-svg-JM9q7LvlQOiRixee{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JM9q7LvlQOiRixee .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JM9q7LvlQOiRixee .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JM9q7LvlQOiRixee .error-icon{fill:#552222;}#mermaid-svg-JM9q7LvlQOiRixee .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JM9q7LvlQOiRixee .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JM9q7LvlQOiRixee .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JM9q7LvlQOiRixee .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JM9q7LvlQOiRixee .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JM9q7LvlQOiRixee .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JM9q7LvlQOiRixee .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JM9q7LvlQOiRixee .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JM9q7LvlQOiRixee .marker.cross{stroke:#333333;}#mermaid-svg-JM9q7LvlQOiRixee svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JM9q7LvlQOiRixee p{margin:0;}#mermaid-svg-JM9q7LvlQOiRixee .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JM9q7LvlQOiRixee .cluster-label text{fill:#333;}#mermaid-svg-JM9q7LvlQOiRixee .cluster-label span{color:#333;}#mermaid-svg-JM9q7LvlQOiRixee .cluster-label span p{background-color:transparent;}#mermaid-svg-JM9q7LvlQOiRixee .label text,#mermaid-svg-JM9q7LvlQOiRixee span{fill:#333;color:#333;}#mermaid-svg-JM9q7LvlQOiRixee .node rect,#mermaid-svg-JM9q7LvlQOiRixee .node circle,#mermaid-svg-JM9q7LvlQOiRixee .node ellipse,#mermaid-svg-JM9q7LvlQOiRixee .node polygon,#mermaid-svg-JM9q7LvlQOiRixee .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JM9q7LvlQOiRixee .rough-node .label text,#mermaid-svg-JM9q7LvlQOiRixee .node .label text,#mermaid-svg-JM9q7LvlQOiRixee .image-shape .label,#mermaid-svg-JM9q7LvlQOiRixee .icon-shape .label{text-anchor:middle;}#mermaid-svg-JM9q7LvlQOiRixee .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JM9q7LvlQOiRixee .rough-node .label,#mermaid-svg-JM9q7LvlQOiRixee .node .label,#mermaid-svg-JM9q7LvlQOiRixee .image-shape .label,#mermaid-svg-JM9q7LvlQOiRixee .icon-shape .label{text-align:center;}#mermaid-svg-JM9q7LvlQOiRixee .node.clickable{cursor:pointer;}#mermaid-svg-JM9q7LvlQOiRixee .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JM9q7LvlQOiRixee .arrowheadPath{fill:#333333;}#mermaid-svg-JM9q7LvlQOiRixee .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JM9q7LvlQOiRixee .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JM9q7LvlQOiRixee .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JM9q7LvlQOiRixee .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JM9q7LvlQOiRixee .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JM9q7LvlQOiRixee .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JM9q7LvlQOiRixee .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JM9q7LvlQOiRixee .cluster text{fill:#333;}#mermaid-svg-JM9q7LvlQOiRixee .cluster span{color:#333;}#mermaid-svg-JM9q7LvlQOiRixee div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JM9q7LvlQOiRixee .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JM9q7LvlQOiRixee rect.text{fill:none;stroke-width:0;}#mermaid-svg-JM9q7LvlQOiRixee .icon-shape,#mermaid-svg-JM9q7LvlQOiRixee .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JM9q7LvlQOiRixee .icon-shape p,#mermaid-svg-JM9q7LvlQOiRixee .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JM9q7LvlQOiRixee .icon-shape .label rect,#mermaid-svg-JM9q7LvlQOiRixee .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JM9q7LvlQOiRixee .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JM9q7LvlQOiRixee .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JM9q7LvlQOiRixee :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 4 个同步点,任一漂移即崩
0 同步点
新架构:单一事实源
LineStatusTracker
ReviewDecorator 纯渲染
旧架构:5 套坐标系
Git BASE
session baseline
effective baseline
store hunks
editor document
日志 red=5 屏幕没红
每次现查 getRanges

这就像有个人已经给你造好了引擎、变速箱、悬挂系统,而我偏偏在车顶上又加装了一套"自制减振"和"自制变速"。结果当然是一坨。

报告的最后一句话最扎心:

"如果要从头整,请从 LineStatusTracker 开始建模,不要从 ComparisonManager 开始。"


第三章:重生的架构

设计理念:核心原则只有一条------不自己管状态

旧版 cc-gui-review 3.0.x 用 5 套状态系统撑了 60+ 版本仍不稳定。本插件的答案:把一切交给官方 LineStatusTracker

我们不做 diff 引擎、不做 baseline 推进、不做 hunk store------只做三件事:听 LST 说 ranges 变了 → 画 UI → 用户点按钮时调 LST 的 rollback 或本地 dismiss。 这叫"零侵入":文档内容始终由 IDE 和 git 管,插件是外挂眼镜,不是第二套大脑。

设计哲学的转变:0套状态系统

旧架构的症结在于:我在git变更的基础上,又叠了5套坐标系,让它们互相"对齐"。

  • git BASE(git HEAD上的行号)
  • session baseline(某个临时基线)
  • effective baseline(算过插入删除后的"真实"基线)
  • store hunks(我自己缓存的hunk数据)
  • editor document(当前编辑器显示的内容)

任何两套数据偏差1行,就得启动"重新对齐"流程。而对齐本身又会引入新的不确定性。最后的结果就是:日志说有5个红删,屏幕上一个都看不到。

新架构彻底不同:

我们不存任何状态。LST是唯一真理。我们只负责渲染。

翻译成人话:

  • IDEA的LST已经知道"这个文件和git HEAD相比,第3-5行新增了,第10行删了"
  • 我们不再问"差异是什么",而是问"LST,你告诉我改动块在哪"
  • LST告诉我们,我们就把这些行涂成绿底,把删除线画成红底
  • 用户点KeepReject按钮时,我们问LST"回滚这个range",然后LST自动重新计算
  • 文档变了?LST会自动通知我们"ranges变了",我们不需要主动探测

这样做的好处是:没有两套数据的同步问题,因为只有一套数据。

新插件只有12个文件

下面是完整的架构(12 个 Kotlin 源文件 + plugin.xml):
#mermaid-svg-CJWdt9h4fApFx0JI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CJWdt9h4fApFx0JI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CJWdt9h4fApFx0JI .error-icon{fill:#552222;}#mermaid-svg-CJWdt9h4fApFx0JI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CJWdt9h4fApFx0JI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CJWdt9h4fApFx0JI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CJWdt9h4fApFx0JI .marker.cross{stroke:#333333;}#mermaid-svg-CJWdt9h4fApFx0JI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CJWdt9h4fApFx0JI p{margin:0;}#mermaid-svg-CJWdt9h4fApFx0JI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CJWdt9h4fApFx0JI .cluster-label text{fill:#333;}#mermaid-svg-CJWdt9h4fApFx0JI .cluster-label span{color:#333;}#mermaid-svg-CJWdt9h4fApFx0JI .cluster-label span p{background-color:transparent;}#mermaid-svg-CJWdt9h4fApFx0JI .label text,#mermaid-svg-CJWdt9h4fApFx0JI span{fill:#333;color:#333;}#mermaid-svg-CJWdt9h4fApFx0JI .node rect,#mermaid-svg-CJWdt9h4fApFx0JI .node circle,#mermaid-svg-CJWdt9h4fApFx0JI .node ellipse,#mermaid-svg-CJWdt9h4fApFx0JI .node polygon,#mermaid-svg-CJWdt9h4fApFx0JI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CJWdt9h4fApFx0JI .rough-node .label text,#mermaid-svg-CJWdt9h4fApFx0JI .node .label text,#mermaid-svg-CJWdt9h4fApFx0JI .image-shape .label,#mermaid-svg-CJWdt9h4fApFx0JI .icon-shape .label{text-anchor:middle;}#mermaid-svg-CJWdt9h4fApFx0JI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CJWdt9h4fApFx0JI .rough-node .label,#mermaid-svg-CJWdt9h4fApFx0JI .node .label,#mermaid-svg-CJWdt9h4fApFx0JI .image-shape .label,#mermaid-svg-CJWdt9h4fApFx0JI .icon-shape .label{text-align:center;}#mermaid-svg-CJWdt9h4fApFx0JI .node.clickable{cursor:pointer;}#mermaid-svg-CJWdt9h4fApFx0JI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CJWdt9h4fApFx0JI .arrowheadPath{fill:#333333;}#mermaid-svg-CJWdt9h4fApFx0JI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CJWdt9h4fApFx0JI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CJWdt9h4fApFx0JI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CJWdt9h4fApFx0JI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CJWdt9h4fApFx0JI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CJWdt9h4fApFx0JI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CJWdt9h4fApFx0JI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CJWdt9h4fApFx0JI .cluster text{fill:#333;}#mermaid-svg-CJWdt9h4fApFx0JI .cluster span{color:#333;}#mermaid-svg-CJWdt9h4fApFx0JI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CJWdt9h4fApFx0JI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CJWdt9h4fApFx0JI rect.text{fill:none;stroke-width:0;}#mermaid-svg-CJWdt9h4fApFx0JI .icon-shape,#mermaid-svg-CJWdt9h4fApFx0JI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CJWdt9h4fApFx0JI .icon-shape p,#mermaid-svg-CJWdt9h4fApFx0JI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CJWdt9h4fApFx0JI .icon-shape .label rect,#mermaid-svg-CJWdt9h4fApFx0JI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CJWdt9h4fApFx0JI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CJWdt9h4fApFx0JI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CJWdt9h4fApFx0JI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工具层
第三层:视觉实现
第二层:状态 / 逻辑
第一层:事件感知
VFS / ChangeList
绑定 LST + 重试
getRanges / rollback
ReviewEditorFactoryListener
ReviewProjectListener
ReviewEditorBinding
ReviewService
ReviewDecorator
ButtonInlayRenderer
DeletedLinesInlayRenderer
ReviewHeaderProvider / ReviewHeaderBar
ReviewButtonStyle
ReviewLog
ForceRefreshAction
LineStatusTracker 官方

总共 12 个文件。相比之下,旧架构 72 个。

代码量从几千行降到1000多行。复杂度从"事件驱动 + 异步对齐"变成了"同步查询 + 被动渲染"。


第四章:生命周期------简到不能再简

新插件的生命周期模型简得不像话:
#mermaid-svg-9HcYZfpeC2Fjv9xg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9HcYZfpeC2Fjv9xg .error-icon{fill:#552222;}#mermaid-svg-9HcYZfpeC2Fjv9xg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9HcYZfpeC2Fjv9xg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .marker.cross{stroke:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9HcYZfpeC2Fjv9xg p{margin:0;}#mermaid-svg-9HcYZfpeC2Fjv9xg defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-9HcYZfpeC2Fjv9xg g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-9HcYZfpeC2Fjv9xg g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-9HcYZfpeC2Fjv9xg g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-9HcYZfpeC2Fjv9xg g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-9HcYZfpeC2Fjv9xg .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-9HcYZfpeC2Fjv9xg .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9HcYZfpeC2Fjv9xg .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9HcYZfpeC2Fjv9xg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9HcYZfpeC2Fjv9xg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9HcYZfpeC2Fjv9xg .edgeLabel .label text{fill:#333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .label div .edgeLabel{color:#333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-9HcYZfpeC2Fjv9xg .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-9HcYZfpeC2Fjv9xg .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-9HcYZfpeC2Fjv9xg .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg #statediagram-barbEnd{fill:#333333;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .cluster-label,#mermaid-svg-9HcYZfpeC2Fjv9xg .nodeLabel{color:#131300;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .note-edge{stroke-dasharray:5;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-note text{fill:black;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram-note .nodeLabel{color:black;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagram .edgeLabel{color:red;}#mermaid-svg-9HcYZfpeC2Fjv9xg #dependencyStart,#mermaid-svg-9HcYZfpeC2Fjv9xg #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-9HcYZfpeC2Fjv9xg .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9HcYZfpeC2Fjv9xg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} VFS 外部写盘 + VCS 文件
visibleRanges 为空
INACTIVE
ACTIVE
不展示 UI

不查 LST 有没有 range

用户编辑 / Cmd+S 不复活
绿底 / 红删 / 块按钮 / 顶栏

Keep / Reject 流程

两个状态,两个转换,就这么多。

INACTIVE:保持沉默

当文件处于INACTIVE状态时:

  • 编辑器里不显示任何审查UI(没有绿底、没有红删、没有按钮、没有顶栏)
  • 不主动查 LST 有没有 range------即使磁盘上和 git 有 diff,也当作没有审查 session
  • LST有改动但我们装作看不见
  • 用户手动改代码 → 不会触发UI
  • 用户Cmd+S保存 → 不会触发UI
  • 一切都很平静

ACTIVE:正常工作

当文件处于ACTIVE状态时:

  • 展示所有改动块(绿底、红删、按钮)
  • 用户可以逐块审查、Keep或Reject
  • Keep = 隐藏这个块(加入dismissed集合)
  • Reject = 调用tracker.rollbackChanges(range),回滚到git HEAD

状态转换的触发条件

INACTIVE → ACTIVE: VFS检测到外部写盘事件(VFileContentChangeEvent),且文件在VCS管理范围内

  • AI改了这个文件
  • git checkout切换了这个文件
  • 任何外部修改

ACTIVE → INACTIVE: 文件的visibleRanges为空

  • 所有块都被Keep或Reject了
  • git rollback清空了LST的ranges
  • 文件和git HEAD完全一致

就这样,没有其他转换,没有中间状态,没有"重启后需要恢复"的问题。


第五章:用户体验的细节

块级按钮:点着舒服

每个改动块下面都有一行按钮:

复制代码
▲ 3 of 11 ▼
[Reject] [Keep]
  • ▲/▼ 导航:在改动块之间跳转,循环导航(第一个按▲跳到最后一个,最后一个按▼跳到第一个)
  • N of M:显示"第3个块,共11个",让你知道还有多少块要审查
  • Reject:红色按钮,回滚这个块
  • Keep:绿色按钮,接受这个块(隐藏)

关键设计:只有1个块时,▲▼变成灰色不可点。 这样就不会有"一直在循环导航"的尴尬。

顶栏:一览无余

编辑器顶部的通知栏:

复制代码
CC Review: 5 block(s) in this file, 2 other file(s)
   ↑↓  ←→  3 of 5 Files   [Review Next File] [Keep All] [Reject File] [Keep File]

每个元素的含义:

  • ↑↓:在当前文件的改动块之间循环(当有≥2个块时启用)
  • ←→:在有改动的文件之间导航(当有≥2个文件时启用)
  • 3 of 5 Files:显示当前文件在所有待审查文件中的位置(只读)
  • Review Next File:跳到下一个有改动的文件
  • Keep All:接受所有打开文件的改动,然后结束审查流程
  • Reject File:回滚当前文件的所有改动
  • Keep File:接受当前文件的所有改动

没有"Reject All"的原因

你可能注意到了:没有"Reject All"按钮。 这是有意的。

为什么?

假设你给AI下了一个指令,它改了10个文件。现在你想全部回滚。如果我们提供了"Reject All":

  • 一键回滚所有打开的tab中的改动
  • 但万一有一个改动其实想保留呢?
  • 万一有个文件关着、改动在LST里但没打开呢?

反而用git rollback更安全:

  1. git status看清楚所有改动的文件
  2. 确认范围无误
  3. 再用标准git命令回滚

这样就把"决策权"还给了用户,而不是让插件代替用户做出不可逆的操作。

顶栏按钮的启用规则(为什么有些按钮是灰的)

顶栏不是"有改动就全亮",每一组控件都有独立的显示/启用条件。这样设计是为了避免用户在只有一个块、只有一个文件时误点无意义的导航

控件 何时显示 何时可点 何时变灰
↑↓ 当前文件有待审查块 有 2 个以上块 0--1 个块
←→ 待审查文件 ≥ 2 有上一个/下一个 到边界(不循环
N of M Files 待审查文件 ≥ 2 只读,不可点 ---
Review Next File 始终显示 还有其他 pending 文件 没有其他 pending 文件
Keep All 始终显示 还有其他 pending 文件 没有其他 pending 文件
Reject File / Keep File 当前文件有 pending 始终可点 无 pending 时不显示顶栏

块导航循环、文件导航不循环------这是刻意的:在同一个文件里你可能想快速扫完所有块;跨文件时则不希望"最后一个文件再按一下就跳回第一个",那太容易迷路。

Keep / Reject 的完整语义

操作 范围 实际行为
Reject(块按钮) 当前块 tracker.rollbackChanges(range),文档回滚
Reject File(顶栏) 当前文件 回滚所有未 Keep 的块,已 Keep 的保留
Keep(块按钮) 当前块 隐藏 UI(dismissedRangeKeys),不改文档
Keep File(顶栏) 当前文件 当前文件所有块加入 dismissed
Keep All(顶栏) 所有已打开的 tab 每个文件的每个块 dismissed,然后 clearAllLifecycles

Keep All 只遍历 FileEditorManager.openFiles,不扫磁盘上所有改动文件。 为什么?因为"自动打开文件"已经保证 AI 改过的 VCS 文件会在后台 tab 里出现------不需要再维护一份 pendingFiles 列表,少一个状态源就少一类 bug。

Accept(Keep)在这个模型里不需要推进 baseline:改动已经在 document 里了,LST 的 BASE 仍是 git HEAD;你 Keep 只是"我看过这块,别再烦我",不是"把这块写进某个 store"。

单块 Keep 在实现上会调用 tracker.dismissRangeChanges(range) 并记入 dismissedRangeKeys;Keep/Reject 后 decorator 重建 inlay,N of M 与顶栏块/文件计数自动刷新(REQUIREMENTS §4.6)。

顶栏布局与样式(REQUIREMENTS §4.1--4.2)

从左到右:

复制代码
CC Review · N block(s), M file(s) · ↑↓ · ←→ · N of M Files · [Review Next File] [Keep All] [Reject File] [Keep File]
  灰色统计          灰色导航    浅蓝文件序              彩色圆角按钮
样式类型 控件 外观
灰色文字 统计文案、↑↓、←→ 无背景、无边框
浅蓝色 N of M Files 只读序号
彩色圆角按钮 Review Next File / Keep All / Reject File / Keep File 有背景色

#mermaid-svg-1VIQnLmHcdoFMhO2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1VIQnLmHcdoFMhO2 .error-icon{fill:#552222;}#mermaid-svg-1VIQnLmHcdoFMhO2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1VIQnLmHcdoFMhO2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .marker.cross{stroke:#333333;}#mermaid-svg-1VIQnLmHcdoFMhO2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1VIQnLmHcdoFMhO2 p{margin:0;}#mermaid-svg-1VIQnLmHcdoFMhO2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster-label text{fill:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster-label span{color:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster-label span p{background-color:transparent;}#mermaid-svg-1VIQnLmHcdoFMhO2 .label text,#mermaid-svg-1VIQnLmHcdoFMhO2 span{fill:#333;color:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .node rect,#mermaid-svg-1VIQnLmHcdoFMhO2 .node circle,#mermaid-svg-1VIQnLmHcdoFMhO2 .node ellipse,#mermaid-svg-1VIQnLmHcdoFMhO2 .node polygon,#mermaid-svg-1VIQnLmHcdoFMhO2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .rough-node .label text,#mermaid-svg-1VIQnLmHcdoFMhO2 .node .label text,#mermaid-svg-1VIQnLmHcdoFMhO2 .image-shape .label,#mermaid-svg-1VIQnLmHcdoFMhO2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1VIQnLmHcdoFMhO2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .rough-node .label,#mermaid-svg-1VIQnLmHcdoFMhO2 .node .label,#mermaid-svg-1VIQnLmHcdoFMhO2 .image-shape .label,#mermaid-svg-1VIQnLmHcdoFMhO2 .icon-shape .label{text-align:center;}#mermaid-svg-1VIQnLmHcdoFMhO2 .node.clickable{cursor:pointer;}#mermaid-svg-1VIQnLmHcdoFMhO2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .arrowheadPath{fill:#333333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1VIQnLmHcdoFMhO2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1VIQnLmHcdoFMhO2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1VIQnLmHcdoFMhO2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster text{fill:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 .cluster span{color:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1VIQnLmHcdoFMhO2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1VIQnLmHcdoFMhO2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1VIQnLmHcdoFMhO2 .icon-shape,#mermaid-svg-1VIQnLmHcdoFMhO2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1VIQnLmHcdoFMhO2 .icon-shape p,#mermaid-svg-1VIQnLmHcdoFMhO2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1VIQnLmHcdoFMhO2 .icon-shape .label rect,#mermaid-svg-1VIQnLmHcdoFMhO2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1VIQnLmHcdoFMhO2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1VIQnLmHcdoFMhO2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1VIQnLmHcdoFMhO2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Reject 块
Reject File
Keep 块
Keep File
Keep All


用户操作
Reject 还是 Keep?
rollbackChanges
自底向上 rollback 未 Keep 块
dismissRangeChanges + dismissedRangeKeys
当前文件全部 dismiss
openFiles 全部 dismiss + clearAllLifecycles
visibleRanges 空?
所有文件 INACTIVE
当前文件 INACTIVE
decorator 重建 UI


第六章:踩过的坑

坑#1:LST异步计算延迟

问题: AI改完代码,要等1-5秒才看到UI。

原因: VFS事件触发后,LST不是立刻重算ranges,而是异步任务中计算的。

解决: 多重监听 + 自动重试
ReviewDecorator LineStatusTracker ReviewService VFS / ChangeList AI 写盘 ReviewDecorator LineStatusTracker ReviewService VFS / ChangeList AI 写盘 #mermaid-svg-DU5QNtk1EMkk6lwO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DU5QNtk1EMkk6lwO .error-icon{fill:#552222;}#mermaid-svg-DU5QNtk1EMkk6lwO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DU5QNtk1EMkk6lwO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DU5QNtk1EMkk6lwO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DU5QNtk1EMkk6lwO .marker.cross{stroke:#333333;}#mermaid-svg-DU5QNtk1EMkk6lwO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DU5QNtk1EMkk6lwO p{margin:0;}#mermaid-svg-DU5QNtk1EMkk6lwO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DU5QNtk1EMkk6lwO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DU5QNtk1EMkk6lwO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DU5QNtk1EMkk6lwO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DU5QNtk1EMkk6lwO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DU5QNtk1EMkk6lwO .sequenceNumber{fill:white;}#mermaid-svg-DU5QNtk1EMkk6lwO #sequencenumber{fill:#333;}#mermaid-svg-DU5QNtk1EMkk6lwO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DU5QNtk1EMkk6lwO .messageText{fill:#333;stroke:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DU5QNtk1EMkk6lwO .labelText,#mermaid-svg-DU5QNtk1EMkk6lwO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .loopText,#mermaid-svg-DU5QNtk1EMkk6lwO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DU5QNtk1EMkk6lwO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DU5QNtk1EMkk6lwO .noteText,#mermaid-svg-DU5QNtk1EMkk6lwO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DU5QNtk1EMkk6lwO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DU5QNtk1EMkk6lwO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DU5QNtk1EMkk6lwO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DU5QNtk1EMkk6lwO .actorPopupMenu{position:absolute;}#mermaid-svg-DU5QNtk1EMkk6lwO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DU5QNtk1EMkk6lwO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DU5QNtk1EMkk6lwO .actor-man circle,#mermaid-svg-DU5QNtk1EMkk6lwO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DU5QNtk1EMkk6lwO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt ranges 非空 loop 重试 120→480→1000→2500→5- 000→8000 ms 文件内容变化 激活 ACTIVE scheduleRefresh onRangesChanged(可能延迟) getRanges() 挂载绿底 / 红删 / inlay

这样即使 LST 延迟,我们也会在它算完后立刻拿到 ranges。

坑#2:多个LST实例

问题: 同一个文件可能有多个LineStatusTracker实例(local change和shelved change各一个)。

解决: 只用第一个。这种情况极少见,而且第一个通常是正确的。

坑#3:文件关闭后残留

问题: 用户关闭了一个有改动的tab,但顶栏还显示"该文件有待审查"。

解决: ReviewEditorFactoryListener.editorReleased时,清掉内存中的状态。

坑#4:git rollback后UI不刷新

问题: 用户在terminal执行git rollback,LST ranges清空了,但编辑器里的UI还在那儿。

解决: Decorator每次重绘时,检查tracker.isValid()和ranges是否真的为空。如果为空,清理UI并自动转入INACTIVE。


第七章:技术细节

为什么监听VFS而不是其他

IDEA里改文件有很多种方式:

  • 用户直接编辑 → Document变了
  • 外部工具改文件 → VFS变了
  • git命令改文件 → VFS变了
  • IDE refactor操作 → Document变了

Document事件太频繁(每敲一个字都触发),而且是"当前编辑器"的局部事件。VFS是全局的、异步的,正好用来检测"外部"写盘。

所以我们用VFS来激活UI、用Document Listener来触发重试。

为什么不自己缓存ranges

旧架构犯的错误。自己缓存ranges,然后在各个时刻同步:

  • Document变了,ranges可能变了
  • git checkout了,ranges肯定变了
  • 用户编辑了,ranges可能变了

每个"可能"都要处理。最后的结果就是缓存和LST数据不一致。

新架构:不缓存。每次需要ranges时直接问LST。 LST的ranges肯定是最新的,因为它24小时都在监听git和document的变化。

为什么用inlay而不是gutter icon

我们用的是IntelliJ的inlay渲染,不是自己硬画:

  • inlay会跟着折叠、滚动、搜索自动移动
  • inlay会自动处理光标位置、选区等复杂情况
  • inlay是IDEA框架的一部分,兼容性天然好

相比之下自己硬画gutter icon就太累了。

线程约束:顶栏为什么要在回调里查 LST

EditorNotificationProvider.collectNotificationData后台线程 调用。如果在那里直接读 LineStatusTracker,轻则数据不一致,重则直接 crash。

所以我们的做法是:collect 阶段只返回一个 Function,真正的 tracker/range 查询放到这个 Function 被 EDT 执行时再干。 这不是过度设计,是 IDEA Platform 的硬约束------违反它,顶栏计数就会和编辑器里的块数对不上。


第七章附:需求规格全貌与覆盖清单

下面是从 REQUIREMENTS.md(v0.3.9)提炼的完整需求。如果你要 fork 或贡献代码,这是唯一来源;任何需求变更必须先改文档,再改代码。

需求十章覆盖对照(自检表)

章节 主题 博客位置 状态
生命周期(含 INACTIVE 不查 LST) 第四章 + 本节
自动打开文件(VCS 过滤、不抢焦点) 第五章附 + 第八章
内联块按钮(样式、循环导航、N of M 刷新) 第五章
顶栏(布局、样式、启禁规则、EDT 约束) 第五章 + 第七章
Reject(单块 / 文件、安全回滚) 第五章附 + 第八章
Keep(dismiss、Keep 后 INACTIVE 条件) 第五章附
绿底 / 红删 第五章 + 第八章
Keep All 仅 openFiles 第五章附
IDEA 启动/关闭/重启不恢复 session 本节
Git rollback / checkout / stash 本节 + 第六章

README 额外项:兼容性、Cursor 对比、迁移、贡献流程、已知限制、致谢 → 第十一 / 十二章及后记。

生命周期(再强调一遍)

#mermaid-svg-ojfCV23hsrHapR4c{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ojfCV23hsrHapR4c .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ojfCV23hsrHapR4c .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ojfCV23hsrHapR4c .error-icon{fill:#552222;}#mermaid-svg-ojfCV23hsrHapR4c .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ojfCV23hsrHapR4c .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ojfCV23hsrHapR4c .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ojfCV23hsrHapR4c .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ojfCV23hsrHapR4c .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ojfCV23hsrHapR4c .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ojfCV23hsrHapR4c .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ojfCV23hsrHapR4c .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ojfCV23hsrHapR4c .marker.cross{stroke:#333333;}#mermaid-svg-ojfCV23hsrHapR4c svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ojfCV23hsrHapR4c p{margin:0;}#mermaid-svg-ojfCV23hsrHapR4c defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-ojfCV23hsrHapR4c g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-ojfCV23hsrHapR4c g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-ojfCV23hsrHapR4c g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-ojfCV23hsrHapR4c g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ojfCV23hsrHapR4c g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-ojfCV23hsrHapR4c .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ojfCV23hsrHapR4c .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-ojfCV23hsrHapR4c .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-ojfCV23hsrHapR4c .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ojfCV23hsrHapR4c .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-ojfCV23hsrHapR4c .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ojfCV23hsrHapR4c .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-ojfCV23hsrHapR4c .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ojfCV23hsrHapR4c .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ojfCV23hsrHapR4c .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ojfCV23hsrHapR4c .edgeLabel .label text{fill:#333;}#mermaid-svg-ojfCV23hsrHapR4c .label div .edgeLabel{color:#333;}#mermaid-svg-ojfCV23hsrHapR4c .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-ojfCV23hsrHapR4c .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-ojfCV23hsrHapR4c .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-ojfCV23hsrHapR4c .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-ojfCV23hsrHapR4c .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-ojfCV23hsrHapR4c .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ojfCV23hsrHapR4c .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ojfCV23hsrHapR4c #statediagram-barbEnd{fill:#333333;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ojfCV23hsrHapR4c .cluster-label,#mermaid-svg-ojfCV23hsrHapR4c .nodeLabel{color:#131300;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-ojfCV23hsrHapR4c .note-edge{stroke-dasharray:5;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-note text{fill:black;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram-note .nodeLabel{color:black;}#mermaid-svg-ojfCV23hsrHapR4c .statediagram .edgeLabel{color:red;}#mermaid-svg-ojfCV23hsrHapR4c #dependencyStart,#mermaid-svg-ojfCV23hsrHapR4c #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-ojfCV23hsrHapR4c .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ojfCV23hsrHapR4c :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} VFileContentChangeEvent + VCS
visibleRanges = ∅
Keep All → clearAllLifecycles
git rollback/checkout/stash
INACTIVE
无 UI / 不查 LST
编辑与保存不复活
ACTIVE
visibleRanges 驱动 UI
IDEA 启动/开项目 → clearAllLifecycles\n重启后 Git 仍有改动也不自动 ACTIVE

场景 结果
所有块逐个 Keep/Reject 当前文件 → INACTIVE
顶栏 Keep File / Reject File 当前文件 → INACTIVE
顶栏 Keep All 所有文件 → INACTIVE
IDEA 启动 / 重启 / 打开项目 clearAllLifecycles("project-open"),不保留旧 session
IDEA 关闭 / 关闭项目 进程退出,内存状态清空
重启后文件仍在 Git 改动 自动 ACTIVE,需下次 AI 外部写盘
git rollback / checkout / stash LST ranges 清空 → decorator 检测 → INACTIVE

INACTIVE 期间 :用户手动编辑、Cmd+S 保存------一律不复活 UI。只有下一次 AI(或外部工具)写盘,才重新 ACTIVE。这样用户改自己的代码时不会被审查 UI 打扰。

自动打开文件

  • 条件:ProjectLevelVcsManager.getVcsFor(vf) != null(非 VCS 文件不打开,自然过滤 build/.gradle/ 等产物;.idea/ 等非跟踪目录同理)
  • 方式:openFile(vf, false),后台 tab,不抢焦点

绿底 / 红删

元素 触发条件 样式
绿底 LST range 有新行(line2 > line1 #E6F4EA,整行背景
红删 LST range 有 VCS 删行(vcsLine2 > vcsLine1 Block inlay 锚点上方,#FFE8EE~~ 前缀

安全回滚(Reject File)

  1. 每回滚一块后,重新从 tracker 取最新 ranges(避免 stale Range)
  2. 从底向上 回滚(maxByOrNull { it.line1 }
  3. 200 次上限防死循环
  4. 全程 writeCommandAction,支持 Ctrl+Z

与旧版 cc-gui-review 的对比(README 摘要)

旧架构 cc-review-lst
5 套状态(Git BASE / session / effective / store / document) LST 唯一真相
HunkZoneMapper 干预 LST 不存在
LiveDiffModel 每次重算 LST 内部维护
FileWriteGate 60ms 写稳 不需要
72 个 Kotlin 文件 12 个文件

Keep 后状态(REQUIREMENTS §6.1)

操作 退出条件
Keep 单块 visibleRanges 为空 → 当前文件 INACTIVE
Keep File 当前文件 decorator.visibleRanges 为空 → INACTIVE
Keep All 直接 clearAllLifecycles → 所有文件 INACTIVE

Git 操作(REQUIREMENTS §十)

Git 命令 插件行为
git rollback 对应文件 LST ranges 变空 → decorator 清理 → INACTIVE
git checkout 同上
git stash 同上

第八章:那些神奇的细节

自动打开文件

AI改了一个Java文件,但你在IDEA里没有打开它。插件会自动在后台tab打开这个文件,这样你就能看到改动了。

这里有个细节:不抢焦点。openFile(vf, false)而不是openFile(vf, true)。这样IDEA不会切到这个文件,而是默默在tab里打开。

为什么?想象一下如果焦点被抢走的体验有多糟糕。

绿底 + 红删

  • 绿底#E6F4EA,浅绿。背景色应用到整行(HighlighterTargetArea.LINES_IN_RANGE)
  • 红删 :Block inlay放在改动块的上方(showAbove=true)。每一行前面加个"~~"前缀,背景色#FFE8EE(浅红)

红删为什么要单独做inlay而不是直接画到绿底上?因为删除的行在当前document里根本不存在,坐标无处可放。只能用inlay在某个位置上方虚拟插入一块。

块按钮的布局

块按钮要显示在"改动块"的下方。但有个问题:DELETED range是什么意思?

DELETED range: 用户删了一块代码,line1 == line2(没有当前行),但vcsLine1 != vcsLine2(git里有删除的行)。

这时块按钮的锚点应该在哪?答案是:range的line1,也就是删除发生的那一行。

kotlin 复制代码
val buttonAnchorLine = if (range.line2 == range.line1) {
    // DELETED case: 锚点用line1
    range.line1.coerceIn(0, (lineCount - 1).coerceAtLeast(0))
} else {
    // ADDED/MODIFIED case: 锚点用最后一行(range.line2 - 1)
    (range.line2 - 1).coerceIn(0, (lineCount - 1).coerceAtLeast(0))
}

安全回滚

当用户点Reject File时,我们要回滚该文件的所有改动块。但有个陷阱:边回滚边改ranges。

当你回滚第一个块时,document变了,LST重新计算ranges,第二个块的坐标就变了。

所以我们的做法是:

  1. 每次回滚一个块后,重新从LST获取最新的ranges
  2. 从底向上回滚(maxByOrNull { it.line1 }),这样不会因为坐标变化影响前面的块
  3. 设一个200次的死循环防护
  4. 所有操作都在writeCommandAction中,支持Ctrl+Z撤销

第九章:这样设计,如何避免 Bug

旧版 60+ 个版本仍不稳定,不是因为测试不够,而是测错了层 ------矩阵跑的是自研 diff 逻辑,故障却在 EDT 重绘、inlay 挂载、多线程对齐。新架构换的不是"更仔细的 if-else",而是把故障域从自研状态机挪到了 JetBrains 已经测过几亿次的 LST 上

旧问题 → 新解法对照表

旧架构的问题 本插件如何避免
5 套状态互相漂移 不存在------LST 就是唯一真相
Git rollback 后顶栏残留 LST ranges 清空,decorator 检测到 → 自然 INACTIVE
重启 IDEA 后假 pending 启动时 clearAllLifecycles,不恢复旧 session
hunk not found(zone vs store) 不存在------只有 LST Range,内部用 RangeMarker 自动迁移
日志 red=5 屏幕没红 每次渲染从 LST 现查,不缓存展示用中间结果
同 zip 复测结果不同 去掉"事件历史决定状态",改为"LST 现状决定 UI"
Accept 后绿线闪回 去掉多入口写 store,Keep 只是内存 dismissed 集合

五条架构级防 Bug 原则

如果你要 fork 或加功能,请把下面五条当作不可违反的 invariant
#mermaid-svg-Cq1FJNw5Uthlt5RN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Cq1FJNw5Uthlt5RN .error-icon{fill:#552222;}#mermaid-svg-Cq1FJNw5Uthlt5RN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Cq1FJNw5Uthlt5RN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Cq1FJNw5Uthlt5RN .marker.cross{stroke:#333333;}#mermaid-svg-Cq1FJNw5Uthlt5RN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Cq1FJNw5Uthlt5RN p{margin:0;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge{stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 text{fill:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth--1{stroke-width:17;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-0{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-0{stroke-width:14;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-1{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-1{stroke-width:11;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 text{fill:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-2{stroke-width:8;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-3{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-3{stroke-width:5;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-4{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-4{stroke-width:2;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-5{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-5{stroke-width:-1;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-6{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-6{stroke-width:-4;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-7{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-7{stroke-width:-7;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-8{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-8{stroke-width:-10;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-9{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-9{stroke-width:-13;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 polygon,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 text{fill:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .node-icon-10{font-size:40px;color:black;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge-depth-10{stroke-width:-16;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:lightgray;}#mermaid-svg-Cq1FJNw5Uthlt5RN .disabled text{fill:#efefef;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root rect,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root path,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root circle,#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root text{fill:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-root span{color:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .section-2 span{color:#ffffff;}#mermaid-svg-Cq1FJNw5Uthlt5RN .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-Cq1FJNw5Uthlt5RN .edge{fill:none;}#mermaid-svg-Cq1FJNw5Uthlt5RN .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-Cq1FJNw5Uthlt5RN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 防 Bug 原则
单一事实源
只问 LST
禁止第二套 store
不缓存 ranges
现查 getRanges
Keep 纯 UI
不推进 baseline
激活只看 VFS
编辑保存不复活
危险操作交 git
无 Reject All

  1. 单一事实源:diff 状态只问 LST,禁止再引入第二套 hunk/store/baseline。
  2. 不缓存 ranges :需要时用 tracker.getRanges() 现取;LST 自己会监听 document 和 git。
  3. Keep 是纯 UI:Accept 不推进 baseline、不写 store;文档里已经有改动就是 accept。
  4. 激活只看 VFS 外部写盘:用户编辑、保存不触发 ACTIVE,避免 UI 和用户意图打架。
  5. 危险操作交给 git :不提供 Reject All;批量回滚用 git rollback,决策权在用户手里。

开发流程上的防 Bug

README 里写得很清楚:需求变更必须先更新 REQUIREMENTS.md,再改代码。 不接受"先改代码再补文档"。这对一个小插件尤其重要------12 个文件能 hold 住,是因为边界被文档钉死了;一旦 silent creep,就会重演 72 文件的老路。

测试上,如果以后要加自动化,应测 EDT 上的真实 Markup/Inlay 数量 ,而不是自研 model 的 zone.size。red=5 屏幕没红 这类 bug,纯逻辑矩阵永远抓不住。


第十章:好处与价值

对开发者

  • Cursor 风格的所见即所审:不用开 Diff 窗、不用点 gutter 小箭头找浮窗;绿底、红删、块按钮就在编辑视图里。
  • 块级 Reject 一键回滚tracker.rollbackChanges(range),比手动删改安全,且支持 Undo。
  • 块级 Keep 一键确认:看过这块了,点 Keep 收起;文档不动,下次 commit 自然进去。
  • 多文件顶栏Review Next File、文件计数、Keep All------AI 一次改十个文件时不用自己记 tab。
  • 零配置 :装好插件、让 AI 改 git 管理的文件,等 1--5 秒 UI 出现;Ctrl+Alt+Shift+R 强制刷新。

对团队与项目

  • 与 AI 工具解耦:监听 VFS 写盘,Claude Code、Copilot、终端里的任何改文件工具都能触发------不绑死某一个 AI。
  • 可维护性 :12 文件、~1500 行,新人读一遍 ReviewService + ReviewDecorator 就能上手;旧版 72 文件几乎无法交接。
  • 稳定性数量级差异:从"同 zip 不同打开顺序不同结果"到"LST 确定性输出";技术债从 5 源 4 同步点降到 1 源 0 同步点。
  • 开源友好:Apache 2.0 / MIT 待定;贡献流程、需求文档、版本历史齐全,适合作为 IDEA 插件 + LST 的参考实现。

我们到底在补 IDEA 的哪一块

分析报告里有一句很准的话:

IDEA 官方给了块级决策的能力 (Diff 窗箭头、gutter Rollback),但没给块级决策的内联视觉

Cursor 把 RejectKeep 绣在行边;IDEA 把按钮放在 Diff 窗。功能近似,视觉位置差就是 cc-review-lst 存在的理由------用官方 LST 当引擎,只自研薄薄一层渲染。


第十一章:已知限制、兼容性与 Cursor 对比

诚实比 demo 重要。以下限制来自 README / REQUIREMENTS,不是隐藏债。

已知限制

限制 说明
不区分"真插入行" LST 的 MODIFY range 整块标绿,不像 Cursor 只高亮真正新写的那几行
仅支持 Git 依赖 Git4Idea;其他 VCS 需另测
仅 IDEA 2026.1.x sinceBuild=261,升级 IDE 需重新验证
LST 异步延迟 AI 改完可能要 1--5 秒才出 UI(见第六章坑 #1)
Keep All 范围 只作用于已打开的 tab,不是仓库里所有改动文件

环境兼容性

项目 版本
IntelliJ IDEA 2026.1.*(261--263.\*
JDK 21.*
Kotlin 1.2.3.x
Gradle 8.10.2
IntelliJ Platform Gradle Plugin 2.2.1
依赖 Git4Idea(IDEA 内置)

与 Cursor 的能力对比

能力 Cursor CC Review (LST)
Keep / Reject 原生
块间导航 原生
多文件 Review Next 原生
实时流式生成 UI 模型驱动 依赖 LST 重算(有延迟)
只高亮真插入行 ❌ 整块 MODIFY 标绿
块状展示位置 行内 Block inlay(块下方独立行)
顶栏文件统计 ✔ 文件计数 + 块计数

结论:我们在 IDEA 里逼近 Cursor 的审查体验,但在" MODIFY 粒度"和"流式零延迟"上仍受 LST 语义约束------这是平台边界,不是再堆 60 个版本能解决的。

从 cc-gui-review 3.0.x 迁移

  1. 卸载 cc-gui-review
  2. 重启 IDE
  3. 安装 cc-review-lst 的 zip(./gradlew buildPlugin 产出)
  4. 再重启 IDE
  5. 无需迁移数据------本插件不读任何旧版状态

第十二章:前景与演进方向

LST 架构把项目从"能不能用"拉到了"日常可用",但分析报告也点出了仍未消灭的硬问题------值得写进前景,避免下一个维护者误以为"已经完美"。

短期(已在路上)

  • 完善文档与需求锁定(REQUIREMENTS.md 为唯一来源)
  • 在 IDEA 2026.1.x 上持续验证 Git 场景:rollback、checkout、stash、多文件 Keep All
  • 开源许可证定稿(建议 Apache 2.0 或 MIT)

中期可能的方向

方向 价值 难度
PartialLocalLineStatusTracker / 自定义 base AI 改之前用户已有本地改动时,以"AI 改前快照"为 base,而不是 git HEAD 中------唯一可能需要自研状态的点
MODIFY 行级高亮 更接近 Cursor,只绿真新增行 高------LST Range 不提供行级 diff,可能要接 ComparisonManager 或自算 intra-hunk diff
更多 VCS SVN、Mercurial 用户 中------需验证 Git4Idea 以外路径
IDEA 新版本跟进 261 → 262/263 平台 API 变更 低但必做

长期判断

  • 如果团队可以迁到 Cursor:审查 UI 原生自带,本插件价值下降------但 Java/Kotlin 重度 IDEA 用户仍会留下。
  • 如果必须留在 IDEA :本插件填的是"内联视觉"缝;再往上堆功能(流式门控、Reject All、session 持久化)会重新引入状态机------每加一项,先问能不能只用 LST + 纯 UI 状态完成
  • 给后来者的笔记 :若从头 fork,预计 2--3 周可出稳定 demo;核心代码控制在 1500 行内。请从 LineStatusTracker 建模,不要从 ComparisonManager 或自研 store 起步。

还剩下的"真硬"问题(诚实清单)

  1. AI 写盘 base 语义 :LST 默认对齐 VCS HEAD,不是"AI 动手前那一秒"的快照------PreToolUse snapshot 若要完美,需要 shadow tracker 或 setBaseRevision
  2. AI _burst 写盘时 LST 闪烁:LST 为人手编辑设计,一秒 50 行时 gutter 也会闪------这是 IDEA 自己的行为,社区接受度高于自研 gate,但不等于零干扰。
  3. Reject All :技术上可 rollbackAllChanges() 遍历打开文件,产品上不提供------安全优先。

第十三章:从放弃到拾起的启示

为什么最初会失败

我一开始就问错了问题。我问的是:

"怎样自己管理diff状态,这样我就能完全控制UI的展示?"

结果就是:造了5套互相冲突的状态系统。

正确的问题应该是:

"IDEA的官方组件能提供什么能力?我应该如何利用而不是绕过这些能力?"

这个故事的道德

不要重新发明轮子。

特别是当这个轮子已经被一个比你聪明得多的团队打磨了10年、被数百万开发者使用过的时候。

LineStatusTracker不是一个小功能,它是IDEA版本控制系统的核心引擎。它处理了所有边界case、异步问题、缓存策略。如果我不用它,就得自己处理所有这些问题。而事实上,我差点就这么做了。

技术债的本质

技术债不是"代码写得烂"。技术债的本质是:错误的抽象选择。
#mermaid-svg-7zWWvNmG0Kz2Hk1x{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .error-icon{fill:#552222;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .marker.cross{stroke:#333333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x p{margin:0;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster-label text{fill:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster-label span{color:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster-label span p{background-color:transparent;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .label text,#mermaid-svg-7zWWvNmG0Kz2Hk1x span{fill:#333;color:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .node rect,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node circle,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node ellipse,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node polygon,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .rough-node .label text,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node .label text,#mermaid-svg-7zWWvNmG0Kz2Hk1x .image-shape .label,#mermaid-svg-7zWWvNmG0Kz2Hk1x .icon-shape .label{text-anchor:middle;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .rough-node .label,#mermaid-svg-7zWWvNmG0Kz2Hk1x .node .label,#mermaid-svg-7zWWvNmG0Kz2Hk1x .image-shape .label,#mermaid-svg-7zWWvNmG0Kz2Hk1x .icon-shape .label{text-align:center;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .node.clickable{cursor:pointer;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .arrowheadPath{fill:#333333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7zWWvNmG0Kz2Hk1x .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zWWvNmG0Kz2Hk1x .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster text{fill:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .cluster span{color:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7zWWvNmG0Kz2Hk1x rect.text{fill:none;stroke-width:0;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .icon-shape,#mermaid-svg-7zWWvNmG0Kz2Hk1x .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .icon-shape p,#mermaid-svg-7zWWvNmG0Kz2Hk1x .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .icon-shape .label rect,#mermaid-svg-7zWWvNmG0Kz2Hk1x .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zWWvNmG0Kz2Hk1x .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7zWWvNmG0Kz2Hk1x .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7zWWvNmG0Kz2Hk1x :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 新系统
LST ranges
ReviewDecorator
零同步点
旧系统
Git BASE
同步点 1
session
同步点 2
effective
同步点 3
store
同步点 4
document
任一漂移即崩

一个系统的复杂度来自于有多少个"源头"需要同步。我的旧系统有5个源头(5套坐标系),所以有4个同步点。每个同步点都是一个崩溃的机会。

新系统只有1个源头(LST ranges),所以零同步点。稳定性从"经常崩"变成了"几乎不崩"。

代码行数从5000+降到1500。不是因为我变聪明了,而是因为我问的问题变对了。


第十四章:最后

从6月2日的放弃到6月5日的重生,只用了3天。

如果我没有放弃,可能还会继续在旧架构的泥潭里打转,写出72个文件、60多个版本、每个版本都有新bug。

但我放弃了。正是这个放弃,给了我重新思考的机会。

然后一份分析报告指向了LineStatusTracker。然后一切就豁然开朗了。

这个插件现在很小,很稳定。不是因为我代码写得好,而是因为我站在了巨人的肩膀上。

所以,如果你现在也在某个项目里挣扎,也许你该做的就是:停下来,放弃眼前的方案,好好想想------还有没有更聪明的方式?

答案就在那儿,等着被发现。


后记:现在怎么用

构建与安装

bash 复制代码
git clone https://github.com/quyixiao/cc-idea-code-review.git
cd cc-idea-code-review/cc-review-lst
./gradlew buildPlugin

然后在 IDEA:Settings → Plugins → Install from Disk ,选 build/distributions/ 下的 zip,重启 IDE。

日常使用

  1. 确保项目是 Git 管理 ,IDEA 版本 2026.1.x
  2. 让任意 AI 或外部工具改一个已跟踪的文件(Claude Code、Copilot、终端编辑均可)
  3. 文件会在后台 tab 自动打开(不抢焦点);若已打开,直接看当前编辑器
  4. 1--5 秒(LST 异步计算);应看到:绿底 + 红删 + 块按钮 + 顶栏
  5. 逐块或逐文件 Keep / Reject;全部处理完 → 自动 INACTIVE,UI 消失
  6. 若 UI 未出现:Ctrl+Alt+Shift+R 强制刷新(ForceRefreshAction

相关文档

文档 用途
README.md 产品概览、快速开始
REQUIREMENTS.md 需求唯一来源
VERSION_HISTORY.md 版本历史
分析报告.md 旧架构诊断与 LST 方案

仓库:https://github.com/quyixiao/cc-idea-code-review

贡献与需求变更流程

#mermaid-svg-Sxv260UyBS0PSmS8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sxv260UyBS0PSmS8 .error-icon{fill:#552222;}#mermaid-svg-Sxv260UyBS0PSmS8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sxv260UyBS0PSmS8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sxv260UyBS0PSmS8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sxv260UyBS0PSmS8 .marker.cross{stroke:#333333;}#mermaid-svg-Sxv260UyBS0PSmS8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sxv260UyBS0PSmS8 p{margin:0;}#mermaid-svg-Sxv260UyBS0PSmS8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster-label text{fill:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster-label span{color:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Sxv260UyBS0PSmS8 .label text,#mermaid-svg-Sxv260UyBS0PSmS8 span{fill:#333;color:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 .node rect,#mermaid-svg-Sxv260UyBS0PSmS8 .node circle,#mermaid-svg-Sxv260UyBS0PSmS8 .node ellipse,#mermaid-svg-Sxv260UyBS0PSmS8 .node polygon,#mermaid-svg-Sxv260UyBS0PSmS8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sxv260UyBS0PSmS8 .rough-node .label text,#mermaid-svg-Sxv260UyBS0PSmS8 .node .label text,#mermaid-svg-Sxv260UyBS0PSmS8 .image-shape .label,#mermaid-svg-Sxv260UyBS0PSmS8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sxv260UyBS0PSmS8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sxv260UyBS0PSmS8 .rough-node .label,#mermaid-svg-Sxv260UyBS0PSmS8 .node .label,#mermaid-svg-Sxv260UyBS0PSmS8 .image-shape .label,#mermaid-svg-Sxv260UyBS0PSmS8 .icon-shape .label{text-align:center;}#mermaid-svg-Sxv260UyBS0PSmS8 .node.clickable{cursor:pointer;}#mermaid-svg-Sxv260UyBS0PSmS8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sxv260UyBS0PSmS8 .arrowheadPath{fill:#333333;}#mermaid-svg-Sxv260UyBS0PSmS8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sxv260UyBS0PSmS8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sxv260UyBS0PSmS8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sxv260UyBS0PSmS8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sxv260UyBS0PSmS8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sxv260UyBS0PSmS8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster text{fill:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 .cluster span{color:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sxv260UyBS0PSmS8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sxv260UyBS0PSmS8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sxv260UyBS0PSmS8 .icon-shape,#mermaid-svg-Sxv260UyBS0PSmS8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sxv260UyBS0PSmS8 .icon-shape p,#mermaid-svg-Sxv260UyBS0PSmS8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sxv260UyBS0PSmS8 .icon-shape .label rect,#mermaid-svg-Sxv260UyBS0PSmS8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sxv260UyBS0PSmS8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sxv260UyBS0PSmS8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sxv260UyBS0PSmS8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提出需求
讨论确认
更新 REQUIREMENTS.md
改代码
gradle build
提交 + tag + PR

  1. Fork 本仓库 → git checkout -b feature/xxx
  2. 先读 REQUIREMENTS.md(唯一来源)
  3. 改代码,确保 ./gradlew build 通过
  4. 写清楚 commit message,升级版本号
  5. Pull Request

不接受「先改代码再补文档」。许可证待定(建议 Apache 2.0 或 MIT)。

致谢

架构灵感来自 分析报告.md:旧版根本问题不是「插件写得不好」,而是重新发明了 LineStatusTracker------从 LST 出发,12 个文件替代 72 个。


希望这个故事能给你一些启发。也许你正在经历类似的困境,也许你正考虑某个架构决策。记住两句话:

"如果要从头整,请从 LineStatusTracker 开始,不要从 ComparisonManager 开始。"
"如果要从头整,请从基础设施开始,不要从你自己的想象开始。"

相关推荐
jeffer_liu1 小时前
Spring AI 生产级实战-结构化输出
java·人工智能·后端·spring·大模型
疏狂难除1 小时前
JetBrains IDE插件开发教程(四)——Action
java·ide·kotlin
laufing1 小时前
java web 基础 ---- servlet
java·servlet·web开发
程序猿乐锅1 小时前
【苍穹外卖|Day01】项目初识:从多模块结构到 OpenAPI 接口文档踩坑
java·spring·maven·mybatis
AiTop1001 小时前
PaddleOCR-VL-1.6正式开源:0.9B轻量架构跑出96.33%准确率,反超GPT、Gemini登顶全球OCR榜单
gpt·架构·开源
invicinble1 小时前
我们对整个IT架构的全视野全场景有个理解(全景理解)
架构
lauo1 小时前
ibbot手机:一部手机,双重革命
人工智能·智能手机·架构·开源·github
李白的天不白1 小时前
针对你遇到的 Client.Timeout exceeded 问题,我判断是防火墙拦截了 HTTPS 流量
java
冷色调的咖啡师1 小时前
1.大数据架构技术 上——搭建分布式Hadoop集群
大数据·linux·hadoop·分布式·hdfs·架构·yarn