/** * Diff 渲染工具 * 功能:生成代码差异的 HTML 展示 * 依赖:无 * 使用场景:在变更面板中展示文件修改的 diff */ interface DiffLine { type: 'add' | 'remove' | 'context'; content: string; oldLineNumber?: number; newLineNumber?: number; } /** * 简单的 diff 算法(基于行) */ export function generateDiff(oldContent: string, newContent: string): DiffLine[] { const oldLines = oldContent.split('\n'); const newLines = newContent.split('\n'); const result: DiffLine[] = []; let oldIndex = 0; let newIndex = 0; while (oldIndex < oldLines.length || newIndex < newLines.length) { const oldLine = oldLines[oldIndex]; const newLine = newLines[newIndex]; if (oldIndex >= oldLines.length) { // 只剩新行 result.push({ type: 'add', content: newLine, newLineNumber: newIndex + 1 }); newIndex++; } else if (newIndex >= newLines.length) { // 只剩旧行 result.push({ type: 'remove', content: oldLine, oldLineNumber: oldIndex + 1 }); oldIndex++; } else if (oldLine === newLine) { // 相同行 result.push({ type: 'context', content: oldLine, oldLineNumber: oldIndex + 1, newLineNumber: newIndex + 1 }); oldIndex++; newIndex++; } else { // 不同行,标记为删除和添加 result.push({ type: 'remove', content: oldLine, oldLineNumber: oldIndex + 1 }); result.push({ type: 'add', content: newLine, newLineNumber: newIndex + 1 }); oldIndex++; newIndex++; } } return result; } /** * 将 diff 结果渲染为 HTML */ export function renderDiffHtml(diffLines: DiffLine[]): string { let html = '
'; for (const line of diffLines) { const lineClass = `diff-line diff-line-${line.type}`; const oldNum = line.oldLineNumber ? `${line.oldLineNumber}` : ''; const newNum = line.newLineNumber ? `${line.newLineNumber}` : ''; const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '; const escapedContent = escapeHtml(line.content); html += `
${oldNum} ${newNum} ${prefix} ${escapedContent}
`; } html += '
'; return html; } /** * 转义 HTML 特殊字符 */ function escapeHtml(text: string): string { const map: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } /** * 获取 diff 样式 */ export function getDiffStyles(): string { return ` .diff-viewer { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.6; background: var(--vscode-editor-background); border: 1px solid var(--vscode-panel-border); border-radius: 6px; overflow: hidden; } .diff-line { display: flex; align-items: center; padding: 4px 0; white-space: pre; transition: background 0.15s; } .diff-line:hover { background: var(--vscode-list-hoverBackground); } .diff-line-add { background: rgba(40, 167, 69, 0.2); border-left: 3px solid #28a745; } .diff-line-add:hover { background: rgba(40, 167, 69, 0.25); } .diff-line-remove { background: rgba(220, 53, 69, 0.2); border-left: 3px solid #dc3545; } .diff-line-remove:hover { background: rgba(220, 53, 69, 0.25); } .diff-line-context { background: transparent; border-left: 3px solid transparent; } .line-num { display: inline-block; width: 45px; text-align: right; padding: 0 10px; color: var(--vscode-editorLineNumber-foreground); user-select: none; flex-shrink: 0; font-size: 11px; opacity: 0.7; } .line-prefix { display: inline-block; width: 24px; text-align: center; font-weight: bold; flex-shrink: 0; font-size: 14px; } .diff-line-add .line-prefix { color: #28a745; } .diff-line-remove .line-prefix { color: #dc3545; } .line-content { flex: 1; padding: 0 12px 0 8px; overflow-x: auto; } `; }