feat(extension): 添加创建新文件功能并重构代码

- 在 extension.ts 中添加了创建新文件的逻辑
- 在 webview 中增加了创建文件按钮和相关事件处理
- 重构了 callQwenAPI 和 generateCodeDiff 函数,移至新的 utils 文件夹
- 新增 getFileExtension 函数用于获取文件扩展名
master
钟良源 6 months ago
parent 196a5eba2a
commit 2b51c4cbde

@ -73,6 +73,18 @@ document.addEventListener('DOMContentLoaded', () => {
msgDiv.appendChild(diffButton); msgDiv.appendChild(diffButton);
} }
// 如果是AI消息且包含代码块添加"创建文件"按钮
if (role === 'ai') {
const codeBlocks = msgDiv.querySelectorAll('pre code');
codeBlocks.forEach((block, index) => {
const createFileButton = document.createElement('button');
createFileButton.className = 'create-file-button';
createFileButton.textContent = `生成代码文件`;
createFileButton.onclick = () => createNewFileFromCode(block.textContent, index);
msgDiv.appendChild(createFileButton);
});
}
chatBox.appendChild(msgDiv); chatBox.appendChild(msgDiv);
// 高亮代码块 // 高亮代码块
@ -281,10 +293,10 @@ document.addEventListener('DOMContentLoaded', () => {
// 关闭模态框 // 关闭模态框
document.getElementById('file-browser-modal').classList.add('hidden'); document.getElementById('file-browser-modal').classList.add('hidden');
// 清空文件搜索输入框 // 清空文件搜索输入框
document.getElementById('file-search-input').value = ''; document.getElementById('file-search-input').value = '';
// 保存当前上下文文件路径 // 保存当前上下文文件路径
contextFilePath = file.path; contextFilePath = file.path;
}); });
@ -312,12 +324,12 @@ document.addEventListener('DOMContentLoaded', () => {
} else if (message.command === 'restoreState') { } else if (message.command === 'restoreState') {
// 恢复面板状态 // 恢复面板状态
currentSessionHistory = message.history || []; currentSessionHistory = message.history || [];
// 恢复显示历史消息 // 恢复显示历史消息
currentSessionHistory.forEach(msg => { currentSessionHistory.forEach(msg => {
addMessage(msg.role, msg.content, null); addMessage(msg.role, msg.content, null);
}); });
// 恢复工作区变更 // 恢复工作区变更
if (message.workspaceChanges) { if (message.workspaceChanges) {
updateWorkspaceFiles(message.workspaceChanges); updateWorkspaceFiles(message.workspaceChanges);
@ -374,11 +386,62 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('close-diff-modal').addEventListener('click', () => { document.getElementById('close-diff-modal').addEventListener('click', () => {
document.getElementById('diff-modal').classList.add('hidden'); document.getElementById('diff-modal').classList.add('hidden');
}); });
// 点击模态框外部关闭 // 点击模态框外部关闭
document.getElementById('diff-modal').addEventListener('click', (e) => { document.getElementById('diff-modal').addEventListener('click', (e) => {
if (e.target.id === 'diff-modal') { if (e.target.id === 'diff-modal') {
document.getElementById('diff-modal').classList.add('hidden'); document.getElementById('diff-modal').classList.add('hidden');
} }
}); });
// 从代码创建新文件
function createNewFileFromCode(codeContent, index) {
// 简单的语言检测
const language = detectLanguage(codeContent);
// 请求创建文件
const fileName = `new_file_${index + 1}${getFileExtension(language)}`;
vscode.postMessage({
command: 'createNewFile',
fileName: fileName,
content: codeContent,
language: language
});
}
// 简单的语言检测
function detectLanguage(code) {
// 可以根据代码特征进行简单判断
if (code.includes('import React') || code.includes('from react')) {
return 'javascript'; // React代码
} else if (code.includes('public class') || code.includes('private static')) {
return 'java';
} else if (code.includes('def ') && code.includes(':')) {
return 'python';
} else if (code.includes('function ') || code.includes('const ') || code.includes('let ')) {
return 'javascript';
} else if (code.includes('interface ') && code.includes('export ')) {
return 'typescript';
} else if (code.includes('<?php')) {
return 'php';
} else if (code.includes('using System')) {
return 'csharp';
}
return 'text'; // 默认
}
// 根据语言获取文件扩展名
function getFileExtension(language) {
const extensions = {
'javascript': '.js',
'typescript': '.ts',
'python': '.py',
'java': '.java',
'html': '.html',
'css': '.css',
'php': '.php',
'csharp': '.cs',
'text': '.txt'
};
return extensions[language] || '.txt';
}
}); });

@ -367,13 +367,27 @@ code {
border: none; border: none;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
font-size: 12px;
} }
.diff-button:hover { .diff-button:hover {
background-color: #005a9e; background-color: #005a9e;
} }
.create-file-button {
margin-top: 10px;
margin-left: 10px;
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.create-file-button:hover {
background-color: #45a049;
}
/* 工作区容器 */ /* 工作区容器 */
.workspace-container { .workspace-container {
margin: 10px 0; margin: 10px 0;

@ -1,6 +1,7 @@
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'; import {callQwenAPI} from "./utils/modelApi";
import {generateCodeDiff,getFileExtension} from "./utils/common";
// 定义全局变量来存储面板状态 // 定义全局变量来存储面板状态
let currentSessionHistory: { role: string; content: string }[] = []; let currentSessionHistory: { role: string; content: string }[] = [];
@ -320,6 +321,40 @@ class AISidebarViewProvider implements vscode.WebviewViewProvider {
changes: workspaceChanges changes: workspaceChanges
}); });
break; break;
case 'createNewFile':
// 创建新文件功能
try {
const { fileName, content, language } = message;
// 获取根工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage('未找到工作区');
return;
}
// 根据语言确定文件扩展名
const fileExtension = getFileExtension(language);
const fullFileName = fileName.endsWith(fileExtension) ? fileName : `${fileName}${fileExtension}`;
// 创建文件路径(在根目录下)
const filePath = path.join(workspaceFolders[0].uri.fsPath, fullFileName);
const fileUri = vscode.Uri.file(filePath);
// 将内容写入文件
const encodedContent = new TextEncoder().encode(content);
await vscode.workspace.fs.writeFile(fileUri, encodedContent);
// 在编辑器中打开新创建的文件
await vscode.window.showTextDocument(fileUri, {
viewColumn: vscode.ViewColumn.One
});
vscode.window.showInformationMessage(`文件 ${fullFileName} 已创建`);
} catch (error: any) {
vscode.window.showErrorMessage(`生成代码文件失败: ${error.message}`);
}
break;
case 'restoreState': case 'restoreState':
// 恢复面板状态 // 恢复面板状态
currentSessionHistory = message.history || []; currentSessionHistory = message.history || [];
@ -388,111 +423,6 @@ class AISidebarViewProvider implements vscode.WebviewViewProvider {
} }
} }
async function callQwenAPI(
question: string,
history: any[],
fileContent: string,
context: vscode.ExtensionContext
): Promise<any> {
const apiKey = 'dev-token';
const url = 'https://aicomp.ngsk.tech:7001/api/v1/chat/completions';
const messages = [
{
role: 'system',
content: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。`
},
...history,
{
role: 'user',
content: `【当前文件内容】:
\`\`\`${fileContent}\`\`\`
:
${question}`
}
];
console.log("messages:",messages)
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'qwen2.5-local',
messages
})
});
if (!response.ok) {
throw new Error('API 请求失败');
}
return await response.json();
} catch (error: any) {
console.error(error);
throw error;
}
}
function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[]; modifiedCode: 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);
}
});
// 返回差异结果,无论是否有差异
return {
added,
removed,
modifiedCode: normalizedModified
};
}
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>
@ -578,4 +508,4 @@ function getWebviewContent(styleUri: vscode.Uri, scriptUri: vscode.Uri,highlight
<script src="${markedScriptUri}"></script> <script src="${markedScriptUri}"></script>
</body> </body>
</html>`; </html>`;
} }

@ -0,0 +1,83 @@
import * as diff from "diff";
export function generateCodeDiff(originalCode: string, aiResponse: string): { added: string[]; removed: string[]; modifiedCode: string } | null {
// 提取代码块中的代码
const codeBlockRegex = /```(?:([a-zA-Z0-9]+))?\s*[\n\r]([\s\S]*?)\s*```/g;
const matches = [...aiResponse.matchAll(codeBlockRegex)];
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);
// 如果标准化后的内容相同,则没有差异
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);
}
});
// 返回差异结果,无论是否有差异
return {
added,
removed,
modifiedCode: normalizedModified
};
}
return null;
}
export function getFileExtension(language: string): string {
const languageMap: { [key: string]: string } = {
'javascript': '.js',
'typescript': '.ts',
'python': '.py',
'java': '.java',
'html': '.html',
'css': '.css',
'json': '.json',
'c': '.c',
'cpp': '.cpp',
'csharp': '.cs',
'php': '.php',
'ruby': '.rb',
'go': '.go',
'rust': '.rs',
'swift': '.swift',
'kotlin': '.kt',
'scala': '.scala',
'dart': '.dart',
'lua': '.lua',
'perl': '.pl',
'r': '.r',
'sql': '.sql',
'yaml': '.yaml',
'xml': '.xml'
};
return languageMap[language?.toLowerCase()] || '.txt';
}

@ -0,0 +1,52 @@
import vscode from "vscode";
export async function callQwenAPI(
question: string,
history: any[],
fileContent: string,
context: vscode.ExtensionContext
): Promise<any> {
const apiKey = 'dev-token';
const url = 'https://aicomp.ngsk.tech:7001/api/v1/chat/completions';
const messages = [
{
role: 'system',
content: `你是一个代码助手,请根据当前文件内容和历史对话回答问题。请使用中文回答。`
},
...history,
{
role: 'user',
content: `【当前文件内容】:
\`\`\`${fileContent}\`\`\`
:
${question}`
}
];
console.log("messages:",messages)
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'qwen2.5-local',
messages
})
});
if (!response.ok) {
throw new Error('API 请求失败');
}
return await response.json();
} catch (error: any) {
console.error(error);
throw error;
}
}
Loading…
Cancel
Save