mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-26 23:11:51 +08:00
242 lines
10 KiB
Markdown
242 lines
10 KiB
Markdown
|
|
# AI Code Diff Preview — 项目文档
|
|||
|
|
|
|||
|
|
## 一、功能概述
|
|||
|
|
|
|||
|
|
AI Code Diff Preview 是一个 VSCode 插件,为 Claude Code 提供类似 Cursor 的 AI 代码变更审查体验:
|
|||
|
|
|
|||
|
|
- **自动收集**:监听 Claude 的 Edit/Write 操作,静默收集所有文件变更
|
|||
|
|
- **总览面板**:Stop 后弹出 Webview 面板,展示每个文件的完整双栏 Diff
|
|||
|
|
- **内置 Diff**:点击单个文件打开 VSCode 原生 Diff 编辑器,状态栏显示 Accept/Reject 按钮
|
|||
|
|
- **粒度控制**:逐文件 Accept/Reject + 全局 Accept All / Reject All
|
|||
|
|
- **编辑合并**:同文件连续多次编辑自动合并为一个 Diff(前次未处理时);已处理后重新编辑则独立追踪
|
|||
|
|
|
|||
|
|
## 二、项目结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
src/
|
|||
|
|
├── extension.ts # 插件入口,初始化模块 + 注册命令
|
|||
|
|
├── trigger/
|
|||
|
|
│ └── hookHandler.ts # Hook 事件处理 (Edit/Stop 分发)
|
|||
|
|
├── snapshot/
|
|||
|
|
│ └── snapshotManager.ts # ★ 核心数据层: ChangeSetManager
|
|||
|
|
├── diff/
|
|||
|
|
│ └── diffEngine.ts # Diff 算法 (Myers)
|
|||
|
|
├── render/
|
|||
|
|
│ ├── reviewPanel.ts # ★ 总览 Webview 面板
|
|||
|
|
│ ├── diffViewer.ts # ★ VSCode 内置 Diff 查看器 + accept/reject + 自动推进
|
|||
|
|
│ ├── statusBar.ts # ★ 状态栏 (Diff 模式按钮 / 摘要模式)
|
|||
|
|
│ ├── inlineDecorator.ts # 内联装饰器 (保留, 未启用)
|
|||
|
|
│ └── codeLensProvider.ts # CodeLens (保留, 未启用)
|
|||
|
|
├── transaction/
|
|||
|
|
│ ├── acceptHandler.ts # Accept 处理器 (legacy)
|
|||
|
|
│ └── rejectHandler.ts # Reject 处理器 (legacy)
|
|||
|
|
└── models/
|
|||
|
|
├── types.ts # 类型定义
|
|||
|
|
└── constants.ts # 常量
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 三、核心数据模型
|
|||
|
|
|
|||
|
|
### 层级关系
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ChangeSet (一轮对话)
|
|||
|
|
└── FileChange[] (每个文件一个)
|
|||
|
|
├── originalContent (首次编辑前的文件快照)
|
|||
|
|
├── latestContent (最新文件内容)
|
|||
|
|
├── diffLines[] (originalContent → latestContent 的聚合 Diff)
|
|||
|
|
├── status (pending | accepted | rejected)
|
|||
|
|
└── EditRecord[] (每次 Edit/Write 一条)
|
|||
|
|
├── beforeContent (本次编辑前快照)
|
|||
|
|
├── afterContent (本次编辑后快照)
|
|||
|
|
├── oldString / newString (精确替换内容)
|
|||
|
|
├── diffLines[] (本次编辑的独立 Diff)
|
|||
|
|
└── status (pending | accepted | rejected)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 编辑合并策略
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
同文件多次 Edit 时:
|
|||
|
|
┌─ 已有 pending FileChange? ── YES ──→ ★ 合并: 追加 EditRecord, 更新聚合 Diff
|
|||
|
|
│
|
|||
|
|
└─ NO (首次 / 前轮已 accept/reject)
|
|||
|
|
└──→ 创建新 FileChange, 独立追踪
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**关键**: 查找 FileChange 时**优先匹配 pending 的**(而非用 `.find()` 取第一个),避免同文件多轮编辑时孤儿 FileChange 的 Bug。
|
|||
|
|
|
|||
|
|
## 四、完整流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A["Claude 对话中"] --> B["Edit / Write 工具调用"]
|
|||
|
|
B --> C["写入 .claude/hooks/pending.json"]
|
|||
|
|
C --> D["FileSystemWatcher 监听到"]
|
|||
|
|
D --> E["读取并删除 pending.json"]
|
|||
|
|
E --> F{"toolName?"}
|
|||
|
|
|
|||
|
|
F -->|"Edit / Write"| G["hookHandler.handleEdit()"]
|
|||
|
|
G --> H["ChangeSetManager.recordEdit()"]
|
|||
|
|
H --> I{"同文件已有<br/>pending FileChange?"}
|
|||
|
|
I -->|"YES"| J["★ 合并: 追加 EditRecord<br/>更新聚合 Diff"]
|
|||
|
|
I -->|"NO"| K["新建 FileChange<br/>捕获 originalContent"]
|
|||
|
|
J --> L{"autoShowDiffPerEdit<br/>配置?"}
|
|||
|
|
K --> L
|
|||
|
|
L -->|"true"| M["弹出 VSCode 内置 Diff"]
|
|||
|
|
L -->|"false"| N["静默收集, 不弹 UI"]
|
|||
|
|
|
|||
|
|
F -->|"Stop"| O["hookHandler.handleStop()"]
|
|||
|
|
O --> P["ChangeSetManager.markReady()"]
|
|||
|
|
P --> Q{"有 pending 变更?"}
|
|||
|
|
Q -->|"NO"| R["无操作"]
|
|||
|
|
Q -->|"YES"| S["★ 弹出总览 Webview 面板<br/>reviewPanel.show()"]
|
|||
|
|
|
|||
|
|
S --> T["总览面板: 每个文件一张卡片"]
|
|||
|
|
T --> U["卡片内含:<br/>— 文件信息 + 增减统计<br/>— 双栏 Diff (原始 vs 最新)<br/>— ✔ 接受 / ✘ 拒绝 按钮"]
|
|||
|
|
T --> V["顶部: Accept All / Reject All"]
|
|||
|
|
|
|||
|
|
U --> W["用户点击 ✔ 接受"]
|
|||
|
|
U --> X["用户点击 ✘ 拒绝"]
|
|||
|
|
V --> Y["用户点击 Accept All"]
|
|||
|
|
V --> Z["用户点击 Reject All"]
|
|||
|
|
|
|||
|
|
W --> AA["acceptFile(fileId)<br/>写入 latestContent 到磁盘"]
|
|||
|
|
X --> AB["rejectFile(fileId)<br/>恢复 originalContent"]
|
|||
|
|
Y --> AC["acceptAll()<br/>遍历所有 pending 文件"]
|
|||
|
|
Z --> AD["rejectAll()<br/>遍历所有 pending 文件"]
|
|||
|
|
|
|||
|
|
AA --> AE["刷新总览面板 + 状态栏"]
|
|||
|
|
AB --> AE
|
|||
|
|
AC --> AE
|
|||
|
|
AD --> AE
|
|||
|
|
|
|||
|
|
T --> AF["用户点击文件卡片"]
|
|||
|
|
AF --> AG["打开 VSCode 内置 Diff 编辑器<br/>diffViewer.showFileDiff()"]
|
|||
|
|
AG --> AH["Diff 界面: 状态栏切换为<br/>$(check) Accept | $(close) Reject | $(check-all) 接受全部"]
|
|||
|
|
AH --> AI["用户操作 Accept/Reject"]
|
|||
|
|
AI --> AJ["★ 自动推进到下一个 pending 文件<br/>diffViewer.advanceToNext()"]
|
|||
|
|
AJ --> AK["全部处理完 → 🎉 通知"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 五、三种 UI 层
|
|||
|
|
|
|||
|
|
### 5.1 Webview 总览面板 (`reviewPanel.ts`)
|
|||
|
|
|
|||
|
|
触发时机:Claude Stop 后自动弹出 / 手动 `Ctrl+Shift+D`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─ AI Diff 总览 ───── [Accept All] [Reject All] ─┐
|
|||
|
|
│ │
|
|||
|
|
│ 📄 file1.ts 修改 +3 -2 · 2编辑 [✔接受][✘拒绝]│
|
|||
|
|
│ ┌──── 原始代码 ────┬──── 变更后 ────────────┐ │
|
|||
|
|
│ │ 1 line1 │ 1 line1 │ │
|
|||
|
|
│ │ 2 -old line2 │ (空) │ │
|
|||
|
|
│ │ 3 line3 │ 2 +new line2 │ │
|
|||
|
|
│ │ 4 -old line4 │ (空) │ │
|
|||
|
|
│ │ 5 line5 │ 3 +extra line │ │
|
|||
|
|
│ │ │ 4 line5 │ │
|
|||
|
|
│ └──────────────────┴────────────────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 📄 file2.ts 新增 +10 -0 · 1编辑 [✔接受][✘拒绝]│
|
|||
|
|
│ ┌──── 新文件内容 ──────────────────────────┐ │
|
|||
|
|
│ │ 1 +new code line 1 │ │
|
|||
|
|
│ │ 2 +new code line 2 │ │
|
|||
|
|
│ └──────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 VSCode 内置 Diff 编辑器 (`diffViewer.ts`)
|
|||
|
|
|
|||
|
|
触发时机:从总览面板点击文件 / `Alt+↑/↓` 导航
|
|||
|
|
|
|||
|
|
- 使用 `vscode.diff` 命令打开原生 Diff 编辑器
|
|||
|
|
- 左侧:原始内容 (红色删除行),右侧:变更后内容 (绿色新增行)
|
|||
|
|
- 临时文件机制:写入 `%TEMP%/ai-diff-preview/`,关闭 Diff 后自动删除(**无保存提示**)
|
|||
|
|
- **自动推进**:Accept/Reject 后自动打开下一个 pending 文件
|
|||
|
|
- 若当前文件仍有 pending 编辑 → 重新打开此文件
|
|||
|
|
- 否则 → 打开下一个 pending 文件
|
|||
|
|
- 全部处理完 → 弹出 🎉 通知
|
|||
|
|
|
|||
|
|
### 5.3 状态栏 (`statusBar.ts`)
|
|||
|
|
|
|||
|
|
双模式自动切换:
|
|||
|
|
|
|||
|
|
| 模式 | 触发 | 显示 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 摘要模式 | 无活跃 Diff | `$(diff) AI Diff: 2文件 5编辑` ← 点击打开总览 |
|
|||
|
|
| Diff 模式 | 内置 Diff 活跃 | `$(check) Accept` `$(close) Reject` `$(check-all) 接受全部` |
|
|||
|
|
|
|||
|
|
## 六、Accept/Reject 行为
|
|||
|
|
|
|||
|
|
| 操作 | Modify 文件 | Create 文件 |
|
|||
|
|
|------|------------|------------|
|
|||
|
|
| Accept (文件级) | 写入 `latestContent` 到磁盘 | 保留文件 |
|
|||
|
|
| Reject (文件级) | 恢复 `originalContent` 到磁盘 | 删除文件 |
|
|||
|
|
| Accept All | 遍历所有 pending 文件执行 Accept | |
|
|||
|
|
| Reject All | 遍历所有 pending 文件执行 Reject | |
|
|||
|
|
|
|||
|
|
## 七、配置项
|
|||
|
|
|
|||
|
|
| 配置 | 类型 | 默认 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| `enableAutoTrigger` | boolean | true | 监听 pending.json |
|
|||
|
|
| `autoShowDiffPerEdit` | boolean | false | 每次 Edit 后弹内置 Diff |
|
|||
|
|
| `showAllDiffsOnStop` | boolean | true | Stop 后弹总览面板 |
|
|||
|
|
| `floatingLabelMode` | string | "statusBar" | 按钮位置 |
|
|||
|
|
| `maxFileSize` | number | 100000 | 文件大小上限 |
|
|||
|
|
|
|||
|
|
## 八、快捷键
|
|||
|
|
|
|||
|
|
| 快捷键 | 条件 | 功能 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| `Ctrl+Shift+D` | `aiDiffPreview.isActive` | 打开总览面板 |
|
|||
|
|
| `Alt+↓` | `aiDiffPreview.isActive` | 下一个变更文件 |
|
|||
|
|
| `Alt+↑` | `aiDiffPreview.isActive` | 上一个变更文件 |
|
|||
|
|
| `Tab` | Diff 活跃 | Accept 当前变更 |
|
|||
|
|
| `Esc` | Diff 活跃 | Reject 当前变更 |
|
|||
|
|
| `Ctrl+Shift+A` | `aiDiffPreview.isActive` | Accept All |
|
|||
|
|
| `Ctrl+Shift+R` | `aiDiffPreview.isActive` | Reject All |
|
|||
|
|
|
|||
|
|
## 九、模块依赖关系
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
extension["extension.ts<br/>入口"]
|
|||
|
|
hook["hookHandler.ts<br/>Hook 处理"]
|
|||
|
|
manager["snapshotManager.ts<br/>ChangeSetManager<br/>★ 核心数据层"]
|
|||
|
|
review["reviewPanel.ts<br/>总览 Webview"]
|
|||
|
|
viewer["diffViewer.ts<br/>内置 Diff + 自动推进"]
|
|||
|
|
bar["statusBar.ts<br/>状态栏"]
|
|||
|
|
types["models/types.ts<br/>类型"]
|
|||
|
|
|
|||
|
|
extension --> hook
|
|||
|
|
extension --> manager
|
|||
|
|
extension --> review
|
|||
|
|
extension --> viewer
|
|||
|
|
extension --> bar
|
|||
|
|
|
|||
|
|
hook --> manager
|
|||
|
|
hook --> viewer
|
|||
|
|
|
|||
|
|
review --> manager
|
|||
|
|
review --> viewer
|
|||
|
|
|
|||
|
|
viewer --> manager
|
|||
|
|
|
|||
|
|
bar --> manager
|
|||
|
|
bar --> viewer
|
|||
|
|
|
|||
|
|
manager --> types
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 十、已修复的 Bug
|
|||
|
|
|
|||
|
|
| Bug | 根因 | 修复 |
|
|||
|
|
|-----|------|------|
|
|||
|
|
| Accept 后同文件再编辑无 Accept/Reject | `recordEdit()` 更新同一 `FileChange`,Accept 后状态机断开 | 引入 `EditRecord` 粒度,Accept/Reject 后重新编辑创建新 FileChange |
|
|||
|
|
| Accept All / Reject All 不生效 | `acceptAllFromDiff()` 调用不存在的方法 | 改为调用 `acceptFile()`,All 方法改为收集列表后逐一处理 |
|
|||
|
|
| Accept 后无后续 Diff | 无推进逻辑 | `advanceToNext()` 自动打开下一个 pending 文件 |
|
|||
|
|
| 临时文件保存提示 | 使用 `openTextDocument({content})` 创建 untitled 文档 | 改为写入临时目录 `%TEMP%/ai-diff-preview/` |
|
|||
|
|
| 同文件多轮编辑孤儿 FileChange | `.find()` 返回第一个 (已 accept) 而非 pending | 改为优先匹配 pending 的 FileChange |
|