mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-10-01 05:41:51 +08:00
feat: 逐次编辑导航 + 状态栏增强 + 依赖更新
- 新增 nextEdit/prevEdit/showEditAtIndex 命令支持逐次查看编辑 - DiffViewer 增加 editIndex/totalEdits 跟踪编辑进度 - StatusBar 增加编辑计数和逐次导航按钮 - SnapshotManager 改进 EditRecord 粒度的 accept/reject - HookHandler 支持多次编辑的二级选择(聚合/逐次) - 更新 package.json 依赖(esbuild win32-x64) - 将 .claude/hooks/ 加入 .gitignore - 删除测试.md
This commit is contained in:
@@ -26,6 +26,12 @@ export const COMMANDS = {
|
||||
NEXT_DIFF: 'aiDiffPreview.nextDiff',
|
||||
/** 跳转到上一个待处理变更 */
|
||||
PREV_DIFF: 'aiDiffPreview.prevDiff',
|
||||
/** ★ 显示下一次编辑 */
|
||||
NEXT_EDIT: 'aiDiffPreview.nextEdit',
|
||||
/** ★ 显示上一次编辑 */
|
||||
PREV_EDIT: 'aiDiffPreview.prevEdit',
|
||||
/** ★ 切换到逐次编辑查看模式 */
|
||||
SHOW_EDIT_AT_INDEX: 'aiDiffPreview.showEditAtIndex',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -46,6 +52,8 @@ export const CONFIG_KEYS = {
|
||||
SHOW_ALL_DIFFS_ON_STOP: 'showAllDiffsOnStop',
|
||||
FLOATING_LABEL_MODE: 'floatingLabelMode',
|
||||
MAX_FILE_SIZE: 'maxFileSize',
|
||||
/** ★ 空闲检测超时时间(毫秒),ESC 打断后自动触发 diff */
|
||||
IDLE_TIMEOUT: 'idleTimeout',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,10 @@ import { ChangeSetManager } from '../snapshot/snapshotManager';
|
||||
export interface DiffSession {
|
||||
fileId: string;
|
||||
editId?: string;
|
||||
/** ★ 当前查看的编辑索引(undefined 表示聚合视图) */
|
||||
editIndex?: number;
|
||||
/** ★ 当前文件的总编辑数 */
|
||||
totalEdits?: number;
|
||||
beforeTmpPath: string;
|
||||
afterTmpPath: string;
|
||||
beforeUri: vscode.Uri;
|
||||
@@ -65,7 +69,7 @@ export class DiffViewer {
|
||||
'vscode.diff',
|
||||
this.activeSession!.beforeUri,
|
||||
this.activeSession!.afterUri,
|
||||
`${fileName} — AI ${toolLabel} #${editRecord.id.slice(0, 6)} · 状态栏操作`
|
||||
`${fileName} — AI ${toolLabel} · 编辑 #${editRecord.id.slice(0, 6)}`
|
||||
);
|
||||
|
||||
this.isOpening = false;
|
||||
@@ -92,7 +96,7 @@ export class DiffViewer {
|
||||
'vscode.diff',
|
||||
this.activeSession!.beforeUri,
|
||||
this.activeSession!.afterUri,
|
||||
`${fileName} — ${fileChange.edits.length} 次 AI 编辑 · 状态栏操作`
|
||||
`${fileName} — 聚合视图 (${fileChange.edits.length} 次编辑)`
|
||||
);
|
||||
|
||||
this.isOpening = false;
|
||||
@@ -112,6 +116,63 @@ export class DiffViewer {
|
||||
else vscode.window.showInformationMessage('AI Diff: 当前文件无待处理变更');
|
||||
}
|
||||
|
||||
// ---- ★ 逐次编辑导航 ----
|
||||
|
||||
/** 显示文件的第 N 次编辑的 diff */
|
||||
async showEditAtIndex(fileChange: FileChange, editIndex: number): Promise<void> {
|
||||
if (editIndex < 0 || editIndex >= fileChange.edits.length) return;
|
||||
const edit = fileChange.edits[editIndex];
|
||||
await this.showEditDiff(edit, fileChange.id);
|
||||
// ★ 更新 session 的编辑索引信息
|
||||
if (this.activeSession) {
|
||||
this.activeSession.editIndex = editIndex;
|
||||
this.activeSession.totalEdits = fileChange.edits.length;
|
||||
}
|
||||
this.onDiffAction?.();
|
||||
}
|
||||
|
||||
/** 显示下一次编辑 */
|
||||
async showNextEdit(): Promise<void> {
|
||||
if (!this.activeSession) return;
|
||||
const { fileId, editIndex } = this.activeSession;
|
||||
if (editIndex === undefined) return;
|
||||
const fc = this.findFileChange(fileId);
|
||||
if (!fc) return;
|
||||
const nextIdx = editIndex + 1;
|
||||
if (nextIdx < fc.edits.length) {
|
||||
await this.showEditAtIndex(fc, nextIdx);
|
||||
} else {
|
||||
vscode.window.showInformationMessage(`AI Diff: 已是最后一次编辑 (${fc.edits.length}/${fc.edits.length})`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示上一次编辑 */
|
||||
async showPrevEdit(): Promise<void> {
|
||||
if (!this.activeSession) return;
|
||||
const { fileId, editIndex } = this.activeSession;
|
||||
if (editIndex === undefined) return;
|
||||
const fc = this.findFileChange(fileId);
|
||||
if (!fc) return;
|
||||
const prevIdx = editIndex - 1;
|
||||
if (prevIdx >= 0) {
|
||||
await this.showEditAtIndex(fc, prevIdx);
|
||||
} else {
|
||||
vscode.window.showInformationMessage('AI Diff: 已是第一次编辑 (1)');
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换到聚合视图(查看文件整体 diff) */
|
||||
async showAggregatedDiff(): Promise<void> {
|
||||
if (!this.activeSession) return;
|
||||
const fc = this.findFileChange(this.activeSession.fileId);
|
||||
if (fc) await this.showFileDiff(fc);
|
||||
}
|
||||
|
||||
private findFileChange(fileId: string): FileChange | undefined {
|
||||
const set = this.changeSetManager.getCurrentSet();
|
||||
return set?.changes.find(c => c.id === fileId);
|
||||
}
|
||||
|
||||
// ---- Accept/Reject(Accept/Reject 后删除临时文件 + 关闭 Diff) ----
|
||||
|
||||
async acceptCurrentDiff(): Promise<void> {
|
||||
|
||||
+40
-1
@@ -19,6 +19,10 @@ export class StatusBarManager {
|
||||
private acceptBtn: vscode.StatusBarItem;
|
||||
/** Diff 模式 Reject 按钮(拒绝当前文件所有变更) */
|
||||
private rejectBtn: vscode.StatusBarItem;
|
||||
/** ★ 上一次编辑按钮 */
|
||||
private prevEditBtn: vscode.StatusBarItem;
|
||||
/** ★ 下一次编辑按钮 */
|
||||
private nextEditBtn: vscode.StatusBarItem;
|
||||
|
||||
constructor(
|
||||
private changeSetManager: ChangeSetManager,
|
||||
@@ -38,9 +42,19 @@ export class StatusBarManager {
|
||||
// Reject 按钮(拒绝当前文件所有变更)
|
||||
this.rejectBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 99);
|
||||
this.rejectBtn.command = 'aiDiffPreview.rejectCurrentDiff';
|
||||
|
||||
// ★ 上一次编辑按钮
|
||||
this.prevEditBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 98);
|
||||
this.prevEditBtn.command = 'aiDiffPreview.prevEdit';
|
||||
this.prevEditBtn.tooltip = '上一次编辑 (Alt+Left)';
|
||||
|
||||
// ★ 下一次编辑按钮
|
||||
this.nextEditBtn = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 97);
|
||||
this.nextEditBtn.command = 'aiDiffPreview.nextEdit';
|
||||
this.nextEditBtn.tooltip = '下一次编辑 (Alt+Right)';
|
||||
}
|
||||
|
||||
enterDiffMode(_session: DiffSession): void {
|
||||
enterDiffMode(session: DiffSession): void {
|
||||
this.acceptBtn.text = '$(check) Accept';
|
||||
this.acceptBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
||||
this.acceptBtn.tooltip = '接受此文件所有变更';
|
||||
@@ -50,11 +64,34 @@ export class StatusBarManager {
|
||||
this.rejectBtn.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
|
||||
this.rejectBtn.tooltip = '拒绝此文件所有变更';
|
||||
this.rejectBtn.show();
|
||||
|
||||
// ★ 根据 session 状态显示编辑导航按钮
|
||||
if (session.editIndex !== undefined && session.totalEdits !== undefined) {
|
||||
// 逐次编辑模式:显示 Prev/Next 和计数
|
||||
this.prevEditBtn.text = `$(arrow-left) ${session.editIndex + 1}/${session.totalEdits}`;
|
||||
this.prevEditBtn.command = 'aiDiffPreview.prevEdit';
|
||||
this.prevEditBtn.tooltip = `上一次编辑 (Alt+Left) — 当前第 ${session.editIndex + 1}/${session.totalEdits} 次`;
|
||||
this.prevEditBtn.show();
|
||||
|
||||
this.nextEditBtn.text = '$(arrow-right)';
|
||||
this.nextEditBtn.command = 'aiDiffPreview.nextEdit';
|
||||
this.nextEditBtn.tooltip = '下一次编辑 (Alt+Right)';
|
||||
this.nextEditBtn.show();
|
||||
} else {
|
||||
// 聚合视图模式:显示"逐次查看"入口
|
||||
this.prevEditBtn.text = '$(list-ordered) 逐次查看';
|
||||
this.prevEditBtn.command = 'aiDiffPreview.showEditAtIndex';
|
||||
this.prevEditBtn.tooltip = '切换到逐次编辑查看模式';
|
||||
this.prevEditBtn.show();
|
||||
this.nextEditBtn.hide();
|
||||
}
|
||||
}
|
||||
|
||||
exitDiffMode(): void {
|
||||
this.acceptBtn.hide();
|
||||
this.rejectBtn.hide();
|
||||
this.prevEditBtn.hide();
|
||||
this.nextEditBtn.hide();
|
||||
}
|
||||
|
||||
/** ★ 常驻刷新 */
|
||||
@@ -98,5 +135,7 @@ export class StatusBarManager {
|
||||
this.summaryItem.dispose();
|
||||
this.acceptBtn.dispose();
|
||||
this.rejectBtn.dispose();
|
||||
this.prevEditBtn.dispose();
|
||||
this.nextEditBtn.dispose();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,13 @@ function norm(p: string): string {
|
||||
export class ChangeSetManager {
|
||||
private currentSet: ChangeSet | null = null;
|
||||
private originalContents: Map<string, string> = new Map();
|
||||
/** ★ .claude/hooks 目录路径,用于读取 PreToolUse 快照 */
|
||||
private hookDir: string | null = null;
|
||||
|
||||
/** 设置 hook 目录路径(由 extension.ts 在启动时调用) */
|
||||
setHookDir(dir: string): void {
|
||||
this.hookDir = dir;
|
||||
}
|
||||
|
||||
startNewSet(): string {
|
||||
const id = uuidv4();
|
||||
@@ -128,7 +135,8 @@ export class ChangeSetManager {
|
||||
}
|
||||
let original: string;
|
||||
if (toolName === 'Write') {
|
||||
original = '';
|
||||
// ★ 读取 PreToolUse 保存的原始文件内容
|
||||
original = this.readPreWriteSnapshot();
|
||||
} else if (toolName === 'Edit' && oldString) {
|
||||
const idx = currentContent.indexOf(newString);
|
||||
if (idx !== -1) {
|
||||
@@ -144,6 +152,26 @@ export class ChangeSetManager {
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 读取 PreToolUse hook 保存的文件原始内容快照
|
||||
* PreToolUse 在 Write 执行前触发,会将文件当前内容保存到 pre-write-snapshot.json
|
||||
*/
|
||||
private readPreWriteSnapshot(): string {
|
||||
try {
|
||||
if (!this.hookDir) return '';
|
||||
const snapshotPath = path.join(this.hookDir, 'pre-write-snapshot.json');
|
||||
if (!fs.existsSync(snapshotPath)) return '';
|
||||
const data = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
|
||||
// 读完即删,避免下次误用
|
||||
try { fs.unlinkSync(snapshotPath); } catch { /* ok */ }
|
||||
const content = data.originalContent || '';
|
||||
console.log(`[AI Diff] 读取 PreToolUse 快照: (${content.length} 字符)`);
|
||||
return content;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private reverseEdit(currentContent: string, oldString: string, newString: string): string {
|
||||
if (!newString) return currentContent;
|
||||
const idx = currentContent.indexOf(newString);
|
||||
@@ -313,6 +341,16 @@ export class ChangeSetManager {
|
||||
clear(): void {
|
||||
this.currentSet = null;
|
||||
this.originalContents.clear();
|
||||
this.cleanupPreWriteSnapshot();
|
||||
}
|
||||
|
||||
/** 清理可能残留的 PreToolUse 快照文件 */
|
||||
private cleanupPreWriteSnapshot(): void {
|
||||
try {
|
||||
if (!this.hookDir) return;
|
||||
const snapshotPath = path.join(this.hookDir, 'pre-write-snapshot.json');
|
||||
if (fs.existsSync(snapshotPath)) fs.unlinkSync(snapshotPath);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
|
||||
hasPendingChanges(): boolean {
|
||||
|
||||
@@ -134,7 +134,29 @@ export class HookHandler {
|
||||
if (!selected) return;
|
||||
|
||||
if (selected.itemKind === 'file' && selected.fileChange) {
|
||||
await this.diffViewer.showFileDiff(selected.fileChange);
|
||||
if (selected.fileChange.edits.length > 1) {
|
||||
// ★ 多次编辑:弹出二级选择
|
||||
const choice = await vscode.window.showQuickPick([
|
||||
{
|
||||
label: '$(diff) 聚合视图',
|
||||
description: `查看 ${selected.fileChange.edits.length} 次编辑的总 diff`,
|
||||
mode: 'aggregate' as const,
|
||||
},
|
||||
{
|
||||
label: '$(list-ordered) 逐次查看',
|
||||
description: '逐个查看每次编辑的 diff,支持 Accept/Reject 单次编辑',
|
||||
mode: 'perEdit' as const,
|
||||
},
|
||||
], { placeHolder: `${path.basename(selected.fileChange.filePath)} — 选择查看模式` });
|
||||
if (!choice) return;
|
||||
if (choice.mode === 'perEdit') {
|
||||
await this.diffViewer.showEditAtIndex(selected.fileChange, 0);
|
||||
} else {
|
||||
await this.diffViewer.showFileDiff(selected.fileChange);
|
||||
}
|
||||
} else {
|
||||
await this.diffViewer.showFileDiff(selected.fileChange);
|
||||
}
|
||||
} else if (selected.itemKind === 'action') {
|
||||
if (selected.action === 'acceptAll') {
|
||||
const n = this.changeSetManager.acceptAll();
|
||||
|
||||
Reference in New Issue
Block a user