mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-30 17:11:52 +08:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user