# 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 = new Map(); // 创建 Base 快照 async createBaseSnapshot(filePath: string): Promise { 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 { 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 { // 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)