feat: 初始化 AI Code Diff Preview 插件项目

- 核心架构搭建:四层架构设计(触发层、隔离层、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
This commit is contained in:
2026-06-17 21:11:33 +08:00
commit 3168e81aa6
21 changed files with 6912 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* CodeLens 提供器 - 提供 Accept/Reject 按钮
*/
import * as vscode from 'vscode';
import { SnapshotManager } from '../snapshot/snapshotManager';
import { COMMANDS } from '../models/constants';
export class CodeLensProvider implements vscode.CodeLensProvider {
private _onDidChangeCodeLenses = new vscode.EventEmitter<void>();
readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event;
constructor(private snapshotManager: SnapshotManager) {}
/**
* 刷新 CodeLens
*/
refresh(): void {
this._onDidChangeCodeLenses.fire();
}
/**
* 提供 CodeLens
*/
provideCodeLenses(
document: vscode.TextDocument,
token: vscode.CancellationToken
): vscode.CodeLens[] {
const snapshot = this.snapshotManager.getSnapshotByFilePath(document.uri.fsPath);
if (!snapshot || snapshot.status !== 'active') {
return [];
}
const codeLenses: vscode.CodeLens[] = [];
// 为每个待处理的块添加 CodeLens
snapshot.chunks.forEach(chunk => {
if (chunk.status !== 'pending') return;
const range = new vscode.Range(
new vscode.Position(chunk.startLine, 0),
new vscode.Position(chunk.startLine, 0)
);
// Accept 按钮
codeLenses.push(
new vscode.CodeLens(range, {
title: '$(check) Accept',
command: COMMANDS.ACCEPT_CHUNK,
arguments: [snapshot.id, chunk.id],
tooltip: '接受此变更块',
})
);
// Reject 按钮
codeLenses.push(
new vscode.CodeLens(range, {
title: '$(close) Reject',
command: COMMANDS.REJECT_CHUNK,
arguments: [snapshot.id, chunk.id],
tooltip: '拒绝此变更块',
})
);
});
// 如果有多个块,添加全局操作按钮
const pendingChunks = snapshot.chunks.filter(c => c.status === 'pending');
if (pendingChunks.length > 1) {
const firstChunk = pendingChunks[0];
const range = new vscode.Range(
new vscode.Position(firstChunk.startLine, 0),
new vscode.Position(firstChunk.startLine, 0)
);
codeLenses.push(
new vscode.CodeLens(range, {
title: '$(check-all) Accept All',
command: COMMANDS.ACCEPT_ALL,
arguments: [snapshot.id],
tooltip: '接受所有变更',
})
);
codeLenses.push(
new vscode.CodeLens(range, {
title: '$(trash) Reject All',
command: COMMANDS.REJECT_ALL,
arguments: [snapshot.id],
tooltip: '拒绝所有变更',
})
);
}
return codeLenses;
}
}
+133
View File
@@ -0,0 +1,133 @@
/**
* 内联装饰器 - 渲染 Diff 高亮和按钮
*/
import * as vscode from 'vscode';
import { DiffChunk, ChunkType } from '../models/types';
import { COLORS, DECORATION_TYPES } from '../models/constants';
export class InlineDecorator {
private addDecorationType: vscode.TextEditorDecorationType;
private deleteDecorationType: vscode.TextEditorDecorationType;
private modifyDecorationType: vscode.TextEditorDecorationType;
private conflictDecorationType: vscode.TextEditorDecorationType;
private acceptButtonDecorationType: vscode.TextEditorDecorationType;
constructor() {
this.addDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: COLORS.ADD_BACKGROUND,
isWholeLine: true,
gutterIconPath: this.createGutterIconPath(COLORS.ADD_GUTTER),
gutterIconSize: 'contain',
});
this.deleteDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: COLORS.DELETE_BACKGROUND,
isWholeLine: true,
textDecoration: 'line-through',
gutterIconPath: this.createGutterIconPath(COLORS.DELETE_GUTTER),
gutterIconSize: 'contain',
});
this.modifyDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: COLORS.MODIFY_BACKGROUND,
isWholeLine: true,
gutterIconPath: this.createGutterIconPath(COLORS.MODIFY_GUTTER),
gutterIconSize: 'contain',
});
this.conflictDecorationType = vscode.window.createTextEditorDecorationType({
backgroundColor: COLORS.CONFLICT_BACKGROUND,
isWholeLine: true,
borderColor: COLORS.MODIFY_GUTTER,
borderStyle: 'solid',
borderWidth: '1px',
});
this.acceptButtonDecorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ' ✓ Accept',
color: '#ffffff',
backgroundColor: COLORS.ADD_GUTTER,
margin: '0 0 0 1em',
border: '1px solid rgba(255,255,255,0.2)',
cursor: 'pointer',
},
});
}
private createGutterIconPath(color: string): vscode.Uri {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<circle cx="8" cy="8" r="6" fill="${color}"/>
</svg>`;
return vscode.Uri.parse(`data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`);
}
/**
* 渲染变更块
*/
renderChunks(editor: vscode.TextEditor, chunks: DiffChunk[]): void {
const addRanges: vscode.Range[] = [];
const deleteRanges: vscode.Range[] = [];
const modifyRanges: vscode.Range[] = [];
const conflictRanges: vscode.Range[] = [];
const buttonRanges: vscode.Range[] = [];
chunks.forEach(chunk => {
if (chunk.status === 'accepted' || chunk.status === 'rejected') {
return; // 跳过已处理的块
}
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);
buttonRanges.push(range);
break;
case 'delete':
deleteRanges.push(range);
break;
case 'modify':
modifyRanges.push(range);
buttonRanges.push(range);
break;
}
if (chunk.status === 'conflict') {
conflictRanges.push(range);
}
});
editor.setDecorations(this.addDecorationType, addRanges);
editor.setDecorations(this.deleteDecorationType, deleteRanges);
editor.setDecorations(this.modifyDecorationType, modifyRanges);
editor.setDecorations(this.conflictDecorationType, conflictRanges);
editor.setDecorations(this.acceptButtonDecorationType, buttonRanges);
}
/**
* 清除所有装饰器
*/
clearDecorations(editor: vscode.TextEditor): void {
editor.setDecorations(this.addDecorationType, []);
editor.setDecorations(this.deleteDecorationType, []);
editor.setDecorations(this.modifyDecorationType, []);
editor.setDecorations(this.conflictDecorationType, []);
editor.setDecorations(this.acceptButtonDecorationType, []);
}
/**
* 释放资源
*/
dispose(): void {
this.addDecorationType.dispose();
this.deleteDecorationType.dispose();
this.modifyDecorationType.dispose();
this.conflictDecorationType.dispose();
this.acceptButtonDecorationType.dispose();
}
}
+70
View File
@@ -0,0 +1,70 @@
/**
* 状态栏管理器 - 显示 Diff 状态信息
*/
import * as vscode from 'vscode';
import { Snapshot } from '../models/types';
import { STATUS_BAR_PRIORITY } from '../models/constants';
export class StatusBarManager {
private statusBarItem: vscode.StatusBarItem;
private chunkCountItem: vscode.StatusBarItem;
constructor() {
this.statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
STATUS_BAR_PRIORITY.DIFF_STATUS
);
this.chunkCountItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left,
STATUS_BAR_PRIORITY.CHUNK_COUNT
);
}
/**
* 更新状态栏
*/
update(snapshot: Snapshot): void {
const pendingChunks = snapshot.chunks.filter(c => c.status === 'pending');
const conflictChunks = snapshot.chunks.filter(c => c.status === 'conflict');
// 更新主状态
this.statusBarItem.text = '$(diff) AI Diff';
this.statusBarItem.tooltip = 'AI Diff Preview 活跃';
this.statusBarItem.show();
// 更新变更块数量
if (pendingChunks.length > 0 || conflictChunks.length > 0) {
this.chunkCountItem.text = `$(edit) ${pendingChunks.length} 个变更`;
if (conflictChunks.length > 0) {
this.chunkCountItem.text += ` (${conflictChunks.length} 个冲突)`;
this.chunkCountItem.backgroundColor = new vscode.ThemeColor(
'statusBarItem.warningBackground'
);
} else {
this.chunkCountItem.backgroundColor = undefined;
}
this.chunkCountItem.tooltip = '点击查看变更详情';
this.chunkCountItem.show();
} else {
this.chunkCountItem.hide();
}
}
/**
* 清除状态栏
*/
clear(): void {
this.statusBarItem.hide();
this.chunkCountItem.hide();
}
/**
* 释放资源
*/
dispose(): void {
this.statusBarItem.dispose();
this.chunkCountItem.dispose();
}
}