/** * 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 { 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 { 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 { 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 { 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 = { '.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'; } }