feat(extension): 添加代码差异对比功能

- 在 extension.ts 中实现 generateCodeDiff 函数,用于生成代码差异
- 在 webview 中添加代码差异模态框,用于显示代码对比结果
- 优化消息添加逻辑,支持显示代码差异按钮
- 添加模态框关闭功能
master
钟良源 6 months ago
parent 52b1b7bcc9
commit 3671cd1b93

@ -40,21 +40,31 @@ document.addEventListener('DOMContentLoaded', () => {
return markdownPatterns.some(pattern => pattern.test(content)); return markdownPatterns.some(pattern => pattern.test(content));
} }
function addMessage(role, content) { // 处理添加消息
function addMessage(role, content, codeDiff) {
const msgDiv = document.createElement('div'); const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`; msgDiv.className = `message ${role}`;
// 渲染内容 // 渲染内容
if (isMarkdown(content)) { if (isMarkdown(content)) {
console.log("content:", content) console.log("content:", content);
msgDiv.innerHTML = marked.parse(content); msgDiv.innerHTML = marked.parse(content);
console.log("msgDiv.innerHTML:", msgDiv.innerHTML) console.log("msgDiv.innerHTML:", msgDiv.innerHTML);
} else { } else {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = content; div.textContent = content;
msgDiv.appendChild(div); msgDiv.appendChild(div);
} }
// 如果有代码差异,添加查看差异按钮
if (codeDiff) {
const diffButton = document.createElement('button');
diffButton.className = 'diff-button';
diffButton.textContent = '查看代码差异';
diffButton.onclick = () => showCodeDiff(codeDiff);
msgDiv.appendChild(diffButton);
}
chatBox.appendChild(msgDiv); chatBox.appendChild(msgDiv);
// 高亮代码块 // 高亮代码块
@ -67,6 +77,36 @@ document.addEventListener('DOMContentLoaded', () => {
chatBox.scrollTop = chatBox.scrollHeight; chatBox.scrollTop = chatBox.scrollHeight;
} }
// 显示代码差异
function showCodeDiff(codeDiff) {
const diffModal = document.getElementById('diff-modal');
const diffContent = document.getElementById('diff-content');
diffContent.innerHTML = '';
// 显示添加的行
if (codeDiff.added && codeDiff.added.length > 0) {
codeDiff.added.forEach(line => {
const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-added';
diffLine.textContent = `+ ${line}`;
diffContent.appendChild(diffLine);
});
}
// 显示删除的行
if (codeDiff.removed && codeDiff.removed.length > 0) {
codeDiff.removed.forEach(line => {
const diffLine = document.createElement('div');
diffLine.className = 'diff-line diff-removed';
diffLine.textContent = `- ${line}`;
diffContent.appendChild(diffLine);
});
}
diffModal.classList.remove('hidden');
}
function showLoading() { function showLoading() {
loading.classList.remove('hidden'); loading.classList.remove('hidden');
} }
@ -79,7 +119,7 @@ document.addEventListener('DOMContentLoaded', () => {
const message = event.data; const message = event.data;
console.log(message) console.log(message)
if (message.command === 'addMessage') { if (message.command === 'addMessage') {
addMessage(message.role, message.content); addMessage(message.role, message.content,message.codeDiff);
} else if (message.command === 'hideLoading') { } else if (message.command === 'hideLoading') {
hideLoading(); hideLoading();
} else if (message.command === 'addToInput') { } else if (message.command === 'addToInput') {
@ -182,4 +222,15 @@ document.addEventListener('DOMContentLoaded', () => {
contextPlaceholder.classList.remove('hidden'); contextPlaceholder.classList.remove('hidden');
selectedCodeForContext = ''; selectedCodeForContext = '';
}); });
// 关闭差异模态框
document.getElementById('close-diff-modal').addEventListener('click', () => {
document.getElementById('diff-modal').classList.add('hidden');
});
// 点击模态框外部关闭
document.getElementById('diff-modal').addEventListener('click', (e) => {
if (e.target.id === 'diff-modal') {
document.getElementById('diff-modal').classList.add('hidden');
}
});
}); });

@ -322,3 +322,49 @@ body {
padding: 20px; padding: 20px;
color: #abb2bf; color: #abb2bf;
} }
.diff-modal-content {
width: 90%;
max-width: 900px;
}
.diff-content {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
background-color: #1e1e1e;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
}
.diff-line {
white-space: pre;
}
.diff-added {
background-color: rgba(0, 255, 0, 0.1);
color: #4ec9b0;
}
.diff-removed {
background-color: rgba(255, 0, 0, 0.1);
color: #f48771;
}
.diff-unchanged {
color: #d4d4d4;
}
.diff-button {
margin-top: 10px;
padding: 5px 10px;
background-color: #007acc;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.diff-button:hover {
background-color: #005a9e;
}

@ -1,7 +1,10 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
import * as diff from 'diff';
let panel: vscode.WebviewPanel | undefined; let panel: vscode.WebviewPanel | undefined;
let currentSessionHistory: { role: string; content: string }[] = [];
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log('Extension activated'); console.log('Extension activated');
@ -127,6 +130,9 @@ function openWebview(
globalState: vscode.Memento, globalState: vscode.Memento,
context: vscode.ExtensionContext // 添加 context 参数 context: vscode.ExtensionContext // 添加 context 参数
) { ) {
// 每次打开新的 Webview 时清空当前会话历史
currentSessionHistory = [];
panel = vscode.window.createWebviewPanel( panel = vscode.window.createWebviewPanel(
'aiChatWebview', 'aiChatWebview',
'AI Chat', 'AI Chat',
@ -166,22 +172,30 @@ function openWebview(
case 'ask': case 'ask':
const question = message.text; const question = message.text;
const fileContent = message.fileContent; const fileContent = message.fileContent;
const history = globalState.get<{ role: string; content: string }[]>('chatHistory', []); const history = currentSessionHistory;
const newHistory = [...history, { role: 'user', content: question }]; currentSessionHistory = [...history, { role: 'user', content: question }];
globalState.update('chatHistory', newHistory);
panel.webview.postMessage({ command: 'addMessage', role: 'user', content: question }); panel.webview.postMessage({ command: 'addMessage', role: 'user', content: question });
try { try {
const response = await callQwenAPI(question, history, fileContent, context); const response = await callQwenAPI(question, history, fileContent, context);
console.log("response:",response) console.log("response:", response);
const aiMessage = response.choices[0]?.message?.content || '未获取到回复'; const aiMessage = response.choices[0]?.message?.content || '未获取到回复';
const updatedHistory = [...newHistory, { role: 'assistant', content: aiMessage }]; // 检查是否包含代码修改
globalState.update('chatHistory', updatedHistory); const codeDiff = generateCodeDiff(fileContent, aiMessage);
console.log("codeDiff:",codeDiff)
// 更新当前会话历史
currentSessionHistory = [...currentSessionHistory, { role: 'assistant', content: aiMessage }];
panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: aiMessage }); panel.webview.postMessage({
command: 'addMessage',
role: 'ai',
content: aiMessage,
codeDiff: codeDiff // 发送代码差异信息
});
} catch (error) { } catch (error) {
panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message }); panel.webview.postMessage({ command: 'addMessage', role: 'ai', content: '调用失败:' + error.message });
} }
@ -253,6 +267,13 @@ function openWebview(
undefined, undefined,
context.subscriptions context.subscriptions
); );
// 添加 dispose 监听器
panel.onDidDispose(() => {
// 面板关闭时清空当前会话历史
currentSessionHistory = [];
panel = undefined;
}, null, context.subscriptions);
} }
async function callQwenAPI( async function callQwenAPI(
@ -269,6 +290,7 @@ async function callQwenAPI(
role: 'system', role: 'system',
content: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。` content: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。`
}, },
...history,
{ {
role: 'user', role: 'user',
content: `【当前文件内容】: content: `【当前文件内容】:
@ -305,6 +327,58 @@ async function callQwenAPI(
} }
} }
function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[] } | null {
console.log("模型返回值:",aiResponse)
// 提取代码块中的代码
const codeBlockRegex = /```(?:([a-zA-Z0-9]+))?\s*[\n\r]([\s\S]*?)\s*```/g;
const matches = [...aiResponse.matchAll(codeBlockRegex)];
console.log("正则过滤:",matches)
if (matches.length > 0) {
const modifiedCode = matches[0][2]; // 提取第一个代码块中的代码
// 智能标准化函数
const smartNormalize = (code: string): string => {
return code
.split('\n')
.map(line => line.trimEnd()) // 使用 trimEnd() 移除行尾空白
.filter(line => line !== null && line !== undefined) // 过滤空行
.join('\n')
.trim(); // 整体去除首尾空白
};
const normalizedOriginal = smartNormalize(originalCode);
const normalizedModified = smartNormalize(modifiedCode);
console.log("原始内容:",normalizedOriginal)
console.log("模型内容:",normalizedModified)
// 如果标准化后的内容相同,则没有差异
if (normalizedOriginal === normalizedModified) {
return null;
}
// 使用更精确的 diff 算法
const diffResult = diff.diffLines(normalizedOriginal, normalizedModified, { ignoreWhitespace: true });
const added: string[] = [];
const removed: string[] = [];
diffResult.forEach(part => {
if (part.added) {
added.push(part.value);
} else if (part.removed) {
removed.push(part.value);
}
});
// 只有当确实有差异时才返回
if (added.length > 0 || removed.length > 0) {
return { added, removed };
}
}
return null;
}
function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string { function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlightScriptUri:vscode.Uri,highlightStyleUri:vscode.Uri,markedScriptUri:vscode.Uri): string {
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
@ -353,6 +427,19 @@ function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlight
</div> </div>
</div> </div>
<!-- -->
<div id="diff-modal" class="modal hidden">
<div class="modal-content diff-modal-content">
<div class="modal-header">
<h3></h3>
<span id="close-diff-modal" class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div id="diff-content" class="diff-content"></div>
</div>
</div>
</div>
<!-- --> <!-- -->
<form id="chat-form"> <form id="chat-form">
<input type="text" id="user-input" placeholder="输入你的问题..." required /> <input type="text" id="user-input" placeholder="输入你的问题..." required />

Loading…
Cancel
Save