feat:对本地文件进行修改

- 对某一行进行修改
- 将文件中的某些词进行替换
- 将文件重命名
This commit is contained in:
Roe-xin
2025-12-12 09:57:33 +08:00
parent 8af5976501
commit 94225a3525
4 changed files with 552 additions and 61 deletions

View File

@ -1,6 +1,13 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { getWebviewContent } from "../views/webviewContent"; import { getWebviewContent } from "../views/webviewContent";
import { handleUserMessage, insertCodeToEditor, handleReadFile } from "../utils/messageHandler"; import {
handleUserMessage,
insertCodeToEditor,
handleReadFile,
handleUpdateFile,
handleRenameFile,
handleReplaceInFile
} from "../utils/messageHandler";
/** /**
* 创建并显示 IC 助手面板 * 创建并显示 IC 助手面板
@ -39,6 +46,15 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "readFile": case "readFile":
handleReadFile(panel, message.filePath); handleReadFile(panel, message.filePath);
break; 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": case "insertCode":
insertCodeToEditor(message.code); insertCodeToEditor(message.code);
break; break;

View File

@ -132,37 +132,6 @@ export async function createMultipleFiles(
return results; return results;
} }
/**
* 追加文件内容
*/
export async function appendToFile(
filePath: string,
content: string
): Promise<void> {
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<void> { export async function deleteFile(filePath: string): Promise<void> {
try { try {
@ -194,3 +163,207 @@ export async function deleteFile(filePath: string): Promise<void> {
throw error; throw error;
} }
} }
// 修改文件内容(完全替换)
export async function updateFile(
filePath: string,
content: string
): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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;
}
}

View File

@ -1,20 +1,36 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { readFileContent } from "./readFiles"; 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); const fileOperation = parseFileOperation(text);
console.log("解析结果:", fileOperation);
if (fileOperation) { if (fileOperation) {
console.log("执行文件操作:", fileOperation.type, fileOperation.filePath);
await handleFileOperation(panel, fileOperation); await handleFileOperation(panel, fileOperation);
return; return;
} }
// 普通消息处理 // 普通消息处理
console.log("作为普通消息处理");
const reply = getMockReply(text); const reply = getMockReply(text);
setTimeout(() => { setTimeout(() => {
panel.webview.postMessage({ panel.webview.postMessage({
@ -28,9 +44,12 @@ export async function handleUserMessage(panel: vscode.WebviewPanel, text: string
* 解析文件操作命令 * 解析文件操作命令
*/ */
function parseFileOperation(text: string): { function parseFileOperation(text: string): {
type: 'create' | 'delete' | 'read'; type: "create" | "delete" | "read" | "update" | "rename" | "replace";
filePath: string; filePath: string;
content?: string; content?: string;
newPath?: string;
searchText?: string;
replaceText?: string;
} | null { } | null {
const lowerText = text.toLowerCase().trim(); const lowerText = text.toLowerCase().trim();
@ -39,9 +58,9 @@ function parseFileOperation(text: string): {
if (createMatch) { if (createMatch) {
const filePath = createMatch[1].trim(); const filePath = createMatch[1].trim();
return { return {
type: 'create', type: "create",
filePath: filePath, filePath: filePath,
content: getDefaultContent(filePath) content: getDefaultContent(filePath),
}; };
} }
@ -50,18 +69,71 @@ function parseFileOperation(text: string): {
if (deleteMatch) { if (deleteMatch) {
const filePath = deleteMatch[1].trim(); const filePath = deleteMatch[1].trim();
return { return {
type: 'delete', type: "delete",
filePath: filePath 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 // 匹配读取文件:读取 xxx.ts 文件 或 打开 xxx.ts
const readMatch = lowerText.match(/(?:读取|打开)(.+?\.\w+)(?:文件)?/); const readMatch = lowerText.match(/(?:读取|打开)\s*(.+?\.\w+)\s*(?:文件)?/);
if (readMatch) { if (readMatch) {
const filePath = readMatch[1].trim(); const filePath = readMatch[1].trim();
return { return {
type: 'read', type: "read",
filePath: filePath 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( async function handleFileOperation(
panel: vscode.WebviewPanel, 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 { try {
switch (operation.type) { switch (operation.type) {
case 'create': case "create":
await createFile(operation.filePath, operation.content || ''); await createFile(operation.filePath, operation.content || "");
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件创建成功: ${operation.filePath}`, text: `✅ 文件创建成功: ${operation.filePath}`,
}); });
vscode.window.showInformationMessage(`文件创建成功: ${operation.filePath}`); vscode.window.showInformationMessage(
`文件创建成功: ${operation.filePath}`
);
break; break;
case 'delete': case "delete":
await deleteFile(operation.filePath); await deleteFile(operation.filePath);
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件删除成功: ${operation.filePath}`, text: `✅ 文件删除成功: ${operation.filePath}`,
}); });
vscode.window.showInformationMessage(`文件删除成功: ${operation.filePath}`); vscode.window.showInformationMessage(
`文件删除成功: ${operation.filePath}`
);
break; break;
case 'read': case "read":
const content = await readFileContent(operation.filePath); const content = await readFileContent(operation.filePath);
panel.webview.postMessage({ panel.webview.postMessage({
command: "fileContent", command: "fileContent",
@ -103,6 +186,47 @@ async function handleFileOperation(
filePath: operation.filePath, filePath: operation.filePath,
}); });
break; 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) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : "操作失败"; const errorMsg = error instanceof Error ? error.message : "操作失败";
@ -118,18 +242,18 @@ async function handleFileOperation(
* 根据文件扩展名生成默认内容 * 根据文件扩展名生成默认内容
*/ */
function getDefaultContent(filePath: string): string { function getDefaultContent(filePath: string): string {
const ext = filePath.split('.').pop()?.toLowerCase(); const ext = filePath.split(".").pop()?.toLowerCase();
switch (ext) { switch (ext) {
case 'ts': case "ts":
return `// ${filePath}\n\nexport {};\n`; return `// ${filePath}\n\nexport {};\n`;
case 'js': case "js":
return `// ${filePath}\n\n`; return `// ${filePath}\n\n`;
case 'json': case "json":
return '{\n \n}\n'; return "{\n \n}\n";
case 'md': case "md":
return `# ${filePath}\n\n`; return `# ${filePath}\n\n`;
case 'html': case "html":
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@ -141,13 +265,15 @@ function getDefaultContent(filePath: string): string {
</body> </body>
</html>`; </html>`;
case 'css': case "css":
return `/* ${filePath} */\n\n`; return `/* ${filePath} */\n\n`;
case 'v': case "v":
case 'sv': case "sv":
return `// ${filePath}\n\nmodule ${filePath.split('.')[0]} (\n \n);\n\nendmodule\n`; return `// ${filePath}\n\nmodule ${
filePath.split(".")[0]
} (\n \n);\n\nendmodule\n`;
default: 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 : "未知错误"}`
);
}
}
/** /**
* 获取模拟回复 * 获取模拟回复
*/ */

View File

@ -145,6 +145,38 @@ export function getWebviewContent(iconUri?: string): string {
border-radius: 4px; border-radius: 4px;
margin-top: 10px; 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;
}
</style> </style>
</head> </head>
<body> <body>
@ -172,6 +204,15 @@ export function getWebviewContent(iconUri?: string): string {
<div id="errorMessage" style="display: none;"></div> <div id="errorMessage" style="display: none;"></div>
</div> </div>
<div id="fileEditorSection" class="file-editor-section">
<h3>✏️ 编辑文件: <span id="editingFileName"></span></h3>
<textarea id="fileEditorTextarea" class="file-editor-textarea"></textarea>
<div class="editor-actions">
<button onclick="saveFile()">保存修改</button>
<button onclick="cancelEdit()" style="background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground);">取消</button>
</div>
</div>
<div class="chat-container"> <div class="chat-container">
<div id="messages" class="messages"> <div id="messages" class="messages">
<div class="message bot-message"> <div class="message bot-message">
@ -204,6 +245,11 @@ export function getWebviewContent(iconUri?: string): string {
const filePathInput = document.getElementById('filePathInput'); const filePathInput = document.getElementById('filePathInput');
const fileContentEl = document.getElementById('fileContent'); const fileContentEl = document.getElementById('fileContent');
const errorMessageEl = document.getElementById('errorMessage'); 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() { function sendMessage() {
const text = messageInput.value.trim(); const text = messageInput.value.trim();
@ -268,6 +314,42 @@ export function getWebviewContent(iconUri?: string): string {
messagesEl.scrollTop = messagesEl.scrollHeight; 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 => { window.addEventListener('message', event => {
const message = event.data; const message = event.data;
@ -281,6 +363,15 @@ export function getWebviewContent(iconUri?: string): string {
case 'fileError': case 'fileError':
showError(message.error); showError(message.error);
break; 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;
} }
}); });