Files
IC-Coder-Plugin/src/utils/diffRenderer.ts
Roe-xin 5f88c7ceac feat: 优化代码变更面板样式和交互
- 优化变更面板和 diff 视图样式
   - 新增全部采纳和全部拒绝按钮
   - 修复删除文件的变更追踪和采纳逻辑
   - 整个标题栏可点击展开/收起
   - 增强视觉效果和用户体验
2026-03-02 10:37:45 +08:00

204 lines
4.5 KiB
TypeScript

/**
* 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 = '<div class="diff-viewer">';
for (const line of diffLines) {
const lineClass = `diff-line diff-line-${line.type}`;
const oldNum = line.oldLineNumber ? `<span class="line-num">${line.oldLineNumber}</span>` : '<span class="line-num"></span>';
const newNum = line.newLineNumber ? `<span class="line-num">${line.newLineNumber}</span>` : '<span class="line-num"></span>';
const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' ';
const escapedContent = escapeHtml(line.content);
html += `
<div class="${lineClass}">
${oldNum}
${newNum}
<span class="line-prefix">${prefix}</span>
<span class="line-content">${escapedContent}</span>
</div>
`;
}
html += '</div>';
return html;
}
/**
* 转义 HTML 特殊字符
*/
function escapeHtml(text: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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;
}
`;
}