目录
- 序言:放弃那一刻
- 产品是什么(README 一句话)
- 第一章:反思之前,先看看现在发生了什么
- 第二章:那份改变人生的分析报告
- 第三章:重生的架构
- 第四章:生命周期——简到不能再简
- 第五章:用户体验的细节
- 第六章:踩过的坑
- 第七章:技术细节
- 第七章附:需求规格全貌与覆盖清单
- 第八章:那些神奇的细节
- 第九章:这样设计,如何避免 Bug
- 第十章:好处与价值
- 第十一章:已知限制、兼容性与 Cursor 对比
- 第十二章:前景与演进方向
- 第十三章:从放弃到拾起的启示
- 第十四章:最后
- 后记:现在怎么用
序言:放弃那一刻
2026年6月2日下午3点,我关掉了电脑。
72个Kotlin文件。60多个版本。将近两个月的时间。所有这一切,只是为了在IntelliJ IDEA里给AI改的代码加一层"审查UI"——绿底高亮改动行、红色标记删除线、两个按钮[Reject]和[Keep]让用户一键接受或回滚。
看起来很简单。做起来地狱。
最绝望的不是持续失败,而是成功和失败在24小时内无缝切换。上午用户说"非常好",下午就bug重现。同一份zip包,不同的打开顺序,完全不同的结果。这不是代码质量问题,这是架构的本质缺陷在对我呐喊。
那一刻我想:算了,不弄了。以后提起这条路,能躲多远躲多远。
但三天后,一份分析报告彻底改变了我对这个项目的理解。
产品是什么(README 一句话)
Cursor 风格的 IntelliJ IDEA 内嵌代码审查插件——基于 LineStatusTracker,零侵入。
AI(Claude Code / CC GUI / Copilot / 终端改文件)写完代码后,你在编辑器里直接看到绿底新增、红删标移除,每个改动块旁 [Reject] [Keep] 一键决策,顶栏负责多文件导航与批量 Keep。不打开 Diff 窗,也能完成块级审查。
先看看目前最终成品的效果
已经和cursor 的效果一样,而且没有任何性能问题,目前支持idea版本是26.1

第一章:反思之前,先看看现在发生了什么
所有AI工具改的代码,都能用这个插件
这个插件的初衷是为Claude Code的IDEA集成(CC GUI)设计的。但重新思考后我意识到一个关键事实:
插件监听的不是"谁在改代码",而是"磁盘上的文件变了"。
这意味着什么?
- Claude Code在终端敲命令,改了一个Java文件 → 磁盘变了 → VFS事件触发 → 插件激活 → 你在IDEA里看到绿底和审查按钮
- GitHub Copilot在编辑器补全代码 → 磁盘变了 → 同样的流程
- 甚至你自己用Vim在terminal改文件 → 也能触发这套流程
只要是VFS(Virtual File System)检测到内容变化,我们就知道有改动了。不管改动来自哪个AI、哪个终端、哪次操作,它都会被捕捉。
#mermaid-svg-69A58NIpq7LIjiuX{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-69A58NIpq7LIjiuX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-69A58NIpq7LIjiuX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-69A58NIpq7LIjiuX .error-icon{fill:#552222;}#mermaid-svg-69A58NIpq7LIjiuX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-69A58NIpq7LIjiuX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-69A58NIpq7LIjiuX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-69A58NIpq7LIjiuX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-69A58NIpq7LIjiuX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-69A58NIpq7LIjiuX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-69A58NIpq7LIjiuX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-69A58NIpq7LIjiuX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-69A58NIpq7LIjiuX .marker.cross{stroke:#333333;}#mermaid-svg-69A58NIpq7LIjiuX svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-69A58NIpq7LIjiuX p{margin:0;}#mermaid-svg-69A58NIpq7LIjiuX .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-69A58NIpq7LIjiuX .cluster-label text{fill:#333;}#mermaid-svg-69A58NIpq7LIjiuX .cluster-label span{color:#333;}#mermaid-svg-69A58NIpq7LIjiuX .cluster-label span p{background-color:transparent;}#mermaid-svg-69A58NIpq7LIjiuX .label text,#mermaid-svg-69A58NIpq7LIjiuX span{fill:#333;color:#333;}#mermaid-svg-69A58NIpq7LIjiuX .node rect,#mermaid-svg-69A58NIpq7LIjiuX .node circle,#mermaid-svg-69A58NIpq7LIjiuX .node ellipse,#mermaid-svg-69A58NIpq7LIjiuX .node polygon,#mermaid-svg-69A58NIpq7LIjiuX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-69A58NIpq7LIjiuX .rough-node .label text,#mermaid-svg-69A58NIpq7LIjiuX .node .label text,#mermaid-svg-69A58NIpq7LIjiuX .image-shape .label,#mermaid-svg-69A58NIpq7LIjiuX .icon-shape .label{text-anchor:middle;}#mermaid-svg-69A58NIpq7LIjiuX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-69A58NIpq7LIjiuX .rough-node .label,#mermaid-svg-69A58NIpq7LIjiuX .node .label,#mermaid-svg-69A58NIpq7LIjiuX .image-shape .label,#mermaid-svg-69A58NIpq7LIjiuX .icon-shape .label{text-align:center;}#mermaid-svg-69A58NIpq7LIjiuX .node.clickable{cursor:pointer;}#mermaid-svg-69A58NIpq7LIjiuX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-69A58NIpq7LIjiuX .arrowheadPath{fill:#333333;}#mermaid-svg-69A58NIpq7LIjiuX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-69A58NIpq7LIjiuX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-69A58NIpq7LIjiuX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-69A58NIpq7LIjiuX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-69A58NIpq7LIjiuX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-69A58NIpq7LIjiuX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-69A58NIpq7LIjiuX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-69A58NIpq7LIjiuX .cluster text{fill:#333;}#mermaid-svg-69A58NIpq7LIjiuX .cluster span{color:#333;}#mermaid-svg-69A58NIpq7LIjiuX 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-69A58NIpq7LIjiuX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-69A58NIpq7LIjiuX rect.text{fill:none;stroke-width:0;}#mermaid-svg-69A58NIpq7LIjiuX .icon-shape,#mermaid-svg-69A58NIpq7LIjiuX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-69A58NIpq7LIjiuX .icon-shape p,#mermaid-svg-69A58NIpq7LIjiuX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-69A58NIpq7LIjiuX .icon-shape .label rect,#mermaid-svg-69A58NIpq7LIjiuX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-69A58NIpq7LIjiuX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-69A58NIpq7LIjiuX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-69A58NIpq7LIjiuX :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官方提供的一个组件,它负责:
而我花了60多个版本自己造的东西,它全都提供了。
我重新发明了什么
看看我费了多少功夫:
| IdeaReviewStore::hunks — 自己存储改动块内容 | Tracker::getRanges() — 直接问它要 |
| EffectiveBaseline — 复杂的基线计算(插入、删除、各种边界case) | Range对象自动计算好的坐标 |
| Tracker.getVcsDocument() — 自己追踪文档变化 | Tracker.getVcsDocument() — LST本身就提供 |
| split_line() writeCommandAction() 复杂的回滚逻辑 | tracker.rollbackChanges(range) — 一行代码 |
| replaceHunksForFile() 递归替换,处理各种冲突 | 文档变了?LST自动重算ranges,不用我干预 |
#mermaid-svg-i439WW2U9DnEdSiZ{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-i439WW2U9DnEdSiZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i439WW2U9DnEdSiZ .error-icon{fill:#552222;}#mermaid-svg-i439WW2U9DnEdSiZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i439WW2U9DnEdSiZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i439WW2U9DnEdSiZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i439WW2U9DnEdSiZ .marker.cross{stroke:#333333;}#mermaid-svg-i439WW2U9DnEdSiZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i439WW2U9DnEdSiZ p{margin:0;}#mermaid-svg-i439WW2U9DnEdSiZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-i439WW2U9DnEdSiZ .cluster-label text{fill:#333;}#mermaid-svg-i439WW2U9DnEdSiZ .cluster-label span{color:#333;}#mermaid-svg-i439WW2U9DnEdSiZ .cluster-label span p{background-color:transparent;}#mermaid-svg-i439WW2U9DnEdSiZ .label text,#mermaid-svg-i439WW2U9DnEdSiZ span{fill:#333;color:#333;}#mermaid-svg-i439WW2U9DnEdSiZ .node rect,#mermaid-svg-i439WW2U9DnEdSiZ .node circle,#mermaid-svg-i439WW2U9DnEdSiZ .node ellipse,#mermaid-svg-i439WW2U9DnEdSiZ .node polygon,#mermaid-svg-i439WW2U9DnEdSiZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i439WW2U9DnEdSiZ .rough-node .label text,#mermaid-svg-i439WW2U9DnEdSiZ .node .label text,#mermaid-svg-i439WW2U9DnEdSiZ .image-shape .label,#mermaid-svg-i439WW2U9DnEdSiZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-i439WW2U9DnEdSiZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i439WW2U9DnEdSiZ .rough-node .label,#mermaid-svg-i439WW2U9DnEdSiZ .node .label,#mermaid-svg-i439WW2U9DnEdSiZ .image-shape .label,#mermaid-svg-i439WW2U9DnEdSiZ .icon-shape .label{text-align:center;}#mermaid-svg-i439WW2U9DnEdSiZ .node.clickable{cursor:pointer;}#mermaid-svg-i439WW2U9DnEdSiZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i439WW2U9DnEdSiZ .arrowheadPath{fill:#333333;}#mermaid-svg-i439WW2U9DnEdSiZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i439WW2U9DnEdSiZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i439WW2U9DnEdSiZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i439WW2U9DnEdSiZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i439WW2U9DnEdSiZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i439WW2U9DnEdSiZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i439WW2U9DnEdSiZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i439WW2U9DnEdSiZ .cluster text{fill:#333;}#mermaid-svg-i439WW2U9DnEdSiZ .cluster span{color:#333;}#mermaid-svg-i439WW2U9DnEdSiZ 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-i439WW2U9DnEdSiZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i439WW2U9DnEdSiZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-i439WW2U9DnEdSiZ .icon-shape,#mermaid-svg-i439WW2U9DnEdSiZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i439WW2U9DnEdSiZ .icon-shape p,#mermaid-svg-i439WW2U9DnEdSiZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i439WW2U9DnEdSiZ .icon-shape .label rect,#mermaid-svg-i439WW2U9DnEdSiZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i439WW2U9DnEdSiZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i439WW2U9DnEdSiZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i439WW2U9DnEdSiZ :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告诉我们,我们就把这些行涂成绿底,把删除线画成红底
- 用户点[Keep]或[Reject]按钮时,我们问LST"回滚这个range",然后LST自动重新计算
- 文档变了?LST会自动通知我们"ranges变了",我们不需要主动探测
这样做的好处是:没有两套数据的同步问题,因为只有一套数据。
新插件只有12个文件
下面是完整的架构(12 个 Kotlin 源文件 + plugin.xml):
#mermaid-svg-uTBiTlWrk7j4vE0f{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-uTBiTlWrk7j4vE0f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uTBiTlWrk7j4vE0f .error-icon{fill:#552222;}#mermaid-svg-uTBiTlWrk7j4vE0f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uTBiTlWrk7j4vE0f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uTBiTlWrk7j4vE0f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uTBiTlWrk7j4vE0f .marker.cross{stroke:#333333;}#mermaid-svg-uTBiTlWrk7j4vE0f svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uTBiTlWrk7j4vE0f p{margin:0;}#mermaid-svg-uTBiTlWrk7j4vE0f .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster-label text{fill:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster-label span{color:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster-label span p{background-color:transparent;}#mermaid-svg-uTBiTlWrk7j4vE0f .label text,#mermaid-svg-uTBiTlWrk7j4vE0f span{fill:#333;color:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f .node rect,#mermaid-svg-uTBiTlWrk7j4vE0f .node circle,#mermaid-svg-uTBiTlWrk7j4vE0f .node ellipse,#mermaid-svg-uTBiTlWrk7j4vE0f .node polygon,#mermaid-svg-uTBiTlWrk7j4vE0f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uTBiTlWrk7j4vE0f .rough-node .label text,#mermaid-svg-uTBiTlWrk7j4vE0f .node .label text,#mermaid-svg-uTBiTlWrk7j4vE0f .image-shape .label,#mermaid-svg-uTBiTlWrk7j4vE0f .icon-shape .label{text-anchor:middle;}#mermaid-svg-uTBiTlWrk7j4vE0f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uTBiTlWrk7j4vE0f .rough-node .label,#mermaid-svg-uTBiTlWrk7j4vE0f .node .label,#mermaid-svg-uTBiTlWrk7j4vE0f .image-shape .label,#mermaid-svg-uTBiTlWrk7j4vE0f .icon-shape .label{text-align:center;}#mermaid-svg-uTBiTlWrk7j4vE0f .node.clickable{cursor:pointer;}#mermaid-svg-uTBiTlWrk7j4vE0f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uTBiTlWrk7j4vE0f .arrowheadPath{fill:#333333;}#mermaid-svg-uTBiTlWrk7j4vE0f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uTBiTlWrk7j4vE0f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uTBiTlWrk7j4vE0f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uTBiTlWrk7j4vE0f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uTBiTlWrk7j4vE0f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uTBiTlWrk7j4vE0f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster text{fill:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f .cluster span{color:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f 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-uTBiTlWrk7j4vE0f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uTBiTlWrk7j4vE0f rect.text{fill:none;stroke-width:0;}#mermaid-svg-uTBiTlWrk7j4vE0f .icon-shape,#mermaid-svg-uTBiTlWrk7j4vE0f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uTBiTlWrk7j4vE0f .icon-shape p,#mermaid-svg-uTBiTlWrk7j4vE0f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uTBiTlWrk7j4vE0f .icon-shape .label rect,#mermaid-svg-uTBiTlWrk7j4vE0f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uTBiTlWrk7j4vE0f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uTBiTlWrk7j4vE0f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uTBiTlWrk7j4vE0f :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-oMqJTrMQL7vPqGNJ{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-oMqJTrMQL7vPqGNJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oMqJTrMQL7vPqGNJ .error-icon{fill:#552222;}#mermaid-svg-oMqJTrMQL7vPqGNJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oMqJTrMQL7vPqGNJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .marker.cross{stroke:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oMqJTrMQL7vPqGNJ p{margin:0;}#mermaid-svg-oMqJTrMQL7vPqGNJ defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-oMqJTrMQL7vPqGNJ g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-oMqJTrMQL7vPqGNJ g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-oMqJTrMQL7vPqGNJ g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-oMqJTrMQL7vPqGNJ g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-oMqJTrMQL7vPqGNJ .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-oMqJTrMQL7vPqGNJ .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oMqJTrMQL7vPqGNJ .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oMqJTrMQL7vPqGNJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oMqJTrMQL7vPqGNJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oMqJTrMQL7vPqGNJ .edgeLabel .label text{fill:#333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .label div .edgeLabel{color:#333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-oMqJTrMQL7vPqGNJ .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-oMqJTrMQL7vPqGNJ .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-oMqJTrMQL7vPqGNJ .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ #statediagram-barbEnd{fill:#333333;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .cluster-label,#mermaid-svg-oMqJTrMQL7vPqGNJ .nodeLabel{color:#131300;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .note-edge{stroke-dasharray:5;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-note text{fill:black;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram-note .nodeLabel{color:black;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagram .edgeLabel{color:red;}#mermaid-svg-oMqJTrMQL7vPqGNJ #dependencyStart,#mermaid-svg-oMqJTrMQL7vPqGNJ #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-oMqJTrMQL7vPqGNJ .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oMqJTrMQL7vPqGNJ :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更安全:
这样就把"决策权"还给了用户,而不是让插件代替用户做出不可逆的操作。
顶栏按钮的启用规则(为什么有些按钮是灰的)
顶栏不是"有改动就全亮",每一组控件都有独立的显示/启用条件。这样设计是为了避免用户在只有一个块、只有一个文件时误点无意义的导航:
| ↑↓ | 当前文件有待审查块 | 有 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-UgI277M90iWPg6Qs{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-UgI277M90iWPg6Qs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UgI277M90iWPg6Qs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UgI277M90iWPg6Qs .error-icon{fill:#552222;}#mermaid-svg-UgI277M90iWPg6Qs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UgI277M90iWPg6Qs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UgI277M90iWPg6Qs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UgI277M90iWPg6Qs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UgI277M90iWPg6Qs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UgI277M90iWPg6Qs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UgI277M90iWPg6Qs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UgI277M90iWPg6Qs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UgI277M90iWPg6Qs .marker.cross{stroke:#333333;}#mermaid-svg-UgI277M90iWPg6Qs svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UgI277M90iWPg6Qs p{margin:0;}#mermaid-svg-UgI277M90iWPg6Qs .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UgI277M90iWPg6Qs .cluster-label text{fill:#333;}#mermaid-svg-UgI277M90iWPg6Qs .cluster-label span{color:#333;}#mermaid-svg-UgI277M90iWPg6Qs .cluster-label span p{background-color:transparent;}#mermaid-svg-UgI277M90iWPg6Qs .label text,#mermaid-svg-UgI277M90iWPg6Qs span{fill:#333;color:#333;}#mermaid-svg-UgI277M90iWPg6Qs .node rect,#mermaid-svg-UgI277M90iWPg6Qs .node circle,#mermaid-svg-UgI277M90iWPg6Qs .node ellipse,#mermaid-svg-UgI277M90iWPg6Qs .node polygon,#mermaid-svg-UgI277M90iWPg6Qs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UgI277M90iWPg6Qs .rough-node .label text,#mermaid-svg-UgI277M90iWPg6Qs .node .label text,#mermaid-svg-UgI277M90iWPg6Qs .image-shape .label,#mermaid-svg-UgI277M90iWPg6Qs .icon-shape .label{text-anchor:middle;}#mermaid-svg-UgI277M90iWPg6Qs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UgI277M90iWPg6Qs .rough-node .label,#mermaid-svg-UgI277M90iWPg6Qs .node .label,#mermaid-svg-UgI277M90iWPg6Qs .image-shape .label,#mermaid-svg-UgI277M90iWPg6Qs .icon-shape .label{text-align:center;}#mermaid-svg-UgI277M90iWPg6Qs .node.clickable{cursor:pointer;}#mermaid-svg-UgI277M90iWPg6Qs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UgI277M90iWPg6Qs .arrowheadPath{fill:#333333;}#mermaid-svg-UgI277M90iWPg6Qs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UgI277M90iWPg6Qs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UgI277M90iWPg6Qs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UgI277M90iWPg6Qs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UgI277M90iWPg6Qs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UgI277M90iWPg6Qs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UgI277M90iWPg6Qs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UgI277M90iWPg6Qs .cluster text{fill:#333;}#mermaid-svg-UgI277M90iWPg6Qs .cluster span{color:#333;}#mermaid-svg-UgI277M90iWPg6Qs 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-UgI277M90iWPg6Qs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UgI277M90iWPg6Qs rect.text{fill:none;stroke-width:0;}#mermaid-svg-UgI277M90iWPg6Qs .icon-shape,#mermaid-svg-UgI277M90iWPg6Qs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UgI277M90iWPg6Qs .icon-shape p,#mermaid-svg-UgI277M90iWPg6Qs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UgI277M90iWPg6Qs .icon-shape .label rect,#mermaid-svg-UgI277M90iWPg6Qs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UgI277M90iWPg6Qs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UgI277M90iWPg6Qs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UgI277M90iWPg6Qs :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-6F9I08FxU04VjHZe{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-6F9I08FxU04VjHZe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6F9I08FxU04VjHZe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6F9I08FxU04VjHZe .error-icon{fill:#552222;}#mermaid-svg-6F9I08FxU04VjHZe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6F9I08FxU04VjHZe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6F9I08FxU04VjHZe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6F9I08FxU04VjHZe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6F9I08FxU04VjHZe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6F9I08FxU04VjHZe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6F9I08FxU04VjHZe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6F9I08FxU04VjHZe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6F9I08FxU04VjHZe .marker.cross{stroke:#333333;}#mermaid-svg-6F9I08FxU04VjHZe svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6F9I08FxU04VjHZe p{margin:0;}#mermaid-svg-6F9I08FxU04VjHZe .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6F9I08FxU04VjHZe text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6F9I08FxU04VjHZe .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6F9I08FxU04VjHZe .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6F9I08FxU04VjHZe .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6F9I08FxU04VjHZe .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6F9I08FxU04VjHZe #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6F9I08FxU04VjHZe .sequenceNumber{fill:white;}#mermaid-svg-6F9I08FxU04VjHZe #sequencenumber{fill:#333;}#mermaid-svg-6F9I08FxU04VjHZe #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6F9I08FxU04VjHZe .messageText{fill:#333;stroke:none;}#mermaid-svg-6F9I08FxU04VjHZe .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6F9I08FxU04VjHZe .labelText,#mermaid-svg-6F9I08FxU04VjHZe .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6F9I08FxU04VjHZe .loopText,#mermaid-svg-6F9I08FxU04VjHZe .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6F9I08FxU04VjHZe .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-6F9I08FxU04VjHZe .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6F9I08FxU04VjHZe .noteText,#mermaid-svg-6F9I08FxU04VjHZe .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6F9I08FxU04VjHZe .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6F9I08FxU04VjHZe .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6F9I08FxU04VjHZe .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6F9I08FxU04VjHZe .actorPopupMenu{position:absolute;}#mermaid-svg-6F9I08FxU04VjHZe .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-6F9I08FxU04VjHZe .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6F9I08FxU04VjHZe .actor-man circle,#mermaid-svg-6F9I08FxU04VjHZe line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6F9I08FxU04VjHZe :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-eM5tG3ATXwuPNImA{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-eM5tG3ATXwuPNImA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eM5tG3ATXwuPNImA .error-icon{fill:#552222;}#mermaid-svg-eM5tG3ATXwuPNImA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eM5tG3ATXwuPNImA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eM5tG3ATXwuPNImA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA .marker.cross{stroke:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eM5tG3ATXwuPNImA p{margin:0;}#mermaid-svg-eM5tG3ATXwuPNImA defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-eM5tG3ATXwuPNImA g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-eM5tG3ATXwuPNImA g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-eM5tG3ATXwuPNImA g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-eM5tG3ATXwuPNImA g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-eM5tG3ATXwuPNImA .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-eM5tG3ATXwuPNImA .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-eM5tG3ATXwuPNImA .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-eM5tG3ATXwuPNImA .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eM5tG3ATXwuPNImA .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-eM5tG3ATXwuPNImA .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-eM5tG3ATXwuPNImA .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-eM5tG3ATXwuPNImA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eM5tG3ATXwuPNImA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eM5tG3ATXwuPNImA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eM5tG3ATXwuPNImA .edgeLabel .label text{fill:#333;}#mermaid-svg-eM5tG3ATXwuPNImA .label div .edgeLabel{color:#333;}#mermaid-svg-eM5tG3ATXwuPNImA .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-eM5tG3ATXwuPNImA .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-eM5tG3ATXwuPNImA .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-eM5tG3ATXwuPNImA .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eM5tG3ATXwuPNImA .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eM5tG3ATXwuPNImA #statediagram-barbEnd{fill:#333333;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eM5tG3ATXwuPNImA .cluster-label,#mermaid-svg-eM5tG3ATXwuPNImA .nodeLabel{color:#131300;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-eM5tG3ATXwuPNImA .note-edge{stroke-dasharray:5;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-note text{fill:black;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram-note .nodeLabel{color:black;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagram .edgeLabel{color:red;}#mermaid-svg-eM5tG3ATXwuPNImA #dependencyStart,#mermaid-svg-eM5tG3ATXwuPNImA #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-eM5tG3ATXwuPNImA .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eM5tG3ATXwuPNImA :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)
与旧版 cc-gui-review 的对比(README 摘要)
| 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 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,也就是删除发生的那一行。
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,第二个块的坐标就变了。
所以我们的做法是:
第九章:这样设计,如何避免 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-tcyOqg3uAcFvicun{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-tcyOqg3uAcFvicun .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tcyOqg3uAcFvicun .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tcyOqg3uAcFvicun .error-icon{fill:#552222;}#mermaid-svg-tcyOqg3uAcFvicun .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tcyOqg3uAcFvicun .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tcyOqg3uAcFvicun .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tcyOqg3uAcFvicun .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tcyOqg3uAcFvicun .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tcyOqg3uAcFvicun .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tcyOqg3uAcFvicun .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tcyOqg3uAcFvicun .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tcyOqg3uAcFvicun .marker.cross{stroke:#333333;}#mermaid-svg-tcyOqg3uAcFvicun svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tcyOqg3uAcFvicun p{margin:0;}#mermaid-svg-tcyOqg3uAcFvicun .edge{stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .section–1 rect,#mermaid-svg-tcyOqg3uAcFvicun .section–1 path,#mermaid-svg-tcyOqg3uAcFvicun .section–1 circle,#mermaid-svg-tcyOqg3uAcFvicun .section–1 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section–1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section–1 text{fill:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon–1{font-size:40px;color:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge–1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth–1{stroke-width:17;}#mermaid-svg-tcyOqg3uAcFvicun .section–1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-0 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-0 path,#mermaid-svg-tcyOqg3uAcFvicun .section-0 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-0 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-tcyOqg3uAcFvicun .section-0 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-0{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-0{stroke-width:14;}#mermaid-svg-tcyOqg3uAcFvicun .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-1 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-1 path,#mermaid-svg-tcyOqg3uAcFvicun .section-1 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-1 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-1 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-1{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-1{stroke-width:11;}#mermaid-svg-tcyOqg3uAcFvicun .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-2 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-2 path,#mermaid-svg-tcyOqg3uAcFvicun .section-2 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-2 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-2 text{fill:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-2{stroke-width:8;}#mermaid-svg-tcyOqg3uAcFvicun .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-3 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-3 path,#mermaid-svg-tcyOqg3uAcFvicun .section-3 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-3 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-3 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-3{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-3{stroke-width:5;}#mermaid-svg-tcyOqg3uAcFvicun .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-4 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-4 path,#mermaid-svg-tcyOqg3uAcFvicun .section-4 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-4 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-4 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-4{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-4{stroke-width:2;}#mermaid-svg-tcyOqg3uAcFvicun .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-5 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-5 path,#mermaid-svg-tcyOqg3uAcFvicun .section-5 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-5 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-5 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-5{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-5{stroke-width:-1;}#mermaid-svg-tcyOqg3uAcFvicun .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-6 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-6 path,#mermaid-svg-tcyOqg3uAcFvicun .section-6 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-6 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-6 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-6{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-6{stroke-width:-4;}#mermaid-svg-tcyOqg3uAcFvicun .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-7 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-7 path,#mermaid-svg-tcyOqg3uAcFvicun .section-7 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-7 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-7 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-7{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-7{stroke-width:-7;}#mermaid-svg-tcyOqg3uAcFvicun .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-8 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-8 path,#mermaid-svg-tcyOqg3uAcFvicun .section-8 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-8 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-8 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-8{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-8{stroke-width:-10;}#mermaid-svg-tcyOqg3uAcFvicun .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-9 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-9 path,#mermaid-svg-tcyOqg3uAcFvicun .section-9 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-9 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-9 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-9{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-9{stroke-width:-13;}#mermaid-svg-tcyOqg3uAcFvicun .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-10 rect,#mermaid-svg-tcyOqg3uAcFvicun .section-10 path,#mermaid-svg-tcyOqg3uAcFvicun .section-10 circle,#mermaid-svg-tcyOqg3uAcFvicun .section-10 polygon,#mermaid-svg-tcyOqg3uAcFvicun .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-10 text{fill:black;}#mermaid-svg-tcyOqg3uAcFvicun .node-icon-10{font-size:40px;color:black;}#mermaid-svg-tcyOqg3uAcFvicun .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .edge-depth-10{stroke-width:-16;}#mermaid-svg-tcyOqg3uAcFvicun .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-tcyOqg3uAcFvicun .disabled,#mermaid-svg-tcyOqg3uAcFvicun .disabled circle,#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:lightgray;}#mermaid-svg-tcyOqg3uAcFvicun .disabled text{fill:#efefef;}#mermaid-svg-tcyOqg3uAcFvicun .section-root rect,#mermaid-svg-tcyOqg3uAcFvicun .section-root path,#mermaid-svg-tcyOqg3uAcFvicun .section-root circle,#mermaid-svg-tcyOqg3uAcFvicun .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-tcyOqg3uAcFvicun .section-root text{fill:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .section-root span{color:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .section-2 span{color:#ffffff;}#mermaid-svg-tcyOqg3uAcFvicun .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-tcyOqg3uAcFvicun .edge{fill:none;}#mermaid-svg-tcyOqg3uAcFvicun .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-tcyOqg3uAcFvicun :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
防 Bug 原则
单一事实源
只问 LST
禁止第二套 store
不缓存 ranges
现查 getRanges
Keep 纯 UI
不推进 baseline
激活只看 VFS
编辑保存不复活
危险操作交 git
无 Reject All
开发流程上的防 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 把 [Reject][Keep] 绣在行边;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 的能力对比
| Keep / Reject | 原生 | ✔ |
| 块间导航 | 原生 | ✔ |
| 多文件 Review Next | 原生 | ✔ |
| 实时流式生成 UI | 模型驱动 | 依赖 LST 重算(有延迟) |
| 只高亮真插入行 | ✔ | ❌ 整块 MODIFY 标绿 |
| 块状展示位置 | 行内 | Block inlay(块下方独立行) |
| 顶栏文件统计 | 无 | ✔ 文件计数 + 块计数 |
结论:我们在 IDEA 里逼近 Cursor 的审查体验,但在" MODIFY 粒度"和"流式零延迟"上仍受 LST 语义约束——这是平台边界,不是再堆 60 个版本能解决的。
从 cc-gui-review 3.0.x 迁移
第十二章:前景与演进方向
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 起步。
还剩下的"真硬"问题(诚实清单)
第十三章:从放弃到拾起的启示
为什么最初会失败
我一开始就问错了问题。我问的是:
“怎样自己管理diff状态,这样我就能完全控制UI的展示?”
结果就是:造了5套互相冲突的状态系统。
正确的问题应该是:
“IDEA的官方组件能提供什么能力?我应该如何利用而不是绕过这些能力?”
这个故事的道德
不要重新发明轮子。
特别是当这个轮子已经被一个比你聪明得多的团队打磨了10年、被数百万开发者使用过的时候。
LineStatusTracker不是一个小功能,它是IDEA版本控制系统的核心引擎。它处理了所有边界case、异步问题、缓存策略。如果我不用它,就得自己处理所有这些问题。而事实上,我差点就这么做了。
技术债的本质
技术债不是"代码写得烂"。技术债的本质是:错误的抽象选择。
#mermaid-svg-bx9OJtVfMcVvNDN3{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-bx9OJtVfMcVvNDN3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bx9OJtVfMcVvNDN3 .error-icon{fill:#552222;}#mermaid-svg-bx9OJtVfMcVvNDN3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bx9OJtVfMcVvNDN3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .marker.cross{stroke:#333333;}#mermaid-svg-bx9OJtVfMcVvNDN3 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bx9OJtVfMcVvNDN3 p{margin:0;}#mermaid-svg-bx9OJtVfMcVvNDN3 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster-label text{fill:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster-label span{color:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster-label span p{background-color:transparent;}#mermaid-svg-bx9OJtVfMcVvNDN3 .label text,#mermaid-svg-bx9OJtVfMcVvNDN3 span{fill:#333;color:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .node rect,#mermaid-svg-bx9OJtVfMcVvNDN3 .node circle,#mermaid-svg-bx9OJtVfMcVvNDN3 .node ellipse,#mermaid-svg-bx9OJtVfMcVvNDN3 .node polygon,#mermaid-svg-bx9OJtVfMcVvNDN3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .rough-node .label text,#mermaid-svg-bx9OJtVfMcVvNDN3 .node .label text,#mermaid-svg-bx9OJtVfMcVvNDN3 .image-shape .label,#mermaid-svg-bx9OJtVfMcVvNDN3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-bx9OJtVfMcVvNDN3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .rough-node .label,#mermaid-svg-bx9OJtVfMcVvNDN3 .node .label,#mermaid-svg-bx9OJtVfMcVvNDN3 .image-shape .label,#mermaid-svg-bx9OJtVfMcVvNDN3 .icon-shape .label{text-align:center;}#mermaid-svg-bx9OJtVfMcVvNDN3 .node.clickable{cursor:pointer;}#mermaid-svg-bx9OJtVfMcVvNDN3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .arrowheadPath{fill:#333333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bx9OJtVfMcVvNDN3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bx9OJtVfMcVvNDN3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bx9OJtVfMcVvNDN3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster text{fill:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 .cluster span{color:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 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-bx9OJtVfMcVvNDN3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bx9OJtVfMcVvNDN3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-bx9OJtVfMcVvNDN3 .icon-shape,#mermaid-svg-bx9OJtVfMcVvNDN3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bx9OJtVfMcVvNDN3 .icon-shape p,#mermaid-svg-bx9OJtVfMcVvNDN3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bx9OJtVfMcVvNDN3 .icon-shape .label rect,#mermaid-svg-bx9OJtVfMcVvNDN3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bx9OJtVfMcVvNDN3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bx9OJtVfMcVvNDN3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bx9OJtVfMcVvNDN3 :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。然后一切就豁然开朗了。
这个插件现在很小,很稳定。不是因为我代码写得好,而是因为我站在了巨人的肩膀上。
所以,如果你现在也在某个项目里挣扎,也许你该做的就是:停下来,放弃眼前的方案,好好想想——还有没有更聪明的方式?
答案就在那儿,等着被发现。
后记:现在怎么用
构建与安装
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。
日常使用
相关文档
| README.md | 产品概览、快速开始 |
| REQUIREMENTS.md | 需求唯一来源 |
| VERSION_HISTORY.md | 版本历史 |
| 分析报告.md | 旧架构诊断与 LST 方案 |
仓库:https://github.com/quyixiao/cc-idea-code-review
贡献与需求变更流程
#mermaid-svg-juLVT8jI6dHLpQ2L{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-juLVT8jI6dHLpQ2L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-juLVT8jI6dHLpQ2L .error-icon{fill:#552222;}#mermaid-svg-juLVT8jI6dHLpQ2L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-juLVT8jI6dHLpQ2L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-juLVT8jI6dHLpQ2L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-juLVT8jI6dHLpQ2L .marker.cross{stroke:#333333;}#mermaid-svg-juLVT8jI6dHLpQ2L svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-juLVT8jI6dHLpQ2L p{margin:0;}#mermaid-svg-juLVT8jI6dHLpQ2L .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster-label text{fill:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster-label span{color:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster-label span p{background-color:transparent;}#mermaid-svg-juLVT8jI6dHLpQ2L .label text,#mermaid-svg-juLVT8jI6dHLpQ2L span{fill:#333;color:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L .node rect,#mermaid-svg-juLVT8jI6dHLpQ2L .node circle,#mermaid-svg-juLVT8jI6dHLpQ2L .node ellipse,#mermaid-svg-juLVT8jI6dHLpQ2L .node polygon,#mermaid-svg-juLVT8jI6dHLpQ2L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-juLVT8jI6dHLpQ2L .rough-node .label text,#mermaid-svg-juLVT8jI6dHLpQ2L .node .label text,#mermaid-svg-juLVT8jI6dHLpQ2L .image-shape .label,#mermaid-svg-juLVT8jI6dHLpQ2L .icon-shape .label{text-anchor:middle;}#mermaid-svg-juLVT8jI6dHLpQ2L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-juLVT8jI6dHLpQ2L .rough-node .label,#mermaid-svg-juLVT8jI6dHLpQ2L .node .label,#mermaid-svg-juLVT8jI6dHLpQ2L .image-shape .label,#mermaid-svg-juLVT8jI6dHLpQ2L .icon-shape .label{text-align:center;}#mermaid-svg-juLVT8jI6dHLpQ2L .node.clickable{cursor:pointer;}#mermaid-svg-juLVT8jI6dHLpQ2L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-juLVT8jI6dHLpQ2L .arrowheadPath{fill:#333333;}#mermaid-svg-juLVT8jI6dHLpQ2L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-juLVT8jI6dHLpQ2L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-juLVT8jI6dHLpQ2L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-juLVT8jI6dHLpQ2L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-juLVT8jI6dHLpQ2L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-juLVT8jI6dHLpQ2L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster text{fill:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L .cluster span{color:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L 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-juLVT8jI6dHLpQ2L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-juLVT8jI6dHLpQ2L rect.text{fill:none;stroke-width:0;}#mermaid-svg-juLVT8jI6dHLpQ2L .icon-shape,#mermaid-svg-juLVT8jI6dHLpQ2L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-juLVT8jI6dHLpQ2L .icon-shape p,#mermaid-svg-juLVT8jI6dHLpQ2L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-juLVT8jI6dHLpQ2L .icon-shape .label rect,#mermaid-svg-juLVT8jI6dHLpQ2L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-juLVT8jI6dHLpQ2L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-juLVT8jI6dHLpQ2L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-juLVT8jI6dHLpQ2L :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
提出需求
讨论确认
更新 REQUIREMENTS.md
改代码
gradle build
提交 + tag + PR
不接受「先改代码再补文档」。许可证待定(建议 Apache 2.0 或 MIT)。
致谢
架构灵感来自 分析报告.md:旧版根本问题不是「插件写得不好」,而是重新发明了 LineStatusTracker——从 LST 出发,12 个文件替代 72 个。
希望这个故事能给你一些启发。也许你正在经历类似的困境,也许你正考虑某个架构决策。记住两句话:
“如果要从头整,请从 LineStatusTracker 开始,不要从 ComparisonManager 开始。”
“如果要从头整,请从基础设施开始,不要从你自己的想象开始。”


