JetBrains IDEA插件开发:核心API与工具详解

JetBrains IDEA插件开发:核心API与工具详解

  1. 我的工具箱 🪜
  2. 2019.03.19
  3. 8 min read

IDEA Plugin API

文件操作

Virtual File System

Virtual File System 是处理文件的一套机制, 用于处理如何加载文件, 如果保存文件, 当文件变化时如何更新缓存等.

IntelliJ Platform 将操作文件封装成了 Virtual File System, 提供了以下几点主要功能:

  1. 封装处理文件的通用 API, 不论文件在磁盘, 存档, HTTP 服务器或者其他地方, 都使用同一套 API;
  2. 提供快照功能, 能跟踪文件的修改;
  3. 提供将附加持久数据与 VFS 中的文件相关联;

为了提供最后两个功能,VFS 管理用户硬盘的某些内容的持久快照。快照仅存储通过 VFS API 至少请求过一次的文件,并且异步更新以匹配磁盘上发生的更改。

快照是应用程序级别,而不是项目级别 - 因此,如果某个文件(例如,JDK 中的某个类)被多个项目引用,则其内容的一个副本将存储在 VFS 中。

所有 VFS 访问操作都通过快照。

如果通过 VFS API 请求某些信息但快照中没有这些信息,则会从磁盘加载并存储到快照中。如果快照中有可用信息,则返回快照数据。仅当访问了特定信息时,文件的内容和目录中的文件列表才存储在快照中 - 否则,仅存储名称,长度,时间戳,属性等文件元数据。

这意味着 IntelliJ Platform UI 中显示的文件系统状态和文件内容来自快照,快照可能并不总是与磁盘的实际内容相匹配。 例如, 在某些情况下,在 IntelliJ 平台选择删除之前,已删除的文件仍可在 UI 中显示一段时间。

在刷新操作期间从磁盘更新快照,这通常是异步发生的。通过 VFS 进行的所有写操作都是同步的 - 即内容立即保存到磁盘。

刷新操作将 VFS 的一部分状态与实际磁盘内容同步。IntelliJ 平台或插件代码显式调用刷新操作 - 即在 IDE 运行时在磁盘上更改文件时,VFS 不会立即获取更改。VFS 将在下一次刷新操作期间更新,其中包括其范围内的文件。

Virtual File

用于表示 Virtual File System 中的一个具体文件, 相当于本地系统文件, 也用于表示 jar 包中的文件中的类, 还可以表示版本管理中的旧文件.

VFS 仅处理二进制内容

获取 VirtualFile

private void getVirtualFile(AnActionEvent e) {
    // 获取 VirtualFile 方式一:
    VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
    // 获取多个 VirtualFile
    VirtualFile[] virtualFiles = e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY);
    // 方式二: 从本地文件系统路径获取
    VirtualFile virtualFileFromLocalFileSystem = LocalFileSystem.getInstance().findFileByIoFile(new File("path"));
    // 方式三: 从 PSI 文件 (如果 PSI 文件仅存在内存中, 则可能返回 null)
    PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
    if (psiFile != null) {
        psiFile.getVirtualFile();
    }
    // 方式四: 从 document 中获取
    Document document = Objects.requireNonNull(e.getData(PlatformDataKeys.EDITOR)).getDocument();
    VirtualFile virtualFileFromDocument = FileDocumentManager.getInstance().getFile(document);
}

20241229154732_TaWCxR2c.webp

遍历文件系统

遍历文件可以使用 File 的 API, 我们也可以通过 VFS 提供的 API 来实现

private void iterateChildrenRecursively(VirtualFile virtualFile) {
    /**
     * 递归遍历子文件
     *
     * @param root     the root         父文件
     * @param filter   the filter       过滤器
     * @param iterator the iterator     处理方式
     * @return the boolean
     */
    VfsUtilCore.iterateChildrenRecursively(virtualFile,
                                           new VirtualFileFilter() {
                                               @Override
                                               public boolean accept(VirtualFile file) {
                                                   // todo-dong4j : (2019 年 03 月 15 日 13:02) [从 .gitignore 中获取忽略的文件]
                                                   boolean allowAccept = file.isDirectory()&& !file.getName().equals(NODE_MODULES_FILE);
                                                   if(allowAccept || file.getName().endsWith(MARKDOWN_FILE_TYPE)){
                                                       log.trace("accept = {}", file.getPath());
                                                       return true;
                                                   }
                                                   return false;
                                               }
                                           },
                                           new ContentIterator() {
                                               @Override
                                               public boolean processFile(@NotNull VirtualFile fileOrDir) {
                                                   // todo-dong4j : (2019 年 03 月 15 日 13:04) [处理 markdown 逻辑实现]
                                                   if(!fileOrDir.isDirectory()){
                                                       log.trace("processFile = {}", fileOrDir.getName());
                                                   }
                                                   return true;
                                               }
                                           });
}

ContentIterator 接口表示处理文件的具体方式, 需要自己实现.

由于在过滤器中已经将非 markdown 文件过滤掉, 因此此处只需要实现处理 markdown 文件的逻辑即可.

20241229154732_qtx1O6IU.webp

Document

Document 是可编辑的 Unicode 字符序列, 对应的是 VirtualFile 中的文本内容.

文档中的换行符 始终\n.

可以通过 Document 对文件做任何操作.

获取 Document

private void getDocument(AnActionEvent e){
    // 从当前编辑器中获取
    Document documentFromEditor = Objects.requireNonNull(e.getData(PlatformDataKeys.EDITOR)).getDocument();
    // 从 VirtualFile 获取 (如果之前未加载文档内容,则此调用会强制从磁盘加载文档内容)
    VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
    if (virtualFile != null) {
        Document documentFromVirtualFile = FileDocumentManager.getInstance().getDocument(virtualFile);
        // 从缓存中获取
        Document documentFromVirtualFileCache = FileDocumentManager.getInstance().getCachedDocument(virtualFile);

        // 从 PSI 中获取
        Project project = e.getProject();
        if (project != null) {
            // 获取 PSI (一)
            PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
            // 获取 PSI (二)
            psiFile = e.getData(CommonDataKeys.PSI_FILE);
            if (psiFile != null) {
                Document documentFromPsi = PsiDocumentManager.getInstance(project).getDocument(psiFile);
                // 从缓存中获取
                Document documentFromPsiCache = PsiDocumentManager.getInstance(project).getCachedDocument(psiFile);
            }
        }
    }
}

20241229154732_QSKPhUZG.webp

对 Document 操作时一定要注意内存泄漏的问题, 因为每次访问文件的 Document 对象是, 都是一个新的实例, 使用完成后一定要记得释放引用.

创建 Document

如果需要在磁盘上创建新文件, 不要直接创建 Document, 而是先创建 PSI 文件, 然后获取它的 Document.

如果需要创建一个没有绑定的 Document 的实例, 可以使用 EditorFactory.createDocument.

Document Listener

  • 接收有关特定 Document 实例中的更改的通知
Document.addDocumentListener
  • 接收有关所有打开文档中的更改的通知
EditorFactory.getEventMulticaster().addDocumentListener

write Document

SDK 规定所有写操作必须通过异步执行, 因此需要将写操作包装到 command 中

CommandProcessor.getInstance().executeCommand()

比如:

WriteCommandAction.runWriteCommandAction(project, () -> {
    document.setText(string);
    psiDocumentManager.doPostponedOperationsAndUnblockDocument(document);
    psiDocumentManager.commitDocument(document);
    FileDocumentManager.getInstance().saveDocument(document);
});

Editor

Editor 相关 API

editor-ui-api package,Editor.java,EditorImpl.java.CommonDataKeys.java,DataKey.java,AnActionEvent,DataContext

PSI

获取 PSI

private void getPsiFile(AnActionEvent e){
    // 从 action 中获取
    PsiFile psiFileFromAction = e.getData(LangDataKeys.PSI_FILE);
    Project project = e.getProject();
    if (project != null) {
        VirtualFile virtualFile = e.getData(PlatformDataKeys.VIRTUAL_FILE);
        if (virtualFile != null) {
            // 从 VirtualFile 获取
            PsiFile psiFileFromVirtualFile = PsiManager.getInstance(project).findFile(virtualFile);

            // 从 document
            Document documentFromEditor = Objects.requireNonNull(e.getData(PlatformDataKeys.EDITOR)).getDocument();
            PsiFile psiFileFromDocument = PsiDocumentManager.getInstance(project).getPsiFile(documentFromEditor);

            // 在 project 范围内查找特定 PsiFile
            FilenameIndex.getFilesByName(project, "fileName", GlobalSearchScope.projectScope(project));
        }
    }
}

如果我知道它的名字但不知道路径,我如何找到文件?

FilenameIndex.getFilesByName()

如何找到特定 PSI 元素的使用位置?

ReferencesSearch.search()

如何重命名 PSI 元素?

RefactoringFactory.createRename()

如何重建虚拟文件的 PSI?

FileContentUtil.reparseFiles()

Java 特定

如何找到类的所有继承者?

ClassInheritorsSearch.search()

如何通过限定名称查找课程?

JavaPsiFacade.findClass()

如何通过短名称找到一个班级?

PsiShortNamesCache.getInstance().getClassesByName()

如何找到 Java 类的超类?

PsiClass.getSuperClass()

如何获取对 Java 类的包含的引用?

PsiJavaFile javaFile = (PsiJavaFile) psiClass.getContaningFile();
PsiPackage pkg = JavaPsiFacade.getInstance(project).findPackage(javaFile.getPackageName());

如何找到覆盖特定方法的方法

OverridingMethodsSearch.search()

IntelliJ IDEA 学习笔记