diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index 4057237..0a29e24 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -1,6 +1,13 @@ import * as vscode from "vscode"; import { getWebviewContent } from "../views/webviewContent"; -import { handleUserMessage, insertCodeToEditor, handleReadFile } from "../utils/messageHandler"; +import { + handleUserMessage, + insertCodeToEditor, + handleReadFile, + handleUpdateFile, + handleRenameFile, + handleReplaceInFile +} from "../utils/messageHandler"; /** * 创建并显示 IC 助手面板 @@ -39,6 +46,15 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { case "readFile": handleReadFile(panel, message.filePath); break; + case "updateFile": + handleUpdateFile(panel, message.filePath, message.content); + break; + case "renameFile": + handleRenameFile(panel, message.oldPath, message.newPath); + break; + case "replaceInFile": + handleReplaceInFile(panel, message.filePath, message.searchText, message.replaceText); + break; case "insertCode": insertCodeToEditor(message.code); break; diff --git a/src/utils/createFiles.ts b/src/utils/createFiles.ts index d992738..18a1d2f 100644 --- a/src/utils/createFiles.ts +++ b/src/utils/createFiles.ts @@ -132,37 +132,6 @@ export async function createMultipleFiles( return results; } -/** - * 追加文件内容 - */ -export async function appendToFile( - filePath: string, - content: string -): Promise { - try { - // 如果是相对路径,转换为绝对路径 - let absolutePath = filePath; - if (!path.isAbsolute(filePath)) { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders && workspaceFolders.length > 0) { - absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); - } else { - throw new Error("没有打开的工作区,无法追加相对路径的文件"); - } - } - - // 检查文件是否存在 - if (!fs.existsSync(absolutePath)) { - throw new Error(`文件不存在: ${absolutePath}`); - } - - // 追加内容 - fs.appendFileSync(absolutePath, content, "utf-8"); - } catch (error) { - throw error; - } -} - // 删除文件 export async function deleteFile(filePath: string): Promise { try { @@ -194,3 +163,207 @@ export async function deleteFile(filePath: string): Promise { throw error; } } + +// 修改文件内容(完全替换) +export async function updateFile( + filePath: string, + content: string +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absolutePath = filePath; + if (!path.isAbsolute(filePath)) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); + } else { + throw new Error("没有打开的工作区,无法修改相对路径的文件"); + } + } + + // 检查文件是否存在 + if (!fs.existsSync(absolutePath)) { + throw new Error(`文件不存在: ${absolutePath}`); + } + + // 检查是否是文件内容 + const state = fs.statSync(absolutePath); + if (!state.isFile()) { + throw new Error(`路径不是文件: ${absolutePath}`); + } + + // 修改文件内容 + fs.writeFileSync(absolutePath, content, "utf-8"); + } catch (error) { + throw error; + } +} + +/** + * 追加文件内容 + */ +export async function appendToFile( + filePath: string, + content: string +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absolutePath = filePath; + if (!path.isAbsolute(filePath)) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); + } else { + throw new Error("没有打开的工作区,无法追加相对路径的文件"); + } + } + + // 检查文件是否存在 + if (!fs.existsSync(absolutePath)) { + throw new Error(`文件不存在: ${absolutePath}`); + } + + // 追加内容 + fs.appendFileSync(absolutePath, content, "utf-8"); + } catch (error) { + throw error; + } +} + +// 部分修改文件内容(查找替换) +export async function replaceFile( + filePath: string, + searchValue: string | RegExp, + replaceValue: string +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absolutePath = filePath; + if (!path.isAbsolute(filePath)) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); + } else { + throw new Error("没有打开的工作区,无法修改相对路径的文件"); + } + } + + // 检查文件是否存在 + if (!fs.existsSync(absolutePath)) { + throw new Error(`文件不存在: ${absolutePath}`); + } + + // 读取文件内容 + const fileContent = fs.readFileSync(absolutePath, "utf-8"); + + // 转义特殊字符,将字符串作为字面量处理 + const escapeRegExp = (str: string) => { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + }; + + // 替换内容 - 如果是字符串,先转义特殊字符 + let newContent: string; + if (typeof searchValue === 'string') { + const escapedSearch = escapeRegExp(searchValue); + newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue); + } else { + newContent = fileContent.replace(searchValue, replaceValue); + } + + // 检查是否有内容被替换 + if (fileContent === newContent) { + throw new Error(`未找到要替换的内容: ${searchValue}`); + } + + // 写回文件 + fs.writeFileSync(absolutePath, newContent, "utf-8"); + } catch (error) { + throw error; + } +} + +// 在指定位置插入内容 +export async function insertAtLine( + filePath: string, + lineNumber: number, + content: string +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absolutePath = filePath; + if (!path.isAbsolute(filePath)) { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); + } else { + throw new Error("没有打开的工作区,无法修改相对路径的文件"); + } + } + + // 检查文件是否存在 + if (!fs.existsSync(absolutePath)) { + throw new Error(`文件不存在: ${absolutePath}`); + } + + // 读取文件内容 + const fileContent = fs.readFileSync(absolutePath, "utf-8"); + const lines = fileContent.split("\n"); + + // 插入内容 + lines.splice(lineNumber, 0, content); + + // 写回文件 + fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8"); + } catch (error) { + throw error; + } +} + +/** + * 重命名文件 + */ +export async function renameFile( + oldPath: string, + newPath: string +): Promise { + try { + // 如果是相对路径,转换为绝对路径 + let absoluteOldPath = oldPath; + let absoluteNewPath = newPath; + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const workspaceRoot = workspaceFolders[0].uri.fsPath; + + if (!path.isAbsolute(oldPath)) { + absoluteOldPath = path.join(workspaceRoot, oldPath); + } + + if (!path.isAbsolute(newPath)) { + absoluteNewPath = path.join(workspaceRoot, newPath); + } + } else { + throw new Error("没有打开的工作区,无法重命名相对路径的文件"); + } + + // 检查原文件是否存在 + if (!fs.existsSync(absoluteOldPath)) { + throw new Error(`文件不存在: ${absoluteOldPath}`); + } + + // 检查新文件名是否已存在 + if (fs.existsSync(absoluteNewPath)) { + throw new Error(`目标文件已存在: ${absoluteNewPath}`); + } + + // 确保目标目录存在 + const newDir = path.dirname(absoluteNewPath); + if (!fs.existsSync(newDir)) { + fs.mkdirSync(newDir, { recursive: true }); + } + + // 重命名文件 + fs.renameSync(absoluteOldPath, absoluteNewPath); + } catch (error) { + throw error; + } +} diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 4390b90..187e6ea 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -1,20 +1,36 @@ import * as vscode from "vscode"; import { readFileContent } from "./readFiles"; -import { createFile, createOrOverwriteFile, deleteFile } from "./createFiles"; +import { + createFile, + createOrOverwriteFile, + deleteFile, + updateFile, + renameFile, + replaceFile, +} from "./createFiles"; /** * 处理用户消息 */ -export async function handleUserMessage(panel: vscode.WebviewPanel, text: string) { +export async function handleUserMessage( + panel: vscode.WebviewPanel, + text: string +) { + console.log("收到用户消息:", text); + // 检查是否是文件操作命令 const fileOperation = parseFileOperation(text); + console.log("解析结果:", fileOperation); + if (fileOperation) { + console.log("执行文件操作:", fileOperation.type, fileOperation.filePath); await handleFileOperation(panel, fileOperation); return; } // 普通消息处理 + console.log("作为普通消息处理"); const reply = getMockReply(text); setTimeout(() => { panel.webview.postMessage({ @@ -28,9 +44,12 @@ export async function handleUserMessage(panel: vscode.WebviewPanel, text: string * 解析文件操作命令 */ function parseFileOperation(text: string): { - type: 'create' | 'delete' | 'read'; + type: "create" | "delete" | "read" | "update" | "rename" | "replace"; filePath: string; content?: string; + newPath?: string; + searchText?: string; + replaceText?: string; } | null { const lowerText = text.toLowerCase().trim(); @@ -39,9 +58,9 @@ function parseFileOperation(text: string): { if (createMatch) { const filePath = createMatch[1].trim(); return { - type: 'create', + type: "create", filePath: filePath, - content: getDefaultContent(filePath) + content: getDefaultContent(filePath), }; } @@ -50,18 +69,71 @@ function parseFileOperation(text: string): { if (deleteMatch) { const filePath = deleteMatch[1].trim(); return { - type: 'delete', - filePath: filePath + type: "delete", + filePath: filePath, + }; + } + + // 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts(优先匹配,避免被修改匹配) + const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/); + if (renameMatch) { + const oldPath = renameMatch[1].trim(); + const newPath = renameMatch[2].trim(); + return { + type: "rename", + filePath: oldPath, + newPath: newPath, + }; + } + + // 匹配替换内容:支持多种格式 + // 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb" + // 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb" + // 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb' + const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/); + if (replaceMatch1) { + const filePath = replaceMatch1[1].trim(); + const searchText = replaceMatch1[2].trim(); + const replaceText = replaceMatch1[3].trim(); + return { + type: "replace", + filePath: filePath, + searchText: searchText, + replaceText: replaceText, + }; + } + + // 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb" + const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/); + if (replaceMatch2) { + const filePath = replaceMatch2[1].trim(); + const searchText = replaceMatch2[2].trim(); + const replaceText = replaceMatch2[3].trim(); + return { + type: "replace", + filePath: filePath, + searchText: searchText, + replaceText: replaceText, }; } // 匹配读取文件:读取 xxx.ts 文件 或 打开 xxx.ts - const readMatch = lowerText.match(/(?:读取|打开)(.+?\.\w+)(?:文件)?/); + const readMatch = lowerText.match(/(?:读取|打开)\s*(.+?\.\w+)\s*(?:文件)?/); if (readMatch) { const filePath = readMatch[1].trim(); return { - type: 'read', - filePath: filePath + type: "read", + filePath: filePath, + }; + } + + // 匹配修改文件:修改 xxx.ts 文件(放在最后,避免误匹配) + const updateMatch = lowerText.match(/修改\s*(.+?\.\w+)\s*(?:文件)?/); + if (updateMatch) { + const filePath = updateMatch[1].trim(); + return { + type: "update", + filePath: filePath, }; } @@ -73,29 +145,40 @@ function parseFileOperation(text: string): { */ async function handleFileOperation( panel: vscode.WebviewPanel, - operation: { type: 'create' | 'delete' | 'read'; filePath: string; content?: string } + operation: { + type: "create" | "delete" | "read" | "update" | "rename" | "replace"; + filePath: string; + content?: string; + newPath?: string; + searchText?: string; + replaceText?: string; + } ) { try { switch (operation.type) { - case 'create': - await createFile(operation.filePath, operation.content || ''); + case "create": + await createFile(operation.filePath, operation.content || ""); panel.webview.postMessage({ command: "receiveMessage", text: `✅ 文件创建成功: ${operation.filePath}`, }); - vscode.window.showInformationMessage(`文件创建成功: ${operation.filePath}`); + vscode.window.showInformationMessage( + `文件创建成功: ${operation.filePath}` + ); break; - case 'delete': + case "delete": await deleteFile(operation.filePath); panel.webview.postMessage({ command: "receiveMessage", text: `✅ 文件删除成功: ${operation.filePath}`, }); - vscode.window.showInformationMessage(`文件删除成功: ${operation.filePath}`); + vscode.window.showInformationMessage( + `文件删除成功: ${operation.filePath}` + ); break; - case 'read': + case "read": const content = await readFileContent(operation.filePath); panel.webview.postMessage({ command: "fileContent", @@ -103,6 +186,47 @@ async function handleFileOperation( filePath: operation.filePath, }); break; + + case "update": + const currentContent = await readFileContent(operation.filePath); + panel.webview.postMessage({ + command: "editFile", + content: currentContent, + filePath: operation.filePath, + }); + break; + + case "rename": + if (!operation.newPath) { + throw new Error("缺少新文件名"); + } + await renameFile(operation.filePath, operation.newPath); + panel.webview.postMessage({ + command: "receiveMessage", + text: `✅ 文件重命名成功: ${operation.filePath} → ${operation.newPath}`, + }); + vscode.window.showInformationMessage( + `文件重命名成功: ${operation.filePath} → ${operation.newPath}` + ); + break; + + case "replace": + if (!operation.searchText || !operation.replaceText) { + throw new Error("缺少替换内容"); + } + await replaceFile( + operation.filePath, + operation.searchText, + operation.replaceText + ); + panel.webview.postMessage({ + command: "receiveMessage", + text: `✅ 文件内容替换成功: ${operation.filePath}`, + }); + vscode.window.showInformationMessage( + `文件内容替换成功: ${operation.filePath}` + ); + break; } } catch (error) { const errorMsg = error instanceof Error ? error.message : "操作失败"; @@ -118,18 +242,18 @@ async function handleFileOperation( * 根据文件扩展名生成默认内容 */ function getDefaultContent(filePath: string): string { - const ext = filePath.split('.').pop()?.toLowerCase(); + const ext = filePath.split(".").pop()?.toLowerCase(); switch (ext) { - case 'ts': + case "ts": return `// ${filePath}\n\nexport {};\n`; - case 'js': + case "js": return `// ${filePath}\n\n`; - case 'json': - return '{\n \n}\n'; - case 'md': + case "json": + return "{\n \n}\n"; + case "md": return `# ${filePath}\n\n`; - case 'html': + case "html": return ` @@ -141,13 +265,15 @@ function getDefaultContent(filePath: string): string { `; - case 'css': + case "css": return `/* ${filePath} */\n\n`; - case 'v': - case 'sv': - return `// ${filePath}\n\nmodule ${filePath.split('.')[0]} (\n \n);\n\nendmodule\n`; + case "v": + case "sv": + return `// ${filePath}\n\nmodule ${ + filePath.split(".")[0] + } (\n \n);\n\nendmodule\n`; default: - return ''; + return ""; } } @@ -206,6 +332,91 @@ export async function handleCreateFile( } } +/** + * 处理文件更新请求 + */ +export async function handleUpdateFile( + panel: vscode.WebviewPanel, + filePath: string, + content: string +) { + try { + await updateFile(filePath, content); + panel.webview.postMessage({ + command: "fileUpdated", + filePath: filePath, + message: " 文件更新成功", + }); + vscode.window.showInformationMessage(`文件更新成功: ${filePath}`); + } catch (error) { + panel.webview.postMessage({ + command: "fileUpdateError", + error: error instanceof Error ? error.message : "更新文件失败", + }); + vscode.window.showErrorMessage( + `更新文件失败: ${error instanceof Error ? error.message : "未知错误"}` + ); + } +} + +/** + * 处理文件重命名请求 + */ +export async function handleRenameFile( + panel: vscode.WebviewPanel, + oldPath: string, + newPath: string +) { + try { + await renameFile(oldPath, newPath); + panel.webview.postMessage({ + command: "fileRenamed", + oldPath: oldPath, + newPath: newPath, + message: "文件重命名成功", + }); + vscode.window.showInformationMessage( + `文件重命名成功: ${oldPath} → ${newPath}` + ); + } catch (error) { + panel.webview.postMessage({ + command: "fileRenameError", + error: error instanceof Error ? error.message : "重命名文件失败", + }); + vscode.window.showErrorMessage( + `重命名文件失败: ${error instanceof Error ? error.message : "未知错误"}` + ); + } +} + +/** + * 处理文件内容替换请求 + */ +export async function handleReplaceInFile( + panel: vscode.WebviewPanel, + filePath: string, + searchText: string, + replaceText: string +) { + try { + await replaceFile(filePath, searchText, replaceText); + panel.webview.postMessage({ + command: "fileReplaced", + filePath: filePath, + message: "文件内容替换成功", + }); + vscode.window.showInformationMessage(`文件内容替换成功: ${filePath}`); + } catch (error) { + panel.webview.postMessage({ + command: "fileReplaceError", + error: error instanceof Error ? error.message : "替换文件内容失败", + }); + vscode.window.showErrorMessage( + `替换文件内容失败: ${error instanceof Error ? error.message : "未知错误"}` + ); + } +} + /** * 获取模拟回复 */ diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index a1281c3..5bead57 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -145,6 +145,38 @@ export function getWebviewContent(iconUri?: string): string { border-radius: 4px; margin-top: 10px; } + .file-editor-section { + margin-bottom: 20px; + padding: 15px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + border-radius: 8px; + display: none; + } + .file-editor-section.active { + display: block; + } + .file-editor-section h3 { + margin: 0 0 10px 0; + color: var(--vscode-button-background); + } + .file-editor-textarea { + width: 100%; + min-height: 300px; + padding: 10px; + background: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 12px; + resize: vertical; + } + .editor-actions { + display: flex; + gap: 10px; + margin-top: 10px; + } @@ -172,6 +204,15 @@ export function getWebviewContent(iconUri?: string): string { +
+

✏️ 编辑文件:

+ +
+ + +
+
+
@@ -204,6 +245,11 @@ export function getWebviewContent(iconUri?: string): string { const filePathInput = document.getElementById('filePathInput'); const fileContentEl = document.getElementById('fileContent'); const errorMessageEl = document.getElementById('errorMessage'); + const fileEditorSection = document.getElementById('fileEditorSection'); + const fileEditorTextarea = document.getElementById('fileEditorTextarea'); + const editingFileName = document.getElementById('editingFileName'); + + let currentEditingFile = null; function sendMessage() { const text = messageInput.value.trim(); @@ -268,6 +314,42 @@ export function getWebviewContent(iconUri?: string): string { messagesEl.scrollTop = messagesEl.scrollHeight; } + function openFileEditor(filePath, content) { + currentEditingFile = filePath; + editingFileName.textContent = filePath; + fileEditorTextarea.value = content; + fileEditorSection.classList.add('active'); + + // 滚动到编辑器位置 + fileEditorSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // 在消息区域显示提示 + addMessage(\`正在编辑文件: \${filePath}\`, 'bot'); + } + + function saveFile() { + if (!currentEditingFile) { + return; + } + + const content = fileEditorTextarea.value; + vscode.postMessage({ + command: 'updateFile', + filePath: currentEditingFile, + content: content + }); + + addMessage(\`正在保存文件: \${currentEditingFile}\`, 'user'); + cancelEdit(); + } + + function cancelEdit() { + currentEditingFile = null; + fileEditorSection.classList.remove('active'); + fileEditorTextarea.value = ''; + editingFileName.textContent = ''; + } + window.addEventListener('message', event => { const message = event.data; @@ -281,6 +363,15 @@ export function getWebviewContent(iconUri?: string): string { case 'fileError': showError(message.error); break; + case 'editFile': + openFileEditor(message.filePath, message.content); + break; + case 'fileUpdated': + addMessage(\`✅ \${message.message}: \${message.filePath}\`, 'bot'); + break; + case 'fileUpdateError': + addMessage(\`❌ \${message.error}\`, 'bot'); + break; } });