Files
ai-diff-preview/PROJECT_INIT.md
T

625 lines
19 KiB
Markdown
Raw Normal View History

# 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)