目录
- 序言:放弃那一刻
- [产品是什么(README 一句话)](#产品是什么(README 一句话))
- 第一章:反思之前,先看看现在发生了什么
- 第二章:那份改变人生的分析报告
- 第三章:重生的架构
- 第四章:生命周期------简到不能再简
- 第五章:用户体验的细节
- 第六章:踩过的坑
- 第七章:技术细节
- 第七章附:需求规格全貌与覆盖清单
- 第八章:那些神奇的细节
- [第九章:这样设计,如何避免 Bug](#第九章:这样设计,如何避免 Bug)
- 第十章:好处与价值
- [第十一章:已知限制、兼容性与 Cursor 对比](#第十一章:已知限制、兼容性与 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 的效果一样

第一章:反思之前,先看看现在发生了什么
所有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官方提供的一个组件,它负责:
- 比较当前editor document和git HEAD
- 计算哪些行新增了、哪些删除了、哪些修改了
- 提供API让你回滚这些改动
- 当文件变了时自动重新计算
而我花了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告诉我们,我们就把这些行涂成绿底,把删除线画成红底
- 用户点Keep或Reject按钮时,我们问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更安全:
git status看清楚所有改动的文件- 确认范围无误
- 再用标准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)
- 每回滚一块后,重新从 tracker 取最新 ranges(避免 stale Range)
- 从底向上 回滚(
maxByOrNull { it.line1 }) - 200 次上限防死循环
- 全程
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,第二个块的坐标就变了。
所以我们的做法是:
- 每次回滚一个块后,重新从LST获取最新的ranges
- 从底向上回滚(maxByOrNull { it.line1 }),这样不会因为坐标变化影响前面的块
- 设一个200次的死循环防护
- 所有操作都在
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
- 单一事实源:diff 状态只问 LST,禁止再引入第二套 hunk/store/baseline。
- 不缓存 ranges :需要时用
tracker.getRanges()现取;LST 自己会监听 document 和 git。 - Keep 是纯 UI:Accept 不推进 baseline、不写 store;文档里已经有改动就是 accept。
- 激活只看 VFS 外部写盘:用户编辑、保存不触发 ACTIVE,避免 UI 和用户意图打架。
- 危险操作交给 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 迁移
- 卸载
cc-gui-review - 重启 IDE
- 安装
cc-review-lst的 zip(./gradlew buildPlugin产出) - 再重启 IDE
- 无需迁移数据------本插件不读任何旧版状态
第十二章:前景与演进方向
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 起步。
还剩下的"真硬"问题(诚实清单)
- AI 写盘 base 语义 :LST 默认对齐 VCS HEAD,不是"AI 动手前那一秒"的快照------PreToolUse snapshot 若要完美,需要 shadow tracker 或
setBaseRevision。 - AI _burst 写盘时 LST 闪烁:LST 为人手编辑设计,一秒 50 行时 gutter 也会闪------这是 IDEA 自己的行为,社区接受度高于自研 gate,但不等于零干扰。
- 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。
日常使用
- 确保项目是 Git 管理 ,IDEA 版本 2026.1.x
- 让任意 AI 或外部工具改一个已跟踪的文件(Claude Code、Copilot、终端编辑均可)
- 文件会在后台 tab 自动打开(不抢焦点);若已打开,直接看当前编辑器
- 等 1--5 秒(LST 异步计算);应看到:绿底 + 红删 + 块按钮 + 顶栏
- 逐块或逐文件 Keep / Reject;全部处理完 → 自动 INACTIVE,UI 消失
- 若 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
- Fork 本仓库 →
git checkout -b feature/xxx - 先读 REQUIREMENTS.md(唯一来源)
- 改代码,确保
./gradlew build通过 - 写清楚 commit message,升级版本号
- Pull Request
不接受「先改代码再补文档」。许可证待定(建议 Apache 2.0 或 MIT)。
致谢
架构灵感来自 分析报告.md:旧版根本问题不是「插件写得不好」,而是重新发明了 LineStatusTracker------从 LST 出发,12 个文件替代 72 个。
希望这个故事能给你一些启发。也许你正在经历类似的困境,也许你正考虑某个架构决策。记住两句话:
"如果要从头整,请从 LineStatusTracker 开始,不要从 ComparisonManager 开始。"
"如果要从头整,请从基础设施开始,不要从你自己的想象开始。"