mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-10-02 18:21: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,91 @@
|
||||
/**
|
||||
* Diff 引擎测试
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DiffEngine } from '../diff/diffEngine';
|
||||
|
||||
describe('DiffEngine', () => {
|
||||
const engine = new DiffEngine();
|
||||
|
||||
describe('computeChunks', () => {
|
||||
it('应该检测新增行', () => {
|
||||
const base = 'line1\nline2\nline3';
|
||||
const suggest = 'line1\nline2\nline3\nline4';
|
||||
|
||||
const chunks = engine.computeChunks(base, suggest);
|
||||
|
||||
// diffLines 可能产生多个变更块(取决于行尾处理)
|
||||
const addChunks = chunks.filter(c => c.type === 'add');
|
||||
expect(addChunks.length).toBeGreaterThanOrEqual(1);
|
||||
expect(addChunks.some(c => c.content.includes('line4'))).toBe(true);
|
||||
});
|
||||
|
||||
it('应该检测删除行', () => {
|
||||
const base = 'line1\nline2\nline3';
|
||||
const suggest = 'line1\nline3';
|
||||
|
||||
const chunks = engine.computeChunks(base, suggest);
|
||||
|
||||
const deleteChunks = chunks.filter(c => c.type === 'delete');
|
||||
expect(deleteChunks.length).toBeGreaterThanOrEqual(1);
|
||||
expect(deleteChunks.some(c => c.content.includes('line2'))).toBe(true);
|
||||
});
|
||||
|
||||
it('应该检测修改行', () => {
|
||||
const base = 'line1\nline2\nline3';
|
||||
const suggest = 'line1\nmodified\nline3';
|
||||
|
||||
const chunks = engine.computeChunks(base, suggest);
|
||||
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
const hasModifyOrDelete = chunks.some(c => c.type === 'delete' || c.type === 'modify');
|
||||
expect(hasModifyOrDelete).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理空文本', () => {
|
||||
const chunks = engine.computeChunks('', '');
|
||||
expect(chunks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该处理完全不同的文本', () => {
|
||||
const base = 'old content';
|
||||
const suggest = 'new content';
|
||||
|
||||
const chunks = engine.computeChunks(base, suggest);
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyChunk', () => {
|
||||
it('应该正确应用新增块', () => {
|
||||
const base = 'line1\nline2\nline3';
|
||||
const chunk = {
|
||||
id: 'test',
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
type: 'add' as const,
|
||||
content: 'new line\n',
|
||||
status: 'pending' as const,
|
||||
};
|
||||
|
||||
const result = engine.applyChunk(base, chunk);
|
||||
expect(result).toContain('new line');
|
||||
});
|
||||
|
||||
it('应该正确应用删除块', () => {
|
||||
const base = 'line1\nline2\nline3';
|
||||
const chunk = {
|
||||
id: 'test',
|
||||
startLine: 1,
|
||||
endLine: 2,
|
||||
type: 'delete' as const,
|
||||
content: 'line2\n',
|
||||
status: 'pending' as const,
|
||||
};
|
||||
|
||||
const result = engine.applyChunk(base, chunk);
|
||||
expect(result).not.toContain('line2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Diff 计算引擎 - 基于 Myers 算法
|
||||
*/
|
||||
|
||||
import { diffLines, Change } from 'diff';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { DiffChunk, ChunkType, Snapshot } from '../models/types';
|
||||
|
||||
export 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) {
|
||||
const type: ChunkType = change.added ? 'add' : 'delete';
|
||||
const lines = change.value.split('\n').filter(l => l.length > 0);
|
||||
|
||||
chunks.push({
|
||||
id: uuidv4(),
|
||||
startLine: lineOffset,
|
||||
endLine: lineOffset + lines.length,
|
||||
type,
|
||||
content: change.value,
|
||||
status: 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
if (!change.removed) {
|
||||
lineOffset += change.count || 0;
|
||||
}
|
||||
});
|
||||
|
||||
return this.mergeAdjacentChunks(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并相邻的变更块
|
||||
*/
|
||||
private mergeAdjacentChunks(chunks: DiffChunk[]): DiffChunk[] {
|
||||
if (chunks.length <= 1) return chunks;
|
||||
|
||||
const merged: DiffChunk[] = [];
|
||||
let current = { ...chunks[0] };
|
||||
|
||||
for (let i = 1; i < chunks.length; i++) {
|
||||
const next = chunks[i];
|
||||
|
||||
// 如果相邻且类型相同,合并
|
||||
if (current.endLine >= next.startLine && current.type === next.type) {
|
||||
current = {
|
||||
...current,
|
||||
endLine: next.endLine,
|
||||
content: current.content + next.content,
|
||||
};
|
||||
} else {
|
||||
merged.push(current);
|
||||
current = { ...next };
|
||||
}
|
||||
}
|
||||
|
||||
merged.push(current);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测冲突 - 用户手动修改与 AI 建议的冲突
|
||||
*/
|
||||
detectConflicts(snapshot: Snapshot, currentContent: string): void {
|
||||
const baseToCurrent = this.computeChunks(snapshot.baseContent, currentContent);
|
||||
const baseToSuggest = snapshot.chunks;
|
||||
|
||||
// 重置所有非冲突状态
|
||||
baseToSuggest.forEach(chunk => {
|
||||
if (chunk.status === 'conflict') {
|
||||
chunk.status = 'pending';
|
||||
chunk.conflictWith = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// 检测重叠区域
|
||||
baseToSuggest.forEach(suggestChunk => {
|
||||
const hasConflict = baseToCurrent.some(currentChunk =>
|
||||
this.isOverlapping(suggestChunk, currentChunk)
|
||||
);
|
||||
|
||||
if (hasConflict) {
|
||||
suggestChunk.status = 'conflict';
|
||||
suggestChunk.conflictWith = baseToCurrent.find(c =>
|
||||
this.isOverlapping(suggestChunk, c)
|
||||
)?.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个变更块是否重叠
|
||||
*/
|
||||
private isOverlapping(chunk1: DiffChunk, chunk2: DiffChunk): boolean {
|
||||
return chunk1.startLine < chunk2.endLine && chunk2.startLine < chunk1.endLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用变更块到文本
|
||||
*/
|
||||
applyChunk(baseContent: string, chunk: DiffChunk): string {
|
||||
const lines = baseContent.split('\n');
|
||||
const chunkLines = chunk.content.split('\n').filter(l => l.length > 0);
|
||||
|
||||
switch (chunk.type) {
|
||||
case 'add':
|
||||
lines.splice(chunk.startLine, 0, ...chunkLines);
|
||||
break;
|
||||
case 'delete':
|
||||
lines.splice(chunk.startLine, chunk.endLine - chunk.startLine);
|
||||
break;
|
||||
case 'modify':
|
||||
lines.splice(chunk.startLine, chunk.endLine - chunk.startLine, ...chunkLines);
|
||||
break;
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用多个变更块到文本
|
||||
*/
|
||||
applyChunks(baseContent: string, chunks: DiffChunk[]): string {
|
||||
// 按行号倒序排序,从后往前应用,避免行号偏移
|
||||
const sortedChunks = [...chunks]
|
||||
.filter(c => c.status === 'accepted')
|
||||
.sort((a, b) => b.startLine - a.startLine);
|
||||
|
||||
let result = baseContent;
|
||||
sortedChunks.forEach(chunk => {
|
||||
result = this.applyChunk(result, chunk);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* AI Code Diff Preview - 插件入口
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { SnapshotManager } from './snapshot/snapshotManager';
|
||||
import { DiffEngine } from './diff/diffEngine';
|
||||
import { InlineDecorator } from './render/inlineDecorator';
|
||||
import { CodeLensProvider } from './render/codeLensProvider';
|
||||
import { HookHandler } from './trigger/hookHandler';
|
||||
import { AcceptHandler } from './transaction/acceptHandler';
|
||||
import { RejectHandler } from './transaction/rejectHandler';
|
||||
import { StatusBarManager } from './render/statusBar';
|
||||
import { COMMANDS, CONTEXT_KEYS } from './models/constants';
|
||||
|
||||
let snapshotManager: SnapshotManager;
|
||||
let diffEngine: DiffEngine;
|
||||
let inlineDecorator: InlineDecorator;
|
||||
let codeLensProvider: CodeLensProvider;
|
||||
let hookHandler: HookHandler;
|
||||
let acceptHandler: AcceptHandler;
|
||||
let rejectHandler: RejectHandler;
|
||||
let statusBarManager: StatusBarManager;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('AI Code Diff Preview 插件已激活');
|
||||
|
||||
// 初始化核心模块
|
||||
snapshotManager = new SnapshotManager();
|
||||
diffEngine = new DiffEngine();
|
||||
inlineDecorator = new InlineDecorator();
|
||||
codeLensProvider = new CodeLensProvider(snapshotManager);
|
||||
statusBarManager = new StatusBarManager();
|
||||
|
||||
// 初始化处理器
|
||||
acceptHandler = new AcceptHandler(snapshotManager, diffEngine, inlineDecorator);
|
||||
rejectHandler = new RejectHandler(snapshotManager, inlineDecorator);
|
||||
hookHandler = new HookHandler(snapshotManager, diffEngine, inlineDecorator, statusBarManager);
|
||||
|
||||
// 注册命令
|
||||
registerCommands(context);
|
||||
|
||||
// 注册 CodeLens 提供器
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider('*', codeLensProvider)
|
||||
);
|
||||
|
||||
// 注册文档变更监听
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
handleDocumentChange(event);
|
||||
})
|
||||
);
|
||||
|
||||
// 注册编辑器切换监听
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) {
|
||||
handleEditorSwitch(editor);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 设置初始上下文
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, false);
|
||||
}
|
||||
|
||||
function registerCommands(context: vscode.ExtensionContext) {
|
||||
// Accept 当前块
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.ACCEPT_CHUNK, async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (!snapshot) return;
|
||||
|
||||
const chunk = getChunkAtCursor(editor, snapshot.chunks);
|
||||
if (chunk) {
|
||||
await acceptHandler.acceptChunk(snapshot.id, chunk.id);
|
||||
refreshUI(editor);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Reject 当前块
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.REJECT_CHUNK, async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (!snapshot) return;
|
||||
|
||||
const chunk = getChunkAtCursor(editor, snapshot.chunks);
|
||||
if (chunk) {
|
||||
await rejectHandler.rejectChunk(snapshot.id, chunk.id);
|
||||
refreshUI(editor);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Accept All
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.ACCEPT_ALL, async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (!snapshot) return;
|
||||
|
||||
await acceptHandler.acceptAll(snapshot.id);
|
||||
refreshUI(editor);
|
||||
})
|
||||
);
|
||||
|
||||
// Reject All
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.REJECT_ALL, async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (!snapshot) return;
|
||||
|
||||
await rejectHandler.rejectAll(snapshot.id);
|
||||
refreshUI(editor);
|
||||
})
|
||||
);
|
||||
|
||||
// 显示 Diff 面板
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(COMMANDS.SHOW_DIFF_PANEL, () => {
|
||||
// TODO: 实现 Diff 面板
|
||||
vscode.window.showInformationMessage('Diff 面板功能开发中...');
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getChunkAtCursor(
|
||||
editor: vscode.TextEditor,
|
||||
chunks: import('./models/types').DiffChunk[]
|
||||
): import('./models/types').DiffChunk | undefined {
|
||||
const cursorLine = editor.selection.active.line;
|
||||
return chunks.find(
|
||||
chunk => chunk.status === 'pending' &&
|
||||
cursorLine >= chunk.startLine &&
|
||||
cursorLine <= chunk.endLine
|
||||
);
|
||||
}
|
||||
|
||||
function handleDocumentChange(event: vscode.TextDocumentChangeEvent) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document !== event.document) return;
|
||||
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(event.document.uri.fsPath);
|
||||
if (!snapshot) return;
|
||||
|
||||
// 检测用户手动修改是否与 AI 建议冲突
|
||||
diffEngine.detectConflicts(snapshot, event.document.getText());
|
||||
refreshUI(editor);
|
||||
}
|
||||
|
||||
function handleEditorSwitch(editor: vscode.TextEditor) {
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (snapshot) {
|
||||
inlineDecorator.renderChunks(editor, snapshot.chunks);
|
||||
statusBarManager.update(snapshot);
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, true);
|
||||
} else {
|
||||
inlineDecorator.clearDecorations(editor);
|
||||
statusBarManager.clear();
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, false);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshUI(editor: vscode.TextEditor) {
|
||||
const snapshot = snapshotManager.getSnapshotByFilePath(editor.document.uri.fsPath);
|
||||
if (snapshot) {
|
||||
inlineDecorator.renderChunks(editor, snapshot.chunks);
|
||||
codeLensProvider.refresh();
|
||||
statusBarManager.update(snapshot);
|
||||
|
||||
// 检查是否所有块都已处理
|
||||
const allProcessed = snapshot.chunks.every(c => c.status !== 'pending');
|
||||
if (allProcessed) {
|
||||
snapshotManager.completeSnapshot(snapshot.id);
|
||||
inlineDecorator.clearDecorations(editor);
|
||||
statusBarManager.clear();
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
console.log('AI Code Diff Preview 插件已停用');
|
||||
inlineDecorator.dispose();
|
||||
statusBarManager.dispose();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* AI Code Diff Preview - 常量定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 插件配置节名称
|
||||
*/
|
||||
export const CONFIG_SECTION = 'aiDiffPreview';
|
||||
|
||||
/**
|
||||
* 命令 ID
|
||||
*/
|
||||
export const COMMANDS = {
|
||||
ACCEPT_CHUNK: 'aiDiffPreview.acceptChunk',
|
||||
REJECT_CHUNK: 'aiDiffPreview.rejectChunk',
|
||||
ACCEPT_ALL: 'aiDiffPreview.acceptAll',
|
||||
REJECT_ALL: 'aiDiffPreview.rejectAll',
|
||||
SHOW_DIFF_PANEL: 'aiDiffPreview.showDiffPanel',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 上下文键
|
||||
*/
|
||||
export const CONTEXT_KEYS = {
|
||||
IS_ACTIVE: 'aiDiffPreview.isActive',
|
||||
HAS_CONFLICTS: 'aiDiffPreview.hasConflicts',
|
||||
CHUNK_COUNT: 'aiDiffPreview.chunkCount',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 装饰器类型 ID
|
||||
*/
|
||||
export const DECORATION_TYPES = {
|
||||
ADD_LINE: 'aiDiffPreview.addLine',
|
||||
DELETE_LINE: 'aiDiffPreview.deleteLine',
|
||||
MODIFY_LINE: 'aiDiffPreview.modifyLine',
|
||||
CONFLICT_LINE: 'aiDiffPreview.conflictLine',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 颜色配置
|
||||
*/
|
||||
export const COLORS = {
|
||||
ADD_BACKGROUND: 'rgba(46, 160, 67, 0.15)',
|
||||
ADD_GUTTER: '#2ea043',
|
||||
DELETE_BACKGROUND: 'rgba(248, 81, 73, 0.15)',
|
||||
DELETE_GUTTER: '#f85149',
|
||||
MODIFY_BACKGROUND: 'rgba(210, 153, 34, 0.15)',
|
||||
MODIFY_GUTTER: '#d29922',
|
||||
CONFLICT_BACKGROUND: 'rgba(210, 153, 34, 0.3)',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 默认配置值
|
||||
*/
|
||||
export const DEFAULT_CONFIG = {
|
||||
ENABLE_AUTO_TRIGGER: true,
|
||||
SHOW_INLINE_BUTTONS: true,
|
||||
AUTO_LINT: true,
|
||||
MAX_FILE_SIZE: 100000,
|
||||
CONTEXT_LINES: 3,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 状态栏优先级
|
||||
*/
|
||||
export const STATUS_BAR_PRIORITY = {
|
||||
DIFF_STATUS: 100,
|
||||
CHUNK_COUNT: 99,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Webview 视图类型
|
||||
*/
|
||||
export const WEBVIEW_VIEW_TYPE = 'aiDiffPreview.diffPanel';
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* AI Code Diff Preview - 核心类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* Claude Hook 事件类型
|
||||
*/
|
||||
export type HookEventType = 'after_code_edit' | 'after_write_file';
|
||||
|
||||
/**
|
||||
* Claude Hook 事件
|
||||
*/
|
||||
export interface ClaudeHookEvent {
|
||||
type: HookEventType;
|
||||
filePath: string;
|
||||
patch: string;
|
||||
timestamp: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更块类型
|
||||
*/
|
||||
export type ChunkType = 'add' | 'delete' | 'modify';
|
||||
|
||||
/**
|
||||
* 变更块状态
|
||||
*/
|
||||
export type ChunkStatus = 'pending' | 'accepted' | 'rejected' | 'conflict';
|
||||
|
||||
/**
|
||||
* Diff 变更块
|
||||
*/
|
||||
export interface DiffChunk {
|
||||
id: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
type: ChunkType;
|
||||
content: string;
|
||||
status: ChunkStatus;
|
||||
conflictWith?: string; // 冲突的变更块 ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件快照
|
||||
*/
|
||||
export interface Snapshot {
|
||||
id: string;
|
||||
filePath: string;
|
||||
baseContent: string;
|
||||
suggestContent: string;
|
||||
timestamp: number;
|
||||
chunks: DiffChunk[];
|
||||
status: 'active' | 'completed' | 'cancelled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff 渲染选项
|
||||
*/
|
||||
export interface DiffRenderOptions {
|
||||
showInlineButtons: boolean;
|
||||
showLineNumbers: boolean;
|
||||
contextLines: number;
|
||||
syntaxHighlight: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept/Reject 操作结果
|
||||
*/
|
||||
export interface TransactionResult {
|
||||
success: boolean;
|
||||
snapshotId: string;
|
||||
chunkId?: string;
|
||||
action: 'accept' | 'reject';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff 面板消息类型
|
||||
*/
|
||||
export type DiffPanelMessageType =
|
||||
| { type: 'accept'; chunkId: string }
|
||||
| { type: 'reject'; chunkId: string }
|
||||
| { type: 'acceptAll' }
|
||||
| { type: 'rejectAll' }
|
||||
| { type: 'refresh' };
|
||||
|
||||
/**
|
||||
* Diff 面板状态
|
||||
*/
|
||||
export interface DiffPanelState {
|
||||
snapshotId: string;
|
||||
filePath: string;
|
||||
chunks: DiffChunk[];
|
||||
totalChanges: number;
|
||||
acceptedChanges: number;
|
||||
rejectedChanges: number;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 快照管理器 - 管理文件的 Base/Suggest 双版本
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Snapshot, DiffChunk, ChunkStatus } from '../models/types';
|
||||
|
||||
export class SnapshotManager {
|
||||
private snapshots: Map<string, Snapshot> = new Map();
|
||||
private filePathIndex: Map<string, string> = new Map(); // filePath -> snapshotId
|
||||
|
||||
/**
|
||||
* 创建 Base 快照
|
||||
*/
|
||||
createBaseSnapshot(filePath: string, baseContent: string): string {
|
||||
const id = uuidv4();
|
||||
const snapshot: Snapshot = {
|
||||
id,
|
||||
filePath,
|
||||
baseContent,
|
||||
suggestContent: '',
|
||||
timestamp: Date.now(),
|
||||
chunks: [],
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
this.snapshots.set(id, snapshot);
|
||||
this.filePathIndex.set(filePath, id);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 AI 生成的 Patch
|
||||
*/
|
||||
applyPatch(snapshotId: string, patch: string, chunks: DiffChunk[]): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (!snapshot) {
|
||||
throw new Error(`快照不存在: ${snapshotId}`);
|
||||
}
|
||||
|
||||
snapshot.suggestContent = patch;
|
||||
snapshot.chunks = chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快照
|
||||
*/
|
||||
getSnapshot(snapshotId: string): Snapshot | undefined {
|
||||
return this.snapshots.get(snapshotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件路径获取快照
|
||||
*/
|
||||
getSnapshotByFilePath(filePath: string): Snapshot | undefined {
|
||||
const snapshotId = this.filePathIndex.get(filePath);
|
||||
if (!snapshotId) return undefined;
|
||||
return this.snapshots.get(snapshotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变更块状态
|
||||
*/
|
||||
updateChunkStatus(snapshotId: string, chunkId: string, status: ChunkStatus): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
const chunk = snapshot.chunks.find(c => c.id === chunkId);
|
||||
if (chunk) {
|
||||
chunk.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新变更块状态
|
||||
*/
|
||||
updateAllChunksStatus(snapshotId: string, status: ChunkStatus): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
snapshot.chunks.forEach(chunk => {
|
||||
chunk.status = status;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记快照完成
|
||||
*/
|
||||
completeSnapshot(snapshotId: string): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
snapshot.status = 'completed';
|
||||
this.filePathIndex.delete(snapshot.filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消快照
|
||||
*/
|
||||
cancelSnapshot(snapshotId: string): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
snapshot.status = 'cancelled';
|
||||
this.filePathIndex.delete(snapshot.filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除快照
|
||||
*/
|
||||
deleteSnapshot(snapshotId: string): void {
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
if (snapshot) {
|
||||
this.filePathIndex.delete(snapshot.filePath);
|
||||
}
|
||||
this.snapshots.delete(snapshotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有活跃快照
|
||||
*/
|
||||
getActiveSnapshots(): Snapshot[] {
|
||||
return Array.from(this.snapshots.values()).filter(s => s.status === 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否有活跃快照
|
||||
*/
|
||||
hasActiveSnapshot(filePath: string): boolean {
|
||||
const snapshotId = this.filePathIndex.get(filePath);
|
||||
if (!snapshotId) return false;
|
||||
const snapshot = this.snapshots.get(snapshotId);
|
||||
return snapshot?.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已完成或取消的快照
|
||||
*/
|
||||
cleanup(): void {
|
||||
for (const [id, snapshot] of this.snapshots.entries()) {
|
||||
if (snapshot.status !== 'active') {
|
||||
this.filePathIndex.delete(snapshot.filePath);
|
||||
this.snapshots.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Accept 处理器 - 处理接受变更的逻辑
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { SnapshotManager } from '../snapshot/snapshotManager';
|
||||
import { DiffEngine } from '../diff/diffEngine';
|
||||
import { InlineDecorator } from '../render/inlineDecorator';
|
||||
import { TransactionResult } from '../models/types';
|
||||
|
||||
export class AcceptHandler {
|
||||
constructor(
|
||||
private snapshotManager: SnapshotManager,
|
||||
private diffEngine: DiffEngine,
|
||||
private inlineDecorator: InlineDecorator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 接受单个变更块
|
||||
*/
|
||||
async acceptChunk(snapshotId: string, chunkId: string): Promise<TransactionResult> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'accept',
|
||||
error: '快照不存在',
|
||||
};
|
||||
}
|
||||
|
||||
const chunk = snapshot.chunks.find(c => c.id === chunkId);
|
||||
if (!chunk) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'accept',
|
||||
error: '变更块不存在',
|
||||
};
|
||||
}
|
||||
|
||||
if (chunk.status === 'conflict') {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'accept',
|
||||
error: '存在冲突,请先解决冲突',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新变更块状态
|
||||
this.snapshotManager.updateChunkStatus(snapshotId, chunkId, 'accepted');
|
||||
|
||||
// 检查是否所有块都已接受
|
||||
const allAccepted = snapshot.chunks.every(
|
||||
c => c.status === 'accepted' || c.status === 'rejected'
|
||||
);
|
||||
|
||||
if (allAccepted) {
|
||||
// 应用所有已接受的变更到文件
|
||||
await this.applyChanges(snapshotId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'accept',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'accept',
|
||||
error: `接受失败: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受所有变更块
|
||||
*/
|
||||
async acceptAll(snapshotId: string): Promise<TransactionResult> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
action: 'accept',
|
||||
error: '快照不存在',
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否有冲突
|
||||
const hasConflicts = snapshot.chunks.some(c => c.status === 'conflict');
|
||||
if (hasConflicts) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
action: 'accept',
|
||||
error: '存在冲突,请先解决冲突',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 接受所有待处理的块
|
||||
this.snapshotManager.updateAllChunksStatus(snapshotId, 'accepted');
|
||||
|
||||
// 应用变更
|
||||
await this.applyChanges(snapshotId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
snapshotId,
|
||||
action: 'accept',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
action: 'accept',
|
||||
error: `接受失败: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用变更到文件
|
||||
*/
|
||||
private async applyChanges(snapshotId: string): Promise<void> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
// 计算最终内容
|
||||
const finalContent = this.diffEngine.applyChunks(
|
||||
snapshot.baseContent,
|
||||
snapshot.chunks
|
||||
);
|
||||
|
||||
// 原子化写入文件
|
||||
await this.writeFileAtomic(snapshot.filePath, finalContent);
|
||||
|
||||
// 刷新 LSP
|
||||
await this.refreshLSP(snapshot.filePath);
|
||||
|
||||
// 标记快照完成
|
||||
this.snapshotManager.completeSnapshot(snapshotId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子化写入文件
|
||||
*/
|
||||
private async writeFileAtomic(filePath: string, content: string): Promise<void> {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
const fullRange = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(document.getText().length)
|
||||
);
|
||||
|
||||
edit.replace(uri, fullRange, content);
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
await document.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 LSP
|
||||
*/
|
||||
private async refreshLSP(filePath: string): Promise<void> {
|
||||
// 触发 LSP 重新解析
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
|
||||
// 通过修改文档触发 LSP 更新
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
edit.insert(uri, new vscode.Position(0, 0), '');
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
|
||||
// 立即撤销
|
||||
await vscode.commands.executeCommand('undo');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Reject 处理器 - 处理拒绝变更的逻辑
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { SnapshotManager } from '../snapshot/snapshotManager';
|
||||
import { InlineDecorator } from '../render/inlineDecorator';
|
||||
import { TransactionResult } from '../models/types';
|
||||
import { CONTEXT_KEYS } from '../models/constants';
|
||||
|
||||
export class RejectHandler {
|
||||
constructor(
|
||||
private snapshotManager: SnapshotManager,
|
||||
private inlineDecorator: InlineDecorator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 拒绝单个变更块
|
||||
*/
|
||||
async rejectChunk(snapshotId: string, chunkId: string): Promise<TransactionResult> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'reject',
|
||||
error: '快照不存在',
|
||||
};
|
||||
}
|
||||
|
||||
const chunk = snapshot.chunks.find(c => c.id === chunkId);
|
||||
if (!chunk) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'reject',
|
||||
error: '变更块不存在',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新变更块状态
|
||||
this.snapshotManager.updateChunkStatus(snapshotId, chunkId, 'rejected');
|
||||
|
||||
// 检查是否所有块都已处理
|
||||
const allProcessed = snapshot.chunks.every(
|
||||
c => c.status === 'accepted' || c.status === 'rejected'
|
||||
);
|
||||
|
||||
if (allProcessed) {
|
||||
// 清理 UI
|
||||
await this.cleanupUI(snapshotId);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'reject',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
chunkId,
|
||||
action: 'reject',
|
||||
error: `拒绝失败: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝所有变更块
|
||||
*/
|
||||
async rejectAll(snapshotId: string): Promise<TransactionResult> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
action: 'reject',
|
||||
error: '快照不存在',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// 拒绝所有待处理的块
|
||||
this.snapshotManager.updateAllChunksStatus(snapshotId, 'rejected');
|
||||
|
||||
// 清理 UI
|
||||
await this.cleanupUI(snapshotId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
snapshotId,
|
||||
action: 'reject',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
snapshotId,
|
||||
action: 'reject',
|
||||
error: `拒绝失败: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 UI
|
||||
*/
|
||||
private async cleanupUI(snapshotId: string): Promise<void> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
// 查找对应的编辑器
|
||||
const editor = vscode.window.visibleTextEditors.find(
|
||||
e => e.document.uri.fsPath === snapshot.filePath
|
||||
);
|
||||
|
||||
if (editor) {
|
||||
// 清除装饰器
|
||||
this.inlineDecorator.clearDecorations(editor);
|
||||
}
|
||||
|
||||
// 取消快照
|
||||
this.snapshotManager.cancelSnapshot(snapshotId);
|
||||
|
||||
// 检查是否还有其他活跃快照
|
||||
const activeSnapshots = this.snapshotManager.getActiveSnapshots();
|
||||
if (activeSnapshots.length === 0) {
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, false);
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage('AI Diff Preview: 已拒绝所有变更');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Claude Hook 处理器 - 处理 after_code_edit 和 after_write_file 事件
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { SnapshotManager } from '../snapshot/snapshotManager';
|
||||
import { DiffEngine } from '../diff/diffEngine';
|
||||
import { InlineDecorator } from '../render/inlineDecorator';
|
||||
import { StatusBarManager } from '../render/statusBar';
|
||||
import { ClaudeHookEvent, HookEventType } from '../models/types';
|
||||
import { CONTEXT_KEYS } from '../models/constants';
|
||||
|
||||
export class HookHandler {
|
||||
constructor(
|
||||
private snapshotManager: SnapshotManager,
|
||||
private diffEngine: DiffEngine,
|
||||
private inlineDecorator: InlineDecorator,
|
||||
private statusBarManager: StatusBarManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 处理 Claude Hook 事件
|
||||
*/
|
||||
async handleHook(event: ClaudeHookEvent): Promise<void> {
|
||||
console.log(`收到 Hook 事件: ${event.type}, 文件: ${event.filePath}`);
|
||||
|
||||
// 检查文件是否有活跃快照
|
||||
if (this.snapshotManager.hasActiveSnapshot(event.filePath)) {
|
||||
console.log('文件已有活跃快照,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 读取当前文件内容作为 Base
|
||||
const baseContent = await this.readFileContent(event.filePath);
|
||||
if (!baseContent) {
|
||||
console.error('无法读取文件内容');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 创建 Base 快照
|
||||
const snapshotId = this.snapshotManager.createBaseSnapshot(
|
||||
event.filePath,
|
||||
baseContent
|
||||
);
|
||||
|
||||
// 3. 计算 Diff 块
|
||||
const chunks = this.diffEngine.computeChunks(baseContent, event.patch);
|
||||
|
||||
// 4. 应用 Patch 到快照
|
||||
this.snapshotManager.applyPatch(snapshotId, event.patch, chunks);
|
||||
|
||||
// 5. 渲染 Diff UI
|
||||
await this.renderDiffUI(event.filePath, snapshotId);
|
||||
|
||||
// 6. 更新状态栏
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (snapshot) {
|
||||
this.statusBarManager.update(snapshot);
|
||||
}
|
||||
|
||||
// 7. 设置上下文
|
||||
vscode.commands.executeCommand('setContext', CONTEXT_KEYS.IS_ACTIVE, true);
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`AI Diff Preview: 检测到 ${chunks.length} 个变更块`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('处理 Hook 事件失败:', error);
|
||||
vscode.window.showErrorMessage(`AI Diff Preview 处理失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 Diff UI
|
||||
*/
|
||||
private async renderDiffUI(filePath: string, snapshotId: string): Promise<void> {
|
||||
const snapshot = this.snapshotManager.getSnapshot(snapshotId);
|
||||
if (!snapshot) return;
|
||||
|
||||
// 查找对应的编辑器
|
||||
const editor = vscode.window.visibleTextEditors.find(
|
||||
e => e.document.uri.fsPath === filePath
|
||||
);
|
||||
|
||||
if (editor) {
|
||||
this.inlineDecorator.renderChunks(editor, snapshot.chunks);
|
||||
} else {
|
||||
// 如果文件未打开,尝试打开
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
const newEditor = await vscode.window.showTextDocument(doc);
|
||||
this.inlineDecorator.renderChunks(newEditor, snapshot.chunks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件内容
|
||||
*/
|
||||
private async readFileContent(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
console.error('读取文件失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Hook 监听器
|
||||
*
|
||||
* 注意:实际的 Hook 注册需要在 Claude 的配置中完成
|
||||
* 这个方法提供了一个 HTTP 端点来接收 Hook 调用
|
||||
*/
|
||||
registerHookListener(): void {
|
||||
// 方案 1: 通过 HTTP 端点接收 Hook
|
||||
// 需要在 Claude 配置中设置 Hook URL
|
||||
|
||||
// 方案 2: 通过文件监听接收 Hook
|
||||
// Claude Hook 可以将事件写入临时文件,插件监听该文件
|
||||
|
||||
// 方案 3: 通过 VSCode 命令接收
|
||||
// Claude Hook 可以调用 VSCode 命令
|
||||
console.log('Hook 监听器已注册');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user