mirror of
https://github.com/Gmaker689/ai-diff-preview.git
synced 2026-09-29 08:31:53 +08:00
a7c992a804
- 移除旧的 DiffEngine、InlineDecorator、CodeLensProvider 等模块 - 新增 ChangeSetManager 统一管理代码变更快照 - 新增 reviewPanel.ts 实现双栏 Review Webview 面板 - 简化 hookHandler.ts,直接通过 ChangeSet 收集变更 - 简化 extension.ts 入口,移除冗余模块导入 - 更新 types.ts,精简 ChangeSet 和 ChangeEntry 类型定义 - package.json 添加 onStartupFinished 激活事件
251 lines
9.3 KiB
TypeScript
251 lines
9.3 KiB
TypeScript
/**
|
|
* Review 面板 - 内嵌 Webview 悬浮窗
|
|
*
|
|
* 双栏 Diff + Accept/Reject 按钮,不弹系统窗口
|
|
*/
|
|
|
|
import * as vscode from 'vscode';
|
|
import { ChangeSetManager } from '../snapshot/snapshotManager';
|
|
import { ChangeSet, FileChange, FileChangeStatus } from '../models/types';
|
|
|
|
export class ReviewPanel {
|
|
private panel: vscode.WebviewPanel | undefined;
|
|
|
|
constructor(
|
|
private changeSetManager: ChangeSetManager
|
|
) {}
|
|
|
|
show(): void {
|
|
const changeSet = this.changeSetManager.getCurrentSet();
|
|
if (!changeSet || changeSet.changes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.panel) {
|
|
this.panel.reveal(vscode.ViewColumn.Active);
|
|
this.sendData(changeSet);
|
|
return;
|
|
}
|
|
|
|
this.panel = vscode.window.createWebviewPanel(
|
|
'aiDiffReview',
|
|
'AI Diff Review',
|
|
{ viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
|
|
{ enableScripts: true, retainContextWhenHidden: true }
|
|
);
|
|
|
|
this.panel.webview.html = this.getHtml();
|
|
|
|
this.panel.webview.onDidReceiveMessage((msg: any) => {
|
|
switch (msg.type) {
|
|
case 'ready':
|
|
this.sendData(changeSet);
|
|
break;
|
|
case 'acceptFile':
|
|
this.changeSetManager.updateFileStatus(msg.fileId, 'accepted');
|
|
this.changeSetManager.applyFileChange(msg.fileId);
|
|
this.refresh();
|
|
break;
|
|
case 'rejectFile':
|
|
this.changeSetManager.updateFileStatus(msg.fileId, 'rejected');
|
|
this.changeSetManager.rejectFileChange(msg.fileId);
|
|
this.refresh();
|
|
break;
|
|
case 'acceptAll':
|
|
this.changeSetManager.updateAllStatus('accepted');
|
|
this.changeSetManager.applyAllAccepted();
|
|
this.refresh();
|
|
break;
|
|
case 'rejectAll':
|
|
this.changeSetManager.updateAllStatus('rejected');
|
|
this.changeSetManager.rejectAll();
|
|
this.refresh();
|
|
break;
|
|
}
|
|
});
|
|
|
|
this.panel.onDidDispose(() => { this.panel = undefined; });
|
|
}
|
|
|
|
private refresh(): void {
|
|
const changeSet = this.changeSetManager.getCurrentSet();
|
|
if (changeSet && this.panel) {
|
|
this.sendData(changeSet);
|
|
}
|
|
}
|
|
|
|
private sendData(changeSet: ChangeSet): void {
|
|
this.panel?.webview.postMessage({ type: 'init', changeSet });
|
|
}
|
|
|
|
dispose(): void {
|
|
this.panel?.dispose();
|
|
this.panel = undefined;
|
|
}
|
|
|
|
private getHtml(): string {
|
|
return /*html*/`<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
body { font-family:var(--vscode-font-family),sans-serif; font-size:13px; color:var(--vscode-foreground); background:var(--vscode-editor-background); }
|
|
|
|
.toolbar {
|
|
display:flex; align-items:center; justify-content:space-between;
|
|
padding:10px 16px; background:var(--vscode-titleBar-activeBackground);
|
|
border-bottom:1px solid var(--vscode-panel-border); position:sticky; top:0; z-index:10;
|
|
}
|
|
.toolbar h2 { font-size:14px; font-weight:600; }
|
|
.toolbar .actions { display:flex; gap:8px; }
|
|
.btn {
|
|
padding:6px 16px; border:none; border-radius:4px; cursor:pointer;
|
|
font-size:12px; font-weight:500; transition:opacity .15s;
|
|
}
|
|
.btn:hover { opacity:.85; }
|
|
.btn-accept { background:#2ea043; color:#fff; }
|
|
.btn-reject { background:#da3633; color:#fff; }
|
|
|
|
.files { padding:8px 0; }
|
|
|
|
.file-card {
|
|
margin:6px 12px; border:1px solid var(--vscode-panel-border);
|
|
border-radius:6px; overflow:hidden;
|
|
}
|
|
.file-header {
|
|
display:flex; align-items:center; justify-content:space-between;
|
|
padding:8px 12px; background:var(--vscode-sideBar-background);
|
|
border-bottom:1px solid var(--vscode-panel-border); cursor:pointer;
|
|
}
|
|
.file-path { font-weight:500; display:flex; align-items:center; gap:8px; }
|
|
.badge {
|
|
font-size:10px; padding:2px 6px; border-radius:3px; font-weight:600;
|
|
}
|
|
.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-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); }
|
|
.diff-title {
|
|
padding:4px 12px; font-size:11px; font-weight:600;
|
|
color:var(--vscode-descriptionForeground); border-bottom:1px solid var(--vscode-panel-border);
|
|
}
|
|
.diff-side.left .diff-title { background:rgba(248,81,73,.06); }
|
|
.diff-side.right .diff-title { background:rgba(46,160,67,.06); }
|
|
.diff-line { display:flex; min-height:20px; }
|
|
.ln { min-width:36px; padding:0 8px; text-align:right; color:var(--vscode-editorLineNumber-foreground); user-select:none; flex-shrink:0; }
|
|
.lc { padding:0 10px; white-space:pre; flex:1; }
|
|
.diff-line.add { background:rgba(46,160,67,.12); }
|
|
.diff-line.del { background:rgba(248,81,73,.12); }
|
|
.diff-line.ctx { opacity:.55; }
|
|
.diff-side.left .diff-line.add { background:transparent; }
|
|
.diff-side.right .diff-line.del { background:transparent; }
|
|
.empty-line { min-height:20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="toolbar">
|
|
<h2>🔍 AI Diff Review</h2>
|
|
<div class="actions">
|
|
<button class="btn btn-accept" onclick="acceptAll()">✓ Accept All</button>
|
|
<button class="btn btn-reject" onclick="rejectAll()">✗ Reject All</button>
|
|
</div>
|
|
</div>
|
|
<div class="files" id="files"></div>
|
|
|
|
<script>
|
|
const vscode = acquireVsCodeApi();
|
|
let cs = null;
|
|
|
|
vscode.postMessage({type:'ready'});
|
|
|
|
window.addEventListener('message', e => {
|
|
const m = e.data;
|
|
if (m.type === 'init') { cs = m.changeSet; render(); }
|
|
});
|
|
|
|
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 render(){
|
|
if(!cs) return;
|
|
const el = document.getElementById('files');
|
|
el.innerHTML = '';
|
|
cs.changes.forEach(c => el.appendChild(card(c)));
|
|
}
|
|
|
|
function card(c){
|
|
const d = document.createElement('div');
|
|
d.className = 'file-card';
|
|
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>';
|
|
|
|
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>' +
|
|
(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>' +
|
|
'</span>' : ''
|
|
) +
|
|
'</div>' +
|
|
'<div class="diff-body">' + buildDiff(c) + '</div>';
|
|
return d;
|
|
}
|
|
|
|
function buildDiff(c){
|
|
if(c.type==='create') return buildSingle(c.newLines||[],'right','新文件');
|
|
if(c.type==='delete') return buildSingle(c.oldLines||[],'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>';
|
|
});
|
|
return h + '</div></div>';
|
|
}
|
|
|
|
function buildDual(dlines){
|
|
let lh='', rh='';
|
|
dlines.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 esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
</script>
|
|
<style>.collapsed .diff-body{display:none;}</style>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
}
|