mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-27 03:21:52 +08:00
3168e81aa6
- 核心架构搭建:四层架构设计(触发层、隔离层、Diff引擎、UI层) - 快照管理器:管理 Base/Suggest 双版本,隔离 AI 生成代码 - Diff 引擎:基于 Myers 算法计算差异,生成变更块 - 内联装饰器:红绿高亮显示删除/新增行 - CodeLens 提供器:提供 Accept/Reject 按钮 - 事务处理:Accept/Reject 原子化操作 - Claude Hooks 集成:支持 after_code_edit/after_write_file 事件 - 状态栏显示:实时显示变更状态 - 基础测试用例:7 个测试全部通过 技术栈:TypeScript + VSCode Extension API + diff 库 + esbuild
625 lines
19 KiB
Markdown
625 lines
19 KiB
Markdown
# AI Code Diff Preview - VSCode 插件立项文档
|
||
|
||
## 一、项目概述
|
||
|
||
### 1.1 项目名称
|
||
**AI Code Diff Preview** - 类 Cursor AI 代码差异预览插件
|
||
|
||
### 1.2 项目目标
|
||
实现类似 Cursor 的 Accept/Reject 差异预览功能,通过 Claude Hooks 触发,在 VSCode 中提供:
|
||
- AI 生成代码的临时隔离层
|
||
- 实时 Diff 差分预览
|
||
- 逐行/整块 Accept/Reject 操作
|
||
- 多文件修改统一管理
|
||
|
||
### 1.3 核心价值
|
||
- **安全隔离**: AI 生成的代码不直接覆盖本地文件
|
||
- **可视化对比**: 红绿高亮显示删除/新增内容
|
||
- **灵活控制**: 支持单行接受、整块拒绝等精细化操作
|
||
- **无缝集成**: 通过 Claude Hooks 自动触发,无需手动操作
|
||
|
||
---
|
||
|
||
## 二、技术选型
|
||
|
||
### 2.1 核心技术栈
|
||
|
||
| 技术领域 | 选型方案 | 理由 |
|
||
|---------|---------|------|
|
||
| **插件框架** | VSCode Extension API | 官方原生支持,API 完善 |
|
||
| **开发语言** | TypeScript | 类型安全,VSCode 插件官方推荐 |
|
||
| **Diff 算法** | `diff` 库 (Myers 算法) | 成熟稳定,支持行级/字符级差异 |
|
||
| **构建工具** | esbuild | 快速打包,VSCode 插件社区标准 |
|
||
| **测试框架** | Vitest + @vscode/test-electron | 单元测试 + 集成测试 |
|
||
| **代码规范** | ESLint + Prettier | 代码质量保证 |
|
||
|
||
### 2.2 关键依赖库
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"diff": "^5.0.0", // Diff 算法核心
|
||
"vscode-languageclient": "^9.0.0", // LSP 客户端(可选)
|
||
"uuid": "^9.0.0" // 变更块唯一标识
|
||
},
|
||
"devDependencies": {
|
||
"@types/vscode": "^1.85.0",
|
||
"@types/diff": "^5.0.0",
|
||
"esbuild": "^0.19.0",
|
||
"typescript": "^5.3.0",
|
||
"vitest": "^1.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 VSCode API 关键模块
|
||
|
||
| API 模块 | 用途 |
|
||
|---------|------|
|
||
| `vscode.workspace.applyEdit` | 原子化文件编辑 |
|
||
| `vscode.window.createTextEditorDecorationType` | Diff 高亮装饰器 |
|
||
| `vscode.languages.registerCodeLensProvider` | Accept/Reject 按钮 |
|
||
| `vscode.workspace.onDidChangeTextDocument` | 文档变更监听 |
|
||
| `vscode.commands.registerCommand` | 命令注册 |
|
||
| `WebviewViewProvider` | 侧边栏 Diff 面板(可选) |
|
||
|
||
---
|
||
|
||
## 三、架构设计
|
||
|
||
### 3.1 分层架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 触发层 (Trigger Layer) │
|
||
│ Claude Hooks: after_code_edit / after_write_file │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 临时变更隔离层 (Suggestion Layer) │
|
||
│ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ Base 快照 │ │ Suggest 临时 │ │
|
||
│ │ (只读原始) │ │ (AI 生成) │ │
|
||
│ └──────────────┘ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Diff 差分引擎 (Diff Engine) │
|
||
│ Myers 算法 → 变更块切割 → 冲突检测 → Chunk 管理 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 渲染 UI 层 (Render Layer) │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ 内联装饰器 │ │ CodeLens │ │ Diff 面板 │ │
|
||
│ │ (行高亮) │ │ (Accept/ │ │ (多文件) │ │
|
||
│ │ │ │ Reject) │ │ │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 提交/回写事务层 (Transaction Layer) │
|
||
│ Accept: 校验 → 原子写入 → LSP 刷新 → 清理临时层 │
|
||
│ Reject: 丢弃快照 → 清理 UI → 恢复原始视图 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 核心模块划分
|
||
|
||
```
|
||
src/
|
||
├── extension.ts # 插件入口
|
||
├── trigger/
|
||
│ ├── hookHandler.ts # Claude Hooks 处理器
|
||
│ └── commandHandler.ts # 手动命令处理器
|
||
├── snapshot/
|
||
│ ├── snapshotManager.ts # 快照管理器
|
||
│ └── snapshotStore.ts # 快照存储
|
||
├── diff/
|
||
│ ├── diffEngine.ts # Diff 计算引擎
|
||
│ ├── chunkManager.ts # 变更块管理
|
||
│ └── conflictDetector.ts # 冲突检测
|
||
├── render/
|
||
│ ├── inlineDecorator.ts # 内联装饰器
|
||
│ ├── codeLensProvider.ts # CodeLens 提供器
|
||
│ ├── diffPanel.ts # Diff 预览面板
|
||
│ └── statusBar.ts # 状态栏
|
||
├── transaction/
|
||
│ ├── acceptHandler.ts # Accept 处理器
|
||
│ ├── rejectHandler.ts # Reject 处理器
|
||
│ └── transactionManager.ts # 事务管理器
|
||
├── models/
|
||
│ ├── types.ts # 类型定义
|
||
│ └── constants.ts # 常量定义
|
||
└── utils/
|
||
├── diffUtils.ts # Diff 工具函数
|
||
├── fileUtils.ts # 文件操作工具
|
||
└── logger.ts # 日志工具
|
||
```
|
||
|
||
---
|
||
|
||
## 四、功能需求
|
||
|
||
### 4.1 核心功能
|
||
|
||
#### 4.1.1 临时变更隔离层
|
||
- [ ] 捕获编辑前文件快照(Base 版本)
|
||
- [ ] 接收 AI 生成的 Patch 并存储为临时层
|
||
- [ ] 维护 Base 与 Suggest 双版本并行状态
|
||
- [ ] 支持会话级别的快照持久化
|
||
|
||
#### 4.1.2 Diff 差分引擎
|
||
- [ ] 实现 Myers 差分算法封装
|
||
- [ ] 将 Patch 切分为独立变更块(Chunk)
|
||
- [ ] 为每个 Chunk 生成唯一 ID、起止行号
|
||
- [ ] 支持行级和字符级差异对比
|
||
- [ ] 检测用户手动修改导致的冲突
|
||
|
||
#### 4.1.3 渲染 UI 层
|
||
- [ ] **内联装饰器**
|
||
- 新增行:绿色背景 + 左侧 ✓ 按钮
|
||
- 删除行:红色背景 + 删除线
|
||
- 修改行:黄色背景高亮
|
||
- [ ] **CodeLens**
|
||
- 单块 Accept 按钮
|
||
- 单块 Reject 按钮
|
||
- 整块 Accept All / Reject All
|
||
- [ ] **Diff 面板**(多文件场景)
|
||
- 左右分栏对比视图
|
||
- 文件树导航
|
||
- 全局操作按钮
|
||
|
||
#### 4.1.4 提交/回写事务层
|
||
- [ ] **Accept 流程**
|
||
- 校验无冲突
|
||
- 原子事务写入文件
|
||
- 触发 LSP 重新解析
|
||
- 清理临时层和 UI
|
||
- 记录变更日志
|
||
- [ ] **Reject 流程**
|
||
- 丢弃临时快照
|
||
- 清理装饰 UI
|
||
- 恢复原始视图
|
||
- 记录拒绝历史
|
||
|
||
### 4.2 辅助功能
|
||
|
||
#### 4.2.1 Claude Hooks 集成
|
||
- [ ] 监听 `after_code_edit` 事件
|
||
- [ ] 监听 `after_write_file` 事件
|
||
- [ ] 解析 Hook 传入的 Patch 数据
|
||
- [ ] 自动触发 Diff 预览流程
|
||
|
||
#### 4.2.2 快捷键支持
|
||
- [ ] `Tab` / `Enter`: 接受当前块
|
||
- [ ] `Esc`: 拒绝本次所有修改
|
||
- [ ] `Ctrl+Shift+A`: Accept All
|
||
- [ ] `Ctrl+Shift+R`: Reject All
|
||
|
||
#### 4.2.3 状态管理
|
||
- [ ] 状态栏显示当前 Diff 状态
|
||
- [ ] 显示待处理变更数量
|
||
- [ ] 提供快速操作入口
|
||
|
||
---
|
||
|
||
## 五、实现流程
|
||
|
||
### 5.1 阶段一:基础框架搭建 (Week 1)
|
||
|
||
```
|
||
Day 1-2: 项目初始化
|
||
├── 使用 yo code 脚手架创建项目
|
||
├── 配置 TypeScript、ESLint、Prettier
|
||
├── 配置 esbuild 构建流程
|
||
└── 设置测试环境
|
||
|
||
Day 3-5: 核心模型定义
|
||
├── 定义 TypeScript 接口和类型
|
||
├── 实现快照管理器基础结构
|
||
├── 实现 Diff 引擎基础封装
|
||
└── 编写单元测试
|
||
```
|
||
|
||
### 5.2 阶段二:Diff 引擎实现 (Week 2)
|
||
|
||
```
|
||
Day 1-3: Myers 算法集成
|
||
├── 集成 diff 库
|
||
├── 实现行级差异计算
|
||
├── 实现变更块切割逻辑
|
||
└── 测试各种代码变更场景
|
||
|
||
Day 4-5: 冲突检测
|
||
├── 实现 Base 与当前文件对比
|
||
├── 检测用户手动修改
|
||
├── 标记冲突变更块
|
||
└── 测试冲突场景
|
||
```
|
||
|
||
### 5.3 阶段三:UI 渲染层 (Week 3)
|
||
|
||
```
|
||
Day 1-3: 内联装饰器
|
||
├── 实现 DecorationType 定义
|
||
├── 实现行高亮渲染
|
||
├── 实现 Accept/Reject 按钮
|
||
└── 测试不同 Diff 场景
|
||
|
||
Day 4-5: CodeLens 集成
|
||
├── 实现 CodeLens Provider
|
||
├── 绑定 Accept/Reject 命令
|
||
├── 处理多块场景
|
||
└── 测试交互流程
|
||
```
|
||
|
||
### 5.4 阶段四:事务处理层 (Week 4)
|
||
|
||
```
|
||
Day 1-3: Accept 流程
|
||
├── 实现冲突校验
|
||
├── 实现原子化文件写入
|
||
├── 触发 LSP 刷新
|
||
├── 清理临时层和 UI
|
||
└── 测试 Accept 全流程
|
||
|
||
Day 4-5: Reject 流程
|
||
├── 实现快照丢弃
|
||
├── 实现 UI 清理
|
||
├── 实现视图恢复
|
||
└── 测试 Reject 全流程
|
||
```
|
||
|
||
### 5.5 阶段五:Hooks 集成与优化 (Week 5)
|
||
|
||
```
|
||
Day 1-3: Claude Hooks 集成
|
||
├── 实现 Hook 监听器
|
||
├── 解析 Patch 数据格式
|
||
├── 自动触发 Diff 预览
|
||
└── 测试端到端流程
|
||
|
||
Day 4-5: 优化与完善
|
||
├── 性能优化(大文件处理)
|
||
├── 快捷键绑定
|
||
├── 状态栏集成
|
||
├── 文档编写
|
||
└── 发布准备
|
||
```
|
||
|
||
---
|
||
|
||
## 六、关键技术实现
|
||
|
||
### 6.1 快照管理器
|
||
|
||
```typescript
|
||
// snapshotManager.ts
|
||
interface Snapshot {
|
||
id: string;
|
||
filePath: string;
|
||
baseContent: string;
|
||
suggestContent: string;
|
||
timestamp: number;
|
||
chunks: DiffChunk[];
|
||
}
|
||
|
||
class SnapshotManager {
|
||
private snapshots: Map<string, Snapshot> = new Map();
|
||
|
||
// 创建 Base 快照
|
||
async createBaseSnapshot(filePath: string): Promise<string> {
|
||
const content = await fs.readFile(filePath, 'utf-8');
|
||
const id = uuid();
|
||
this.snapshots.set(id, {
|
||
id,
|
||
filePath,
|
||
baseContent: content,
|
||
suggestContent: '',
|
||
timestamp: Date.now(),
|
||
chunks: []
|
||
});
|
||
return id;
|
||
}
|
||
|
||
// 存储 AI 生成的 Patch
|
||
async applyPatch(snapshotId: string, patch: string): Promise<void> {
|
||
const snapshot = this.snapshots.get(snapshotId);
|
||
if (!snapshot) throw new Error('Snapshot not found');
|
||
snapshot.suggestContent = patch;
|
||
snapshot.chunks = this.diffEngine.computeChunks(
|
||
snapshot.baseContent,
|
||
patch
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.2 Diff 引擎
|
||
|
||
```typescript
|
||
// diffEngine.ts
|
||
import { diffLines, Change } from 'diff';
|
||
|
||
interface DiffChunk {
|
||
id: string;
|
||
startLine: number;
|
||
endLine: number;
|
||
type: 'add' | 'delete' | 'modify';
|
||
content: string;
|
||
accepted?: boolean;
|
||
}
|
||
|
||
class DiffEngine {
|
||
computeChunks(base: string, suggest: string): DiffChunk[] {
|
||
const changes = diffLines(base, suggest);
|
||
const chunks: DiffChunk[] = [];
|
||
let lineOffset = 0;
|
||
|
||
changes.forEach((change, index) => {
|
||
if (change.added || change.removed) {
|
||
chunks.push({
|
||
id: `chunk-${index}`,
|
||
startLine: lineOffset,
|
||
endLine: lineOffset + (change.count || 0),
|
||
type: change.added ? 'add' : 'delete',
|
||
content: change.value
|
||
});
|
||
}
|
||
if (!change.removed) {
|
||
lineOffset += change.count || 0;
|
||
}
|
||
});
|
||
|
||
return chunks;
|
||
}
|
||
|
||
detectConflicts(base: string, current: string, suggest: string): DiffChunk[] {
|
||
// 检测用户手动修改与 AI 建议的冲突
|
||
const baseToCurrent = this.computeChunks(base, current);
|
||
const baseToSuggest = this.computeChunks(base, suggest);
|
||
|
||
return baseToSuggest.filter(suggestChunk => {
|
||
return baseToCurrent.some(currentChunk =>
|
||
this.isOverlapping(suggestChunk, currentChunk)
|
||
);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.3 内联装饰器
|
||
|
||
```typescript
|
||
// inlineDecorator.ts
|
||
import * as vscode from 'vscode';
|
||
|
||
class InlineDecorator {
|
||
private addDecorationType: vscode.TextEditorDecorationType;
|
||
private deleteDecorationType: vscode.TextEditorDecorationType;
|
||
private modifyDecorationType: vscode.TextEditorDecorationType;
|
||
|
||
constructor() {
|
||
this.addDecorationType = vscode.window.createTextEditorDecorationType({
|
||
backgroundColor: 'rgba(46, 160, 67, 0.15)',
|
||
isWholeLine: true,
|
||
gutterIconPath: new vscode.ThemeIcon('check').id,
|
||
gutterIconSize: 'contain'
|
||
});
|
||
|
||
this.deleteDecorationType = vscode.window.createTextEditorDecorationType({
|
||
backgroundColor: 'rgba(248, 81, 73, 0.15)',
|
||
isWholeLine: true,
|
||
textDecoration: 'line-through'
|
||
});
|
||
|
||
this.modifyDecorationType = vscode.window.createTextEditorDecorationType({
|
||
backgroundColor: 'rgba(210, 153, 34, 0.15)',
|
||
isWholeLine: true
|
||
});
|
||
}
|
||
|
||
renderChunks(editor: vscode.TextEditor, chunks: DiffChunk[]) {
|
||
const addRanges: vscode.Range[] = [];
|
||
const deleteRanges: vscode.Range[] = [];
|
||
const modifyRanges: vscode.Range[] = [];
|
||
|
||
chunks.forEach(chunk => {
|
||
const range = new vscode.Range(
|
||
new vscode.Position(chunk.startLine, 0),
|
||
new vscode.Position(chunk.endLine, 0)
|
||
);
|
||
|
||
switch (chunk.type) {
|
||
case 'add':
|
||
addRanges.push(range);
|
||
break;
|
||
case 'delete':
|
||
deleteRanges.push(range);
|
||
break;
|
||
case 'modify':
|
||
modifyRanges.push(range);
|
||
break;
|
||
}
|
||
});
|
||
|
||
editor.setDecorations(this.addDecorationType, addRanges);
|
||
editor.setDecorations(this.deleteDecorationType, deleteRanges);
|
||
editor.setDecorations(this.modifyDecorationType, modifyRanges);
|
||
}
|
||
|
||
clearDecorations(editor: vscode.TextEditor) {
|
||
editor.setDecorations(this.addDecorationType, []);
|
||
editor.setDecorations(this.deleteDecorationType, []);
|
||
editor.setDecorations(this.modifyDecorationType, []);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.4 Claude Hooks 处理器
|
||
|
||
```typescript
|
||
// hookHandler.ts
|
||
interface ClaudeHookEvent {
|
||
type: 'after_code_edit' | 'after_write_file';
|
||
filePath: string;
|
||
patch: string;
|
||
timestamp: number;
|
||
}
|
||
|
||
class HookHandler {
|
||
private snapshotManager: SnapshotManager;
|
||
private diffEngine: DiffEngine;
|
||
private decorator: InlineDecorator;
|
||
|
||
async handleHook(event: ClaudeHookEvent): Promise<void> {
|
||
// 1. 创建 Base 快照
|
||
const snapshotId = await this.snapshotManager.createBaseSnapshot(
|
||
event.filePath
|
||
);
|
||
|
||
// 2. 应用 AI Patch
|
||
await this.snapshotManager.applyPatch(snapshotId, event.patch);
|
||
|
||
// 3. 计算 Diff
|
||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||
const chunks = this.diffEngine.computeChunks(
|
||
snapshot.baseContent,
|
||
snapshot.suggestContent
|
||
);
|
||
|
||
// 4. 渲染 Diff UI
|
||
const editor = vscode.window.activeTextEditor;
|
||
if (editor && editor.document.uri.fsPath === event.filePath) {
|
||
this.decorator.renderChunks(editor, chunks);
|
||
this.showDiffPanel(snapshot);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、配置与扩展点
|
||
|
||
### 7.1 插件配置项
|
||
|
||
```json
|
||
{
|
||
"aiDiffPreview.enableAutoTrigger": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "启用 Claude Hooks 自动触发"
|
||
},
|
||
"aiDiffPreview.showInlineButtons": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "显示内联 Accept/Reject 按钮"
|
||
},
|
||
"aiDiffPreview.autoLint": {
|
||
"type": "boolean",
|
||
"default": true,
|
||
"description": "Diff 预览时自动运行 Lint 检查"
|
||
},
|
||
"aiDiffPreview.maxFileSize": {
|
||
"type": "number",
|
||
"default": 100000,
|
||
"description": "最大处理文件大小(字符数)"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 命令注册
|
||
|
||
```json
|
||
{
|
||
"contributes": {
|
||
"commands": [
|
||
{
|
||
"command": "aiDiffPreview.acceptChunk",
|
||
"title": "AI Diff: 接受当前变更块"
|
||
},
|
||
{
|
||
"command": "aiDiffPreview.rejectChunk",
|
||
"title": "AI Diff: 拒绝当前变更块"
|
||
},
|
||
{
|
||
"command": "aiDiffPreview.acceptAll",
|
||
"title": "AI Diff: 接受所有变更"
|
||
},
|
||
{
|
||
"command": "aiDiffPreview.rejectAll",
|
||
"title": "AI Diff: 拒绝所有变更"
|
||
},
|
||
{
|
||
"command": "aiDiffPreview.showDiffPanel",
|
||
"title": "AI Diff: 显示 Diff 面板"
|
||
}
|
||
],
|
||
"keybindings": [
|
||
{
|
||
"command": "aiDiffPreview.acceptChunk",
|
||
"key": "tab",
|
||
"when": "aiDiffPreview.isActive"
|
||
},
|
||
{
|
||
"command": "aiDiffPreview.rejectAll",
|
||
"key": "escape",
|
||
"when": "aiDiffPreview.isActive"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 八、测试策略
|
||
|
||
### 8.1 单元测试
|
||
|
||
- 快照管理器:创建、更新、删除快照
|
||
- Diff 引擎:各种代码变更场景
|
||
- 冲突检测:用户手动修改检测
|
||
|
||
### 8.2 集成测试
|
||
|
||
- 完整 Accept/Reject 流程
|
||
- 多文件 Diff 预览
|
||
- Claude Hooks 触发测试
|
||
|
||
### 8.3 E2E 测试
|
||
|
||
- 使用 @vscode/test-electron
|
||
- 模拟用户交互
|
||
- 验证 UI 渲染效果
|
||
|
||
---
|
||
|
||
## 九、发布与维护
|
||
|
||
### 9.1 发布清单
|
||
|
||
- [ ] 完善 README 文档
|
||
- [ ] 添加 CHANGELOG
|
||
- [ ] 配置 GitHub Actions CI/CD
|
||
- [ ] 发布到 VSCode Marketplace
|
||
- [ ] 收集用户反馈
|
||
|
||
### 9.2 后续迭代
|
||
|
||
- 支持更多 AI 模型(GPT-4、Gemini 等)
|
||
- 支持 Git 集成(生成 Commit 建议)
|
||
- 支持团队协作(共享 Diff 配置)
|
||
- 性能优化(大文件分块处理)
|
||
|
||
---
|
||
|
||
## 十、参考资料
|
||
|
||
- [VSCode Extension API](https://code.visualstudio.com/api)
|
||
- [Myers Diff Algorithm](https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/)
|
||
- [diff 库文档](https://github.com/kpdecker/jsdiff)
|
||
- [Cursor 官方文档](https://cursor.sh/docs)
|