mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-10-01 22:01:51 +08:00
refactor: 架构变更:Webview → VSCode 内置 Diff + 浮动标签
This commit is contained in:
@@ -2,6 +2,29 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.2.0] - 2026-06-18
|
||||
|
||||
### Added
|
||||
- **VSCode 内置 Diff 编辑器**:不再使用自定义 Webview,直接调用 `vscode.diff` 展示差异
|
||||
- **EditRecord 粒度**:每次 Edit/Write 创建独立 EditRecord,支持逐编辑 Accept/Reject
|
||||
- **QuickPick 文件列表**:Stop 后弹出原生 QuickPick 文件列表,点击即打开内置 Diff
|
||||
- **导航快捷键**:`Alt+↑/↓` 在变更文件间跳转
|
||||
- **配置项**:`autoShowDiffPerEdit`(每次编辑自动弹 Diff)、`showAllDiffsOnStop`(Stop 后弹文件列表)、`floatingLabelMode`(浮动标签位置)
|
||||
- **新命令**:`showCurrentFileDiff`、`acceptCurrentEdit`、`rejectCurrentEdit`、`nextDiff`、`prevDiff`
|
||||
- **新快捷键**:`Alt+↑/↓`、`Ctrl+Shift+D`、`Ctrl+Shift+F`
|
||||
|
||||
### Fixed
|
||||
- **Bug Fix**: Accept 后同文件再编辑不再触发 Accept/Reject 选项,且 Diff 合并的问题
|
||||
- 根因:`recordEdit()` 对同文件多次编辑更新同一个 `FileChange` 对象
|
||||
- 修复:已 accepted/rejected 的 `FileChange` 后再次编辑时自动创建新 `FileChange`,清除 `originalContents` 缓存
|
||||
|
||||
### Changed
|
||||
- `ChangeSetManager` 重构为 `EditRecord[]` 独立记录模式
|
||||
- `FileChange` 变为聚合视图(持有 `edits: EditRecord[]`)
|
||||
- `HookHandler` 支持 `autoShowDiffPerEdit` / `showAllDiffsOnStop` 配置分支
|
||||
- `ReviewPanel` Webview 降级为次要选项(保留兼容)
|
||||
- 版本号升级至 `0.2.0`
|
||||
|
||||
## [0.1.0] - 2026-06-17
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,74 +1,70 @@
|
||||
# AI Code Diff Preview
|
||||
|
||||
类 Cursor 的 AI 代码差异预览 VSCode 插件,支持 Accept/Reject 操作。
|
||||
类 Cursor 的 AI 代码差异预览 VSCode 插件,使用 **VSCode 内置 Diff 编辑器** + **浮动 Accept/Reject 标签**。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ **临时变更隔离** - AI 生成的代码不直接覆盖文件
|
||||
- ✅ **实时 Diff 预览** - 红绿高亮显示删除/新增内容
|
||||
- ✅ **逐行 Accept/Reject** - 支持单块、整块操作
|
||||
- ✅ **冲突检测** - 自动检测用户手动修改导致的冲突
|
||||
- ✅ **Claude Hooks 集成** - 自动触发 Diff 预览
|
||||
- ✅ **状态栏显示** - 实时显示变更状态
|
||||
- ✅ **VSCode 原生 Diff 编辑器** — 使用内置 `vscode.diff`,不自定义 Webview
|
||||
- ✅ **EditRecord 独立追踪** — 每次 Edit/Write 独立记录,不合并
|
||||
- ✅ **逐次编辑 Accept/Reject** — 细粒度控制每次 AI 修改
|
||||
- ✅ **QuickPick 文件列表** — 对话结束后弹出原生文件选择器
|
||||
- ✅ **快捷键导航** — `Alt+↑/↓` 跳转变更文件
|
||||
- ✅ **Bug 修复** — Accept 后再编辑不再丢失追踪
|
||||
- ✅ **冲突检测** — 自动检测用户手动修改导致的冲突
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
Claude Edit/Write → 收集到 ChangeSet (EditRecord 粒度)
|
||||
├── autoShowDiffPerEdit=true → 立即弹出 VSCode 内置 Diff
|
||||
└── autoShowDiffPerEdit=false → 静默收集,更新状态栏
|
||||
|
||||
Claude Stop → 变更集就绪
|
||||
├── showAllDiffsOnStop=true → QuickPick 文件列表 → 点击打开内置 Diff
|
||||
└── showAllDiffsOnStop=false → 仅通知,用户手动 Ctrl+Shift+D 查看
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/your-username/ai-diff-preview.git
|
||||
|
||||
# 安装依赖
|
||||
cd ai-diff-preview
|
||||
npm install
|
||||
|
||||
# 编译
|
||||
npm run build
|
||||
|
||||
# 打包
|
||||
npm run package
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 配置 Claude Hooks
|
||||
### 快捷键
|
||||
|
||||
在 Claude 配置文件中添加 Hook:
|
||||
| 快捷键 | 条件 | 功能 |
|
||||
|--------|------|------|
|
||||
| `Alt+↓` | `aiDiffPreview.isActive` | 下一个变更文件 |
|
||||
| `Alt+↑` | `aiDiffPreview.isActive` | 上一个变更文件 |
|
||||
| `Tab` | `aiDiffPreview.isActive` | 接受当前编辑 |
|
||||
| `Esc` | `aiDiffPreview.isActive` | 拒绝所有变更 |
|
||||
| `Ctrl+Shift+A` | `aiDiffPreview.isActive` | Accept All |
|
||||
| `Ctrl+Shift+R` | `aiDiffPreview.isActive` | Reject All |
|
||||
| `Ctrl+Shift+D` | `aiDiffPreview.isActive` | 显示文件列表 |
|
||||
| `Ctrl+Shift+F` | `aiDiffPreview.isActive` | 查看当前文件 Diff |
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"after_code_edit": {
|
||||
"command": "code",
|
||||
"args": ["--command", "aiDiffPreview.handleHook"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### 配置项
|
||||
|
||||
### 2. 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Tab` / `Enter` | 接受当前块 |
|
||||
| `Esc` | 拒绝所有变更 |
|
||||
| `Ctrl+Shift+A` | Accept All |
|
||||
| `Ctrl+Shift+R` | Reject All |
|
||||
|
||||
### 3. CodeLens 按钮
|
||||
|
||||
在代码上方会出现 Accept/Reject 按钮,点击即可操作。
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `aiDiffPreview.enableAutoTrigger` | boolean | true | 启用自动触发 |
|
||||
| `aiDiffPreview.autoShowDiffPerEdit` | boolean | false | 每次编辑自动弹 Diff |
|
||||
| `aiDiffPreview.showAllDiffsOnStop` | boolean | true | Stop 后弹文件列表 |
|
||||
| `aiDiffPreview.floatingLabelMode` | string | "statusBar" | 浮动标签位置 |
|
||||
| `aiDiffPreview.maxFileSize` | number | 100000 | 最大文件大小 |
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 监听模式
|
||||
npm run watch
|
||||
|
||||
# 运行测试
|
||||
npm run test
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
npm run watch # 监听模式
|
||||
npm run test # 运行测试 (13 tests)
|
||||
npm run lint # 代码检查
|
||||
npm run package # 打包 vsix
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
@@ -77,12 +73,14 @@ npm run lint
|
||||
src/
|
||||
├── extension.ts # 插件入口
|
||||
├── trigger/
|
||||
│ └── hookHandler.ts # Claude Hook 处理器
|
||||
│ └── hookHandler.ts # Hook 处理器 + QuickPick 文件列表
|
||||
├── snapshot/
|
||||
│ └── snapshotManager.ts # 快照管理器
|
||||
│ └── snapshotManager.ts # 变更集管理器 (ChangeSetManager)
|
||||
├── diff/
|
||||
│ └── diffEngine.ts # Diff 计算引擎
|
||||
├── render/
|
||||
│ ├── diffViewer.ts # ★ VSCode 内置 Diff 查看器
|
||||
│ ├── reviewPanel.ts # Webview 面板 (次要)
|
||||
│ ├── inlineDecorator.ts # 内联装饰器
|
||||
│ ├── codeLensProvider.ts # CodeLens 提供器
|
||||
│ └── statusBar.ts # 状态栏
|
||||
@@ -94,15 +92,6 @@ src/
|
||||
└── constants.ts # 常量定义
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `aiDiffPreview.enableAutoTrigger` | boolean | true | 启用自动触发 |
|
||||
| `aiDiffPreview.showInlineButtons` | boolean | true | 显示内联按钮 |
|
||||
| `aiDiffPreview.autoLint` | boolean | true | 自动运行 Lint |
|
||||
| `aiDiffPreview.maxFileSize` | number | 100000 | 最大文件大小 |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
+68
-19
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "ai-diff-preview",
|
||||
"displayName": "AI Code Diff Preview",
|
||||
"description": "类 Cursor 的 AI 代码差异预览插件,支持 Accept/Reject 操作",
|
||||
"version": "0.1.0",
|
||||
"description": "类 Cursor 的 AI 代码差异预览插件,使用 VSCode 内置 Diff 编辑器 + 浮动 Accept/Reject 标签",
|
||||
"version": "0.2.0",
|
||||
"publisher": "your-publisher-name",
|
||||
"engines": {
|
||||
"vscode": "^1.85.0"
|
||||
@@ -17,13 +17,23 @@
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "aiDiffPreview.acceptChunk",
|
||||
"title": "AI Diff: 接受当前变更块",
|
||||
"command": "aiDiffPreview.showDiffPanel",
|
||||
"title": "AI Diff: 显示所有变更文件列表",
|
||||
"icon": "$(list-tree)"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.showCurrentFileDiff",
|
||||
"title": "AI Diff: 查看当前文件 Diff",
|
||||
"icon": "$(diff)"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.acceptCurrentEdit",
|
||||
"title": "AI Diff: 接受当前编辑",
|
||||
"icon": "$(check)"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.rejectChunk",
|
||||
"title": "AI Diff: 拒绝当前变更块",
|
||||
"command": "aiDiffPreview.rejectCurrentEdit",
|
||||
"title": "AI Diff: 拒绝当前编辑",
|
||||
"icon": "$(close)"
|
||||
},
|
||||
{
|
||||
@@ -37,21 +47,24 @@
|
||||
"icon": "$(trash)"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.showDiffPanel",
|
||||
"title": "AI Diff: 显示 Diff 面板",
|
||||
"icon": "$(diff)"
|
||||
"command": "aiDiffPreview.nextDiff",
|
||||
"title": "AI Diff: 下一个变更"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.prevDiff",
|
||||
"title": "AI Diff: 上一个变更"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "aiDiffPreview.acceptChunk",
|
||||
"command": "aiDiffPreview.acceptCurrentEdit",
|
||||
"key": "tab",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
"when": "aiDiffPreview.isActive && editorTextFocus"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.rejectAll",
|
||||
"key": "escape",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
"when": "aiDiffPreview.isActive && editorTextFocus"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.acceptAll",
|
||||
@@ -64,6 +77,30 @@
|
||||
"key": "ctrl+shift+r",
|
||||
"mac": "cmd+shift+r",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.nextDiff",
|
||||
"key": "alt+down",
|
||||
"mac": "alt+down",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.prevDiff",
|
||||
"key": "alt+up",
|
||||
"mac": "alt+up",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.showDiffPanel",
|
||||
"key": "ctrl+shift+d",
|
||||
"mac": "cmd+shift+d",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
},
|
||||
{
|
||||
"command": "aiDiffPreview.showCurrentFileDiff",
|
||||
"key": "ctrl+shift+f",
|
||||
"mac": "cmd+shift+f",
|
||||
"when": "aiDiffPreview.isActive"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
@@ -72,17 +109,29 @@
|
||||
"aiDiffPreview.enableAutoTrigger": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "启用 Claude Hooks 自动触发"
|
||||
"description": "启用自动触发(监听 pending.json 文件变更)"
|
||||
},
|
||||
"aiDiffPreview.showInlineButtons": {
|
||||
"aiDiffPreview.autoShowDiffPerEdit": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "每次 Edit/Write 后自动弹出 VSCode 内置 Diff 编辑器。关闭时静默收集变更,等待 Stop 后统一展示"
|
||||
},
|
||||
"aiDiffPreview.showAllDiffsOnStop": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "显示内联 Accept/Reject 按钮"
|
||||
"description": "对话结束后自动弹出 QuickPick 文件列表,展示所有变更文件。关闭时仅显示状态栏通知"
|
||||
},
|
||||
"aiDiffPreview.autoLint": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Diff 预览时自动运行 Lint 检查"
|
||||
"aiDiffPreview.floatingLabelMode": {
|
||||
"type": "string",
|
||||
"default": "statusBar",
|
||||
"enum": ["inline", "statusBar", "both", "none"],
|
||||
"enumDescriptions": [
|
||||
"编辑器行内 Accept/Reject 标签(通过 CodeLens 显示)",
|
||||
"状态栏底部固定 Accept/Reject 按钮",
|
||||
"两者都显示",
|
||||
"不显示浮动标签"
|
||||
],
|
||||
"description": "Accept/Reject 浮动标签显示位置"
|
||||
},
|
||||
"aiDiffPreview.maxFileSize": {
|
||||
"type": "number",
|
||||
|
||||
+163
-6
@@ -2,10 +2,16 @@
|
||||
* AI Code Diff Preview - 插件入口
|
||||
*
|
||||
* 工作流程:
|
||||
* 1. PostToolUse (Edit/Write) → 收集变更到 ChangeSet
|
||||
* 2. Stop (对话结束) → 弹出通知
|
||||
* 3. 用户点击"查看更改" → 打开 Review 双栏面板
|
||||
* 1. PostToolUse (Edit/Write) → 收集变更到 ChangeSet (EditRecord 粒度)
|
||||
* 2. Stop (对话结束) → 根据配置弹出 QuickPick 文件列表或通知
|
||||
* 3. 用户选择文件 → 打开 VSCode 内置 Diff 编辑器
|
||||
* 4. 用户 Accept/Reject → 应用或丢弃变更
|
||||
*
|
||||
* 快捷键:
|
||||
* - Alt+↑/↓: 上一个/下一个变更
|
||||
* - Tab: 接受当前编辑
|
||||
* - Esc: 拒绝当前编辑
|
||||
* - Ctrl+Shift+D: 显示文件列表
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
@@ -13,12 +19,14 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { ChangeSetManager } from './snapshot/snapshotManager';
|
||||
import { HookHandler } from './trigger/hookHandler';
|
||||
import { DiffViewer } from './render/diffViewer';
|
||||
import { ReviewPanel } from './render/reviewPanel';
|
||||
import { COMMANDS } from './models/constants';
|
||||
import { ClaudeHookEvent } from './models/types';
|
||||
|
||||
let changeSetManager: ChangeSetManager;
|
||||
let hookHandler: HookHandler;
|
||||
let diffViewer: DiffViewer;
|
||||
let reviewPanel: ReviewPanel;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
@@ -27,6 +35,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
// 初始化核心模块
|
||||
changeSetManager = new ChangeSetManager();
|
||||
hookHandler = new HookHandler(changeSetManager);
|
||||
diffViewer = hookHandler.getDiffViewer();
|
||||
reviewPanel = new ReviewPanel(changeSetManager);
|
||||
|
||||
// 注册命令
|
||||
@@ -40,12 +49,160 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
function registerCommands(context: vscode.ExtensionContext) {
|
||||
// 显示 Review 面板
|
||||
// === Diff 查看 ===
|
||||
|
||||
// 显示文件列表 (QuickPick)
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.SHOW_DIFF_PANEL, () => {
|
||||
reviewPanel.show();
|
||||
hookHandler.showFileListPicker();
|
||||
})
|
||||
);
|
||||
|
||||
// 显示当前活动文件的 Diff
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.SHOW_CURRENT_FILE_DIFF, () => {
|
||||
diffViewer.showCurrentFileDiff();
|
||||
})
|
||||
);
|
||||
|
||||
// === Accept / Reject ===
|
||||
|
||||
// 接受当前编辑(光标所在位置的编辑)
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.ACCEPT_CURRENT_EDIT, () => {
|
||||
const result = findCurrentEditAtCursor();
|
||||
if (!result) {
|
||||
vscode.window.showInformationMessage('AI Diff: 光标不在变更区域内');
|
||||
return;
|
||||
}
|
||||
changeSetManager.acceptEditRecord(result.fileChange.id, result.edit.id);
|
||||
vscode.window.showInformationMessage(`AI Diff: 已接受编辑 #${result.edit.id.slice(0, 6)}`);
|
||||
})
|
||||
);
|
||||
|
||||
// 拒绝当前编辑
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.REJECT_CURRENT_EDIT, () => {
|
||||
const result = findCurrentEditAtCursor();
|
||||
if (!result) {
|
||||
vscode.window.showInformationMessage('AI Diff: 光标不在变更区域内');
|
||||
return;
|
||||
}
|
||||
changeSetManager.rejectEditRecord(result.fileChange.id, result.edit.id);
|
||||
vscode.window.showInformationMessage(`AI Diff: 已拒绝编辑 #${result.edit.id.slice(0, 6)}`);
|
||||
})
|
||||
);
|
||||
|
||||
// 接受全部
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.ACCEPT_ALL, () => {
|
||||
const count = changeSetManager.acceptAll();
|
||||
vscode.window.showInformationMessage(`AI Diff: 已接受 ${count} 个文件的变更`);
|
||||
})
|
||||
);
|
||||
|
||||
// 拒绝全部
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.REJECT_ALL, () => {
|
||||
const count = changeSetManager.rejectAll();
|
||||
vscode.window.showInformationMessage(`AI Diff: 已拒绝 ${count} 个文件的变更`);
|
||||
})
|
||||
);
|
||||
|
||||
// === 导航 ===
|
||||
|
||||
// 下一个变更
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.NEXT_DIFF, () => {
|
||||
navigateDiff(1);
|
||||
})
|
||||
);
|
||||
|
||||
// 上一个变更
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.PREV_DIFF, () => {
|
||||
navigateDiff(-1);
|
||||
})
|
||||
);
|
||||
|
||||
// 旧版兼容命令
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.ACCEPT_CHUNK, () => {
|
||||
vscode.commands.executeCommand(COMMANDS.ACCEPT_CURRENT_EDIT);
|
||||
})
|
||||
);
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.REJECT_CHUNK, () => {
|
||||
vscode.commands.executeCommand(COMMANDS.REJECT_CURRENT_EDIT);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找光标所在位置的编辑记录
|
||||
*/
|
||||
function findCurrentEditAtCursor(): { fileChange: any; edit: any } | null {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return null;
|
||||
|
||||
const set = changeSetManager.getCurrentSet();
|
||||
if (!set) return null;
|
||||
|
||||
const normalizedPath = path.normalize(editor.document.uri.fsPath).replace(/\\/g, '/');
|
||||
const fileChange = set.changes.find(
|
||||
c => path.normalize(c.filePath).replace(/\\/g, '/') === normalizedPath && c.status === 'pending'
|
||||
);
|
||||
if (!fileChange) return null;
|
||||
|
||||
const cursorLine = editor.selection.active.line;
|
||||
|
||||
// 在文件聚合 diff 中查找光标所在行对应的 edit
|
||||
for (const edit of fileChange.edits) {
|
||||
if (edit.status !== 'pending') continue;
|
||||
// 找到该 edit 中匹配的行范围
|
||||
for (const d of edit.diffLines) {
|
||||
if (d.type === 'context' || d.type === 'add') {
|
||||
if (d.newLineNum - 1 === cursorLine) {
|
||||
return { fileChange, edit };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:返回第一个 pending edit
|
||||
const firstPending = fileChange.edits.find(e => e.status === 'pending');
|
||||
if (firstPending) {
|
||||
return { fileChange, edit: firstPending };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在变更文件之间导航
|
||||
*/
|
||||
function navigateDiff(direction: 1 | -1): void {
|
||||
const set = changeSetManager.getCurrentSet();
|
||||
if (!set) return;
|
||||
|
||||
const pending = set.changes.filter(c => c.status === 'pending');
|
||||
if (pending.length === 0) {
|
||||
vscode.window.showInformationMessage('AI Diff: 无待处理变更');
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const currentPath = editor
|
||||
? path.normalize(editor.document.uri.fsPath).replace(/\\/g, '/')
|
||||
: null;
|
||||
|
||||
// 找到当前文件在 pending 列表中的位置
|
||||
let idx = currentPath
|
||||
? pending.findIndex(c => path.normalize(c.filePath).replace(/\\/g, '/') === currentPath)
|
||||
: -1;
|
||||
|
||||
idx = idx === -1 ? 0 : (idx + direction + pending.length) % pending.length;
|
||||
diffViewer.showFileDiff(pending[idx]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +210,7 @@ function registerCommands(context: vscode.ExtensionContext) {
|
||||
*
|
||||
* 监听 .claude/hooks/pending.json:
|
||||
* - toolName: Edit/Write → 收集变更
|
||||
* - toolName: Stop → 弹出 Review 通知
|
||||
* - toolName: Stop → 弹出 Review 通知/文件列表
|
||||
*/
|
||||
function registerHookFileWatcher(context: vscode.ExtensionContext) {
|
||||
const hookDir = vscode.workspace.workspaceFolders?.[0]
|
||||
|
||||
@@ -16,6 +16,16 @@ export const COMMANDS = {
|
||||
ACCEPT_ALL: 'aiDiffPreview.acceptAll',
|
||||
REJECT_ALL: 'aiDiffPreview.rejectAll',
|
||||
SHOW_DIFF_PANEL: 'aiDiffPreview.showDiffPanel',
|
||||
/** 显示当前文件的 Diff */
|
||||
SHOW_CURRENT_FILE_DIFF: 'aiDiffPreview.showCurrentFileDiff',
|
||||
/** 接受光标所在位置的编辑 */
|
||||
ACCEPT_CURRENT_EDIT: 'aiDiffPreview.acceptCurrentEdit',
|
||||
/** 拒绝光标所在位置的编辑 */
|
||||
REJECT_CURRENT_EDIT: 'aiDiffPreview.rejectCurrentEdit',
|
||||
/** 跳转到下一个待处理变更 */
|
||||
NEXT_DIFF: 'aiDiffPreview.nextDiff',
|
||||
/** 跳转到上一个待处理变更 */
|
||||
PREV_DIFF: 'aiDiffPreview.prevDiff',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -27,6 +37,17 @@ export const CONTEXT_KEYS = {
|
||||
CHUNK_COUNT: 'aiDiffPreview.chunkCount',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 配置项键名
|
||||
*/
|
||||
export const CONFIG_KEYS = {
|
||||
ENABLE_AUTO_TRIGGER: 'enableAutoTrigger',
|
||||
AUTO_SHOW_DIFF_PER_EDIT: 'autoShowDiffPerEdit',
|
||||
SHOW_ALL_DIFFS_ON_STOP: 'showAllDiffsOnStop',
|
||||
FLOATING_LABEL_MODE: 'floatingLabelMode',
|
||||
MAX_FILE_SIZE: 'maxFileSize',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 装饰器类型 ID
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Diff 查看器 - 调用 VSCode 内置 Diff 编辑器
|
||||
*
|
||||
* 使用 vscode.diff 命令展示原始内容 vs 变更后内容,
|
||||
* 完全不使用自定义 Webview,保持 VSCode 原生交互体验。
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { EditRecord, FileChange } from '../models/types';
|
||||
import { ChangeSetManager } from '../snapshot/snapshotManager';
|
||||
|
||||
export class DiffViewer {
|
||||
constructor(private changeSetManager: ChangeSetManager) {}
|
||||
|
||||
/**
|
||||
* 打开单次编辑的 Diff(VSCode 内置 Diff 编辑器)
|
||||
*/
|
||||
async showEditDiff(editRecord: EditRecord): Promise<void> {
|
||||
const beforeDoc = await vscode.workspace.openTextDocument({
|
||||
content: editRecord.beforeContent,
|
||||
language: this.getLanguageId(editRecord.filePath),
|
||||
});
|
||||
const afterDoc = await vscode.workspace.openTextDocument({
|
||||
content: editRecord.afterContent,
|
||||
language: this.getLanguageId(editRecord.filePath),
|
||||
});
|
||||
|
||||
const fileName = path.basename(editRecord.filePath);
|
||||
const toolLabel = editRecord.toolName === 'Write' ? '写入' : '编辑';
|
||||
|
||||
await vscode.commands.executeCommand(
|
||||
'vscode.diff',
|
||||
beforeDoc.uri,
|
||||
afterDoc.uri,
|
||||
`${fileName} — AI ${toolLabel} #${editRecord.id.slice(0, 6)} (原始 ↔ 变更后)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开整个文件的聚合 Diff
|
||||
*/
|
||||
async showFileDiff(fileChange: FileChange): Promise<void> {
|
||||
const beforeDoc = await vscode.workspace.openTextDocument({
|
||||
content: fileChange.originalContent,
|
||||
language: this.getLanguageId(fileChange.filePath),
|
||||
});
|
||||
const afterDoc = await vscode.workspace.openTextDocument({
|
||||
content: fileChange.latestContent,
|
||||
language: this.getLanguageId(fileChange.filePath),
|
||||
});
|
||||
|
||||
const fileName = path.basename(fileChange.filePath);
|
||||
|
||||
await vscode.commands.executeCommand(
|
||||
'vscode.diff',
|
||||
beforeDoc.uri,
|
||||
afterDoc.uri,
|
||||
`${fileName} — 原始 ↔ 变更后 (${fileChange.edits.length} 次编辑)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开当前活动文件在编辑器中的 Diff
|
||||
*/
|
||||
async showCurrentFileDiff(): Promise<void> {
|
||||
const changeSet = this.changeSetManager.getCurrentSet();
|
||||
if (!changeSet) return;
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const normalizedPath = path.normalize(editor.document.uri.fsPath).replace(/\\/g, '/');
|
||||
const fileChange = changeSet.changes.find(
|
||||
c => path.normalize(c.filePath).replace(/\\/g, '/') === normalizedPath && c.status === 'pending'
|
||||
);
|
||||
if (!fileChange) {
|
||||
vscode.window.showInformationMessage('AI Diff: 当前文件无待处理变更');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showFileDiff(fileChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚焦文件到编辑器(跳转到该文件并定位到第一个变更行)
|
||||
*/
|
||||
async focusFileInEditor(fileChange: FileChange): Promise<void> {
|
||||
const uri = vscode.Uri.file(fileChange.filePath);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const editor = await vscode.window.showTextDocument(doc, { preview: false });
|
||||
|
||||
// 滚动到第一个变更行
|
||||
if (fileChange.diffLines.length > 0) {
|
||||
const firstChange = fileChange.diffLines.find(
|
||||
d => d.type === 'add' || d.type === 'delete'
|
||||
);
|
||||
if (firstChange) {
|
||||
const line = (firstChange.newLineNum || firstChange.oldLineNum) - 1;
|
||||
const pos = new vscode.Position(Math.max(0, line), 0);
|
||||
editor.selection = new vscode.Selection(pos, pos);
|
||||
editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguageId(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const map: Record<string, string> = {
|
||||
'.ts': 'typescript', '.tsx': 'typescriptreact',
|
||||
'.js': 'javascript', '.jsx': 'javascriptreact',
|
||||
'.json': 'json', '.md': 'markdown',
|
||||
'.css': 'css', '.scss': 'scss', '.less': 'less',
|
||||
'.html': 'html', '.htm': 'html',
|
||||
'.py': 'python', '.rs': 'rust', '.go': 'go',
|
||||
'.java': 'java', '.cpp': 'cpp', '.c': 'c',
|
||||
'.yaml': 'yaml', '.yml': 'yaml',
|
||||
'.xml': 'xml', '.sql': 'sql', '.sh': 'shell',
|
||||
};
|
||||
return map[ext] || 'plaintext';
|
||||
}
|
||||
}
|
||||
+104
-15
@@ -1,23 +1,41 @@
|
||||
/**
|
||||
* Hook 处理器
|
||||
*
|
||||
* PostToolUse (Edit/Write) → 收集变更,不显示 UI
|
||||
* Stop → 对话结束,弹出 Review 通知
|
||||
* PostToolUse (Edit/Write) → 收集变更
|
||||
* + autoShowDiffPerEdit=true → 立即弹出 VSCode 内置 Diff
|
||||
* + autoShowDiffPerEdit=false → 静默收集
|
||||
*
|
||||
* Stop → 对话结束
|
||||
* + showAllDiffsOnStop=true → 弹出 QuickPick 文件列表
|
||||
* + showAllDiffsOnStop=false → 仅状态栏通知
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { ChangeSetManager } from '../snapshot/snapshotManager';
|
||||
import { ClaudeHookEvent } from '../models/types';
|
||||
import { DiffViewer } from '../render/diffViewer';
|
||||
import { ClaudeHookEvent, FileChange } from '../models/types';
|
||||
import { CONFIG_SECTION, CONFIG_KEYS } from '../models/constants';
|
||||
|
||||
const SETTING_AUTO_SHOW_DIFF = `${CONFIG_SECTION}.${CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT}`;
|
||||
const SETTING_SHOW_ALL_ON_STOP = `${CONFIG_SECTION}.${CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP}`;
|
||||
|
||||
export class HookHandler {
|
||||
private diffViewer: DiffViewer;
|
||||
|
||||
constructor(
|
||||
private changeSetManager: ChangeSetManager
|
||||
) {}
|
||||
) {
|
||||
this.diffViewer = new DiffViewer(changeSetManager);
|
||||
}
|
||||
|
||||
getDiffViewer(): DiffViewer {
|
||||
return this.diffViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 PostToolUse 事件(Edit/Write)
|
||||
* 只收集变更,不弹 UI
|
||||
* 收集变更,根据配置决定是否自动弹 Diff
|
||||
*/
|
||||
handleEdit(event: ClaudeHookEvent): void {
|
||||
event.filePath = path.normalize(event.filePath);
|
||||
@@ -30,11 +48,29 @@ export class HookHandler {
|
||||
event.newString || '',
|
||||
event.content || ''
|
||||
);
|
||||
|
||||
// 根据配置决定是否自动弹 Diff
|
||||
const autoShow = vscode.workspace.getConfiguration(CONFIG_SECTION).get<boolean>(CONFIG_KEYS.AUTO_SHOW_DIFF_PER_EDIT, false);
|
||||
if (autoShow) {
|
||||
const set = this.changeSetManager.getCurrentSet();
|
||||
if (set) {
|
||||
const normalizedPath = path.normalize(event.filePath).replace(/\\/g, '/');
|
||||
const fileChange = set.changes.find(
|
||||
c => path.normalize(c.filePath).replace(/\\/g, '/') === normalizedPath
|
||||
);
|
||||
if (fileChange) {
|
||||
const lastEdit = fileChange.edits[fileChange.edits.length - 1];
|
||||
this.diffViewer.showEditDiff(lastEdit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新上下文键
|
||||
vscode.commands.executeCommand('setContext', 'aiDiffPreview.isActive', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Stop 事件(对话结束)
|
||||
* 弹出通知,提示用户查看变更
|
||||
*/
|
||||
handleStop(): void {
|
||||
const changeSet = this.changeSetManager.markReady();
|
||||
@@ -44,16 +80,69 @@ export class HookHandler {
|
||||
}
|
||||
|
||||
const count = changeSet.changes.length;
|
||||
console.log(`[AI Diff] 对话结束,${count} 个文件有变更`);
|
||||
const pendingCount = changeSet.changes.filter(c => c.status === 'pending').length;
|
||||
console.log(`[AI Diff] 对话结束,${count} 个文件有变更 (pending: ${pendingCount})`);
|
||||
|
||||
// 弹出通知,带"查看更改"按钮
|
||||
vscode.window.showInformationMessage(
|
||||
`AI Diff: ${count} 个文件已修改`,
|
||||
'查看更改'
|
||||
).then(selection => {
|
||||
if (selection === '查看更改') {
|
||||
vscode.commands.executeCommand('aiDiffPreview.showDiffPanel');
|
||||
}
|
||||
const showAll = vscode.workspace.getConfiguration(CONFIG_SECTION).get<boolean>(CONFIG_KEYS.SHOW_ALL_DIFFS_ON_STOP, true);
|
||||
|
||||
if (showAll && pendingCount > 0) {
|
||||
// 弹出 QuickPick 文件列表
|
||||
this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
|
||||
} else {
|
||||
// 仅状态栏通知
|
||||
const msg = pendingCount > 0
|
||||
? `AI Diff: ${pendingCount} 个文件有待处理变更`
|
||||
: `AI Diff: ${count} 个文件变更已完成`;
|
||||
vscode.window.showInformationMessage(msg, '查看变更').then(selection => {
|
||||
if (selection === '查看变更') {
|
||||
this.showFileListPicker(changeSet.changes.filter(c => c.status === 'pending'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出 QuickPick 文件列表
|
||||
*/
|
||||
async showFileListPicker(fileChanges?: FileChange[]): Promise<void> {
|
||||
let changes = fileChanges;
|
||||
if (!changes) {
|
||||
const set = this.changeSetManager.getCurrentSet();
|
||||
if (!set) return;
|
||||
changes = set.changes.filter(c => c.status === 'pending');
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
vscode.window.showInformationMessage('AI Diff: 无待处理变更');
|
||||
return;
|
||||
}
|
||||
|
||||
const items: vscode.QuickPickItem[] = changes.map(c => {
|
||||
const name = path.basename(c.filePath);
|
||||
const adds = c.diffLines.filter(d => d.type === 'add').length;
|
||||
const dels = c.diffLines.filter(d => d.type === 'delete').length;
|
||||
const typeMap: Record<string, string> = { modify: '$(edit)', create: '$(new-file)', delete: '$(trash)' };
|
||||
|
||||
return {
|
||||
label: `${typeMap[c.type] || '$(file)'} ${name}`,
|
||||
description: `+${adds} -${dels} · ${c.edits.length} 次编辑`,
|
||||
detail: c.filePath,
|
||||
// 存储 fileChange id 以便查找
|
||||
id: c.id,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await vscode.window.showQuickPick(items, {
|
||||
placeHolder: `AI Diff Review — ${changes.length} 个文件有变更`,
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
});
|
||||
|
||||
if (selected && selected.id) {
|
||||
const fileChange = changes.find(c => c.id === selected.id);
|
||||
if (fileChange) {
|
||||
await this.diffViewer.showFileDiff(fileChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user