IDEA插件开发:符号链接文件夹创建文件后视图异常解决问询
IDEA插件符号链接文件夹创建文件后显示异常的解决方案
问题说明
开发的IDEA插件支持右键点击文件夹创建代码文件,但当目标文件夹是符号链接时,新文件会错误显示在父文件夹下(实际磁盘位置正确),必须手动执行“Reload Project”才能恢复正常显示。
问题根源
IDEA的虚拟文件系统(VFS)对符号链接有特殊映射逻辑,直接通过真实磁盘路径创建文件会绕过VFS的符号链接处理,导致VFS无法将新文件关联到符号链接节点,最终出现视图显示错误。
解决方案
核心修改思路
- 避免直接操作真实磁盘路径,改用IDEA VFS API处理文件创建,让VFS自动管理符号链接映射
- 针对符号链接对应的VirtualFile进行精准刷新,确保VFS更新该节点的子文件列表
- 调整刷新逻辑顺序,优先触发VFS同步,再更新项目视图
修改后的代码
package coocoogame.com.et_tool; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.externalSystem.model.ExternalSystemDataKeys; import com.intellij.openapi.externalSystem.model.ProjectSystemId; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowId; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.projectView.ProjectView; import java.io.IOException; public class CreateCsFileAction extends AnAction { // 定义字符串模板(保持原模板不变) private static final String TEMPLATESystem = "namespace ET${locationType} \n { \n [FriendOf(typeof(${fileName}))] \n" + " [EntitySystemOf(typeof(${fileName}))] \n" + "public static partial class ${fileName}System { \n" + "[EntitySystem] \n private static void Awake(this ${fileName} self) \n { \n } \n " + " [EntitySystem] \n private static void Destroy(this ${fileName} self) \n { \n } \n " + "} \n }"; private static final String TEMPLATEComponent = "namespace ET${locationType} \n { " + "\n //[ChildOf(typeof(${fileName}))] \n" + "\n [ComponentOf(typeof(${fileName}))] \n" + "public class ${fileName} : Entity, IAwake, IDestroy { \n" + "\n \n" + "} \n }"; private static final String TEMPLATEMessageHandler = "namespace ET${locationType} \n { " + "\n [MessageHandler(SceneType.Current)] \n" + "public class ${fileName} : MessageHandler<Scene, ${fileName}> { \n" + " protected override async ETTask Run(Scene root, ${fileName} message) \n {"+ "\n \n" + " await ETTask.CompletedTask; \n }"+ "} \n }"; @Override public void actionPerformed(AnActionEvent e) { Project project = e.getData(CommonDataKeys.PROJECT); ProjectSystemId projectSystemId = e.getData(ExternalSystemDataKeys.EXTERNAL_SYSTEM_ID); if (project == null) return; VirtualFile[] selectedFiles = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY); if (selectedFiles == null || selectedFiles.length == 0) return; VirtualFile targetDir = selectedFiles[0]; if (!targetDir.isDirectory()) { targetDir = targetDir.getParent(); } if (targetDir == null || !targetDir.isDirectory()) return; // 显示对话框获取文件名 FileNameInputDialog dialog = new FileNameInputDialog(project); dialog.show(); if (dialog.isOK()) { String fileName = dialog.getFileName(); LocationType selectedLocationType = dialog.getSelectedLocationType(); CSType selectedCSType = dialog.getSelectedCSType(); if (fileName != null && !fileName.isEmpty()) { createFile(project, projectSystemId, targetDir, fileName, selectedLocationType, selectedCSType); } } } private void createFile(Project project, ProjectSystemId projectSystemId, VirtualFile targetDir, String fileName, LocationType locationType, CSType csType) { String fullFileName = fileName + ".cs"; String fileContent = ""; // 生成模板内容逻辑保持不变 if (csType == CSType.System) { fileContent = TEMPLATESystem.replace("${fileName}", fileName) .replace("${locationType}", locationType == LocationType.Client || locationType == LocationType.Server ? "." + locationType.name() : ""); } else if (csType == CSType.Component) { fileContent = TEMPLATEComponent.replace("${fileName}", fileName) .replace("${locationType}", locationType == LocationType.Client || locationType == LocationType.Server ? "." + locationType.name() : ""); } else if (csType == CSType.MessageHandler) { fileContent = TEMPLATEMessageHandler.replace("${fileName}", fileName) .replace("${locationType}", locationType == LocationType.Client || locationType == LocationType.Server ? "." + locationType.name() : ""); } // 使用VFS API在写操作线程中创建文件 ApplicationManager.getApplication().runWriteAction(() -> { try { VirtualFile newFile = targetDir.createChildData(this, fullFileName); VfsUtil.saveText(newFile, fileContent); // 精准刷新符号链接文件夹 VfsUtil.markDirtyAndRefresh(false, true, false, targetDir); // 异步刷新VFS并激活项目视图 ApplicationManager.getApplication().invokeLater(() -> { VirtualFileManager.getInstance().asyncRefresh(() -> { ToolWindow projectView = ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.PROJECT_VIEW); if (projectView != null) { projectView.activate(null); // 可选:自动展开目标文件夹并选中新文件 ProjectView.getInstance(project).select(newFile, newFile, true); } }); }); Messages.showMessageDialog(project, "文件创建成功:" + newFile.getPath(), "成功", Messages.getInformationIcon()); } catch (IOException ex) { Messages.showMessageDialog(project, "创建文件失败:" + ex.getMessage(), "错误", Messages.getErrorIcon()); } }); } }
关键修改点说明
- 移除符号链接拦截逻辑:允许直接在符号链接文件夹操作,由VFS自动处理真实路径映射
- 改用VFS API创建文件:使用
VirtualFile.createChildData和VfsUtil.saveText,确保VFS跟踪符号链接下的文件变化 - 精准刷新目标文件夹:直接刷新符号链接对应的
VirtualFile,而非真实路径的父文件夹,让VFS更新该节点的子文件列表 - 优化刷新顺序:在写操作完成后立即标记文件夹为脏并刷新,再异步触发VFS全局刷新,确保视图及时同步
内容的提问来源于stack exchange,提问作者melontaro
相关产品推荐
相关产品推荐

