feat:代码变更diff可视化功能实现
This commit is contained in:
184
src/utils/diffRenderer.ts
Normal file
184
src/utils/diffRenderer.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/**
|
||||
* 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> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 diff 样式
|
||||
*/
|
||||
export function getDiffStyles(): string {
|
||||
return `
|
||||
.diff-viewer {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diff-line-add {
|
||||
background: rgba(40, 167, 69, 0.15);
|
||||
}
|
||||
|
||||
.diff-line-remove {
|
||||
background: rgba(220, 53, 69, 0.15);
|
||||
}
|
||||
|
||||
.diff-line-context {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.line-num {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
padding: 0 8px;
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.line-prefix {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.diff-line-add .line-prefix {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.diff-line-remove .line-prefix {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
flex: 1;
|
||||
padding-right: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user