fix:修复diff范围问题

This commit is contained in:
2026-06-18 07:58:54 +08:00
parent a7c992a804
commit f06914994d
5 changed files with 1122 additions and 594 deletions
+120 -18
View File
@@ -1,12 +1,15 @@
/**
* Review 面板 - 内嵌 Webview 悬浮窗
*
* 双栏 Diff + Accept/Reject 按钮,不弹系统窗口
* ★ v0.2.0: 支持 per-edit 和 per-file 两级 Accept/Reject
* - 每个文件卡片展开后显示各次编辑记录
* - 每次编辑有独立的 ✓/✗ 按钮
* - 文件级 Accept/Reject 一键处理所有编辑
*/
import * as vscode from 'vscode';
import { ChangeSetManager } from '../snapshot/snapshotManager';
import { ChangeSet, FileChange, FileChangeStatus } from '../models/types';
import { ChangeSet, FileChange, EditRecord, ChangeStatus } from '../models/types';
export class ReviewPanel {
private panel: vscode.WebviewPanel | undefined;
@@ -41,23 +44,33 @@ export class ReviewPanel {
case 'ready':
this.sendData(changeSet);
break;
// 单次编辑 Accept/Reject
case 'acceptEdit':
this.changeSetManager.acceptEditRecord(msg.fileId, msg.editId);
this.refresh();
break;
case 'rejectEdit':
this.changeSetManager.rejectEditRecord(msg.fileId, msg.editId);
this.refresh();
break;
// 整个文件 Accept/Reject
case 'acceptFile':
this.changeSetManager.updateFileStatus(msg.fileId, 'accepted');
this.changeSetManager.applyFileChange(msg.fileId);
this.changeSetManager.acceptFile(msg.fileId);
this.refresh();
break;
case 'rejectFile':
this.changeSetManager.updateFileStatus(msg.fileId, 'rejected');
this.changeSetManager.rejectFileChange(msg.fileId);
this.changeSetManager.rejectFile(msg.fileId);
this.refresh();
break;
// 全局操作
case 'acceptAll':
this.changeSetManager.updateAllStatus('accepted');
this.changeSetManager.applyAllAccepted();
this.changeSetManager.acceptAll();
this.refresh();
break;
case 'rejectAll':
this.changeSetManager.updateAllStatus('rejected');
this.changeSetManager.rejectAll();
this.refresh();
break;
@@ -124,12 +137,12 @@ export class ReviewPanel {
}
.badge-modify { background:rgba(210,153,34,.2); color:#d29922; }
.badge-create { background:rgba(46,160,67,.2); color:#3fb950; }
.badge-delete { background:rgba(248,81,73,.2); color:#f85149; }
.badge-accepted { background:rgba(46,160,67,.3); color:#3fb950; }
.badge-rejected { background:rgba(248,81,73,.3); color:#f85149; text-decoration:line-through; }
.file-actions { display:flex; gap:6px; }
.btn-sm { padding:3px 10px; border:none; border-radius:3px; cursor:pointer; font-size:11px; }
/* Diff 双栏 */
.diff-wrap { display:grid; grid-template-columns:1fr 1fr; font-family:'Cascadia Code','Fira Code',Consolas,monospace; font-size:12px; line-height:20px; }
.diff-side { overflow-x:auto; }
.diff-side.left { border-right:1px solid var(--vscode-panel-border); }
@@ -148,6 +161,28 @@ export class ReviewPanel {
.diff-side.left .diff-line.add { background:transparent; }
.diff-side.right .diff-line.del { background:transparent; }
.empty-line { min-height:20px; }
/* ★ 编辑记录列表 */
.edits-list { padding:0; }
.edit-item {
border-bottom:1px solid var(--vscode-panel-border);
}
.edit-item:last-child { border-bottom:none; }
.edit-header {
display:flex; align-items:center; justify-content:space-between;
padding:6px 12px; background:var(--vscode-editor-background); cursor:pointer;
font-size:12px;
}
.edit-header:hover { background:var(--vscode-list-hoverBackground); }
.edit-label { display:flex; align-items:center; gap:6px; color:var(--vscode-descriptionForeground); }
.edit-label .edit-num { font-weight:600; color:var(--vscode-foreground); }
.edit-status { font-size:10px; }
.edit-actions { display:flex; gap:4px; }
.btn-xs { padding:2px 8px; border:none; border-radius:3px; cursor:pointer; font-size:10px; }
.summary-stats { font-size:11px; color:var(--vscode-descriptionForeground); padding:4px 12px; }
.stat-add { color:#3fb950; }
.stat-del { color:#f85149; }
</style>
</head>
<body>
@@ -175,6 +210,8 @@ function acceptAll(){ vscode.postMessage({type:'acceptAll'}); }
function rejectAll(){ vscode.postMessage({type:'rejectAll'}); }
function acceptFile(id){ vscode.postMessage({type:'acceptFile',fileId:id}); }
function rejectFile(id){ vscode.postMessage({type:'rejectFile',fileId:id}); }
function acceptEdit(fid, eid){ vscode.postMessage({type:'acceptEdit',fileId:fid,editId:eid}); }
function rejectEdit(fid, eid){ vscode.postMessage({type:'rejectEdit',fileId:fid,editId:eid}); }
function render(){
if(!cs) return;
@@ -189,34 +226,99 @@ function card(c){
const name = c.filePath.split(/[/\\\\]/).pop();
const typeMap = {modify:'修改',create:'新增',delete:'删除'};
const badgeCls = 'badge badge-' + c.type;
let statusBadge = '';
if(c.status==='accepted') statusBadge = '<span class="badge badge-accepted">已接受</span>';
if(c.status==='rejected') statusBadge = '<span class="badge badge-rejected">已拒绝</span>';
// 统计增减行数
let adds = 0, dels = 0;
c.diffLines.forEach(l => { if(l.type==='add') adds++; if(l.type==='delete') dels++; });
d.innerHTML =
'<div class="file-header" onclick="this.nextElementSibling.classList.toggle(' + "'" + 'collapsed' + "'" + ')">' +
'<span class="file-path">📄 ' + esc(name) + ' <span class="' + badgeCls + '">' + typeMap[c.type] + '</span>' + statusBadge + '</span>' +
'<span class="summary-stats"><span class="stat-add">+' + adds + '</span> <span class="stat-del">-' + dels + '</span></span>' +
(c.status==='pending' ?
'<span class="file-actions">' +
'<button class="btn-sm btn-accept" onclick="event.stopPropagation();acceptFile(' + "'" + c.id + "'" + ')">✓</button>' +
'<button class="btn-sm btn-reject" onclick="event.stopPropagation();rejectFile(' + "'" + c.id + "'" + ')">✗</button>' +
'<button class="btn-sm btn-accept" onclick="event.stopPropagation();acceptFile(' + "'" + c.id + "'" + ')">✓ 全部接受</button>' +
'<button class="btn-sm btn-reject" onclick="event.stopPropagation();rejectFile(' + "'" + c.id + "'" + ')">✗ 全部拒绝</button>' +
'</span>' : ''
) +
'</div>' +
'<div class="diff-body">' + buildDiff(c) + '</div>';
'<div class="diff-body">' +
buildEdits(c) +
'<div style="padding:6px 12px;font-size:11px;color:var(--vscode-descriptionForeground);border-top:1px solid var(--vscode-panel-border);">📊 聚合 Diff(原始 → 最新)</div>' +
buildDiff(c) +
'</div>';
return d;
}
/** ★ 构建编辑记录列表 */
function buildEdits(c){
if(!c.edits || c.edits.length === 0) return '';
const toolMap = {Edit:'✏️ 编辑', Write:'📝 写入'};
let h = '<div class="edits-list">';
c.edits.forEach((edit, i) => {
let statusCls = '', statusText = '';
if(edit.status==='accepted'){ statusCls='badge-accepted'; statusText='已接受'; }
if(edit.status==='rejected'){ statusCls='badge-rejected'; statusText='已拒绝'; }
let editAdds = 0, editDels = 0;
(edit.diffLines||[]).forEach(l => { if(l.type==='add') editAdds++; if(l.type==='delete') editDels++; });
h += '<div class="edit-item">' +
'<div class="edit-header" onclick="event.stopPropagation();this.nextElementSibling.classList.toggle(' + "'" + 'collapsed' + "'" + ')">' +
'<span class="edit-label">' +
'<span class="edit-num">#' + (i+1) + '</span> ' + (toolMap[edit.toolName]||edit.toolName) +
' <span class="stat-add">+' + editAdds + '</span> <span class="stat-del">-' + editDels + '</span>' +
(statusText ? ' <span class="badge ' + statusCls + ' edit-status">' + statusText + '</span>' : '') +
'</span>' +
(edit.status==='pending' ?
'<span class="edit-actions">' +
'<button class="btn-xs btn-accept" onclick="event.stopPropagation();acceptEdit(' + "'" + c.id + "','" + edit.id + "'" + ')">✓</button>' +
'<button class="btn-xs btn-reject" onclick="event.stopPropagation();rejectEdit(' + "'" + c.id + "','" + edit.id + "'" + ')">✗</button>' +
'</span>' : ''
) +
'</div>' +
'<div class="">' + buildEditDiff(edit) + '</div>' +
'</div>';
});
h += '</div>';
return h;
}
/** 单次编辑的 Diff */
function buildEditDiff(edit){
let lh='', rh='';
(edit.diffLines||[]).forEach(l => {
if(l.type==='context'){
lh += '<div class="diff-line ctx"><span class="ln">'+l.oldLineNum+'</span><span class="lc">'+esc(l.content)+'</span></div>';
rh += '<div class="diff-line ctx"><span class="ln">'+l.newLineNum+'</span><span class="lc">'+esc(l.content)+'</span></div>';
} else if(l.type==='delete'){
lh += '<div class="diff-line del"><span class="ln">'+l.oldLineNum+'</span><span class="lc">'+esc(l.content)+'</span></div>';
rh += '<div class="diff-line empty-line"></div>';
} else if(l.type==='add'){
lh += '<div class="diff-line empty-line"></div>';
rh += '<div class="diff-line add"><span class="ln">'+l.newLineNum+'</span><span class="lc">'+esc(l.content)+'</span></div>';
}
});
return '<div class="diff-wrap">' +
'<div class="diff-side left"><div class="diff-title">编辑前</div>'+lh+'</div>' +
'<div class="diff-side right"><div class="diff-title">编辑后</div>'+rh+'</div>' +
'</div>';
}
function buildDiff(c){
if(c.type==='create') return buildSingle(c.newLines||[],'right','新文件');
if(c.type==='delete') return buildSingle(c.oldLines||[],'left','已删除');
if(c.type==='create') return buildSingle(c.latestContent.split('\\n'),'right','新文件');
if(c.type==='delete') return buildSingle(c.originalContent.split('\\n'),'left','已删除');
return buildDual(c.diffLines||[]);
}
function buildSingle(lines,side,title){
let h = '<div class="diff-wrap"><div class="diff-side ' + side + '"><div class="diff-title">' + title + '</div>';
lines.forEach(l => {
h += '<div class="diff-line add"><span class="ln">' + (l.num||'') + '</span><span class="lc">' + esc(l.content||l) + '</span></div>';
lines.forEach((l,i) => {
h += '<div class="diff-line add"><span class="ln">' + (i+1) + '</span><span class="lc">' + esc(l) + '</span></div>';
});
return h + '</div></div>';
}
@@ -243,7 +345,7 @@ function buildDual(dlines){
function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
</script>
<style>.collapsed .diff-body{display:none;}</style>
<style>.collapsed .diff-body{display:none;} .collapsed{display:none;}</style>
</body>
</html>`;
}