IntelliJ IDEA 架构概述(面向插件开发者)

原文未定稿(2014/11)


这篇文章的目的是从插件开发者的视角描述IntelliJ IDEA的结构。文章将以一种任务驱动的方式组织:相比列出所有你可以对各组件对象进行的操作并描述这些操作它们的实现方式,这篇文章将尽可能回答“我能用这个对象做些什么”、“我如何能得到这个对象”这样的问题。

这篇文章假定读者已经熟悉IntelliJ IDEA插件开发的基本观念。如果你对插件开发还一无所知,你应该先从live demo和入门教程开始着手,之后再回到这里阅读这篇文章。

这篇文章包含以下主题:

  • 通用线程规则
  • 虚拟文件
  • 文档
  • PSI文件
  • 文件视图Providers
  • PSI元素

通用线程规则

通常,IntelliJ IDEA中的数据结构会被加上“可多读/单写入”的锁。来自任何线程的数据读取都是允许的。从UI线程中读取数据并不要求任何特殊请求,不过,从其他线程中进行的读取操作必需被封装在一个read action(ApplicationManager.getApplication().runReadAction())中。只允许UI线程进行写入数据操作,并且写操作始终需要封装在一个write action(ApplicationManager.getApplication().runWriteAction())中。

为了将控制权从后台线程移交到事件处理线程,插件应该调用ApplicationManager.getApplication().invokeLater()方法,而不是(Swing GUI编程中)标准的SwingUtilities.invokeLater()方法。前者的API允许为调用指定模式状态——调用被允许在一组模式对话框下执行。传递值ModalityState.NON_MODAL表示操作将在所有模式对话框关闭后被执行,而值ModalityState.stateForComponent() 表示操作可以在指定的组件(某个对话框的部分)仍可访问时就被执行。


虚拟文件

一个虚拟文件(com.intellij.openapi.vfs.VirtualFile)是一个文件系统(VFS)中一个文件的IntelliJ IDEA的(对应)代理对象。最常见的虚拟文件实例是你本地文件系统中的一个文件。然而,IDEA支持多种可插入的文件系统实现,所以虚拟文件也可以代理一个JAR包文件中的class、来自CVS仓库中加载的旧版本文件等等。

VFS水平只由二进制内容对应。你可以将虚拟文件内容当作字节流进行获取和设置,但像编码和换行符这类概念是由系统高层处理的。

How do I get one?

  • 从action中:e.getData(PlatformDataKeys.VIRTUAL_FILE)。如果你关心多选,你也可以用e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY)。
  • 从本地文件系统路径中:LocalFileSystem.getInstance().findFileByIoFile()
  • 从一个PSI文件:psiFile.getVirtualFile()(如果PSI文件只存在于内存中,可能返回 null)
  • 从一个文档中:FileDocumentManager.getInstance().getFile()

What can I do with one?

在文件系统中移动、获取文件内容、重命名、移动、删除等典型的文件操作

递归遍历操作应使用VfsUtilCore.iterateChildrenRecursively进行,防止因递归符号链接产生的无限循环。

Where does it come from?

从项目目录根部开始通过来回扫描(各层)文件系统目录,VFS会被增量构建。文件系统中新出现的文件会通过VFS的refreshes被探测到。一次重新刷新操作可以使用VirtualFileManager.getInstance().refresh() 或VirtualFile.refresh()程序性的被引发。VFS的重新刷新也可以由文件系统监视器接收到的文件系统变更通知来引发(Windows和Mac操作系统可用)。

作为一个插件开发者,如果你需要访问一个刚刚被外部工具创建的文件,你可能需要通过IDEA的API手动地触发一次VFS重新刷新操作。

How long does it live?

要提供一个定制的文件系统实现(例如FTP文件系统),实现VirtualFileSystem类(你很可能也需要实现VirtualFile类)并将你的实现注册为一个application组件。要在本地文件系统中注入操作钩子(例如,如果你要开发一个需要自定义重命名/移动操作的版本控制系统整合插件),实现LocalFileOperationsHandler接口并通过LocalFileSystem.registerAuxiliaryFileOperationsHandler方法注册。

What are the rules for working with it?

查看IntelliJ IDEA Virtual File System以获得VFS架构与使用规则的详细描述。

Samples

一个说明如何使用虚拟文件的示例插件项目可以在<%IDEA project directory%>/community/samples/vfs和<%IDEA project directory%>/community/samples/textEditor文件夹下得到。

文档

一篇文档是一串可编辑的Unicode字符序列,典型的对应虚拟文件的字符内容。文档中的断行总是被标准化为\n。IntelliJ IDEA会在加载和保存文档时透明化地处理编码和换行符转换。

How do I get one?

  • 从action中:e.getData(PlatformDataKeys.EDITOR).getDocument()
  • 从一个虚拟文件中:FileDocumentManager.getDocument()。如果文档内容之前未加载,此调用会强制文档从磁盘加载内容;如果你只关心已经打开的文档或者文档可能已经被修改了,使用FileDocumentManager.getCachedDocument()替代前面的调用。
  • 从一个PSI文件中:PsiDocumentManager.getInstance().getDocument()或PsiDocumentManager.getInstance().getCachedDocument()

What can I do with one?

用来访问和修改“plain text”水平的文件内容(作为字符串而不是Java元素树)的任何操作。

Where does it come from?

文档实例在一些需要访问文件的文本内容的时候被创建(特别地,为文件构建PSI时需要创建文档实例)。另外,文档实例不会和任何可临时创建的虚拟文件关联,例如(文档不会)代理一个对话框的文本编辑域的内容。

How long does it live?

文档实例被对应的虚拟文件实例微弱的引用。因此,如果不再被引用,一篇未修改的文档实例可以被垃圾回收,而如果文档内容之后又再次被访问,新的实例将被创建。将文档引用保存在你的插件的持久数据结构中将导致内存泄漏。

How do I create one?

如果你需要在磁盘创建一个新文件,你不会创建一个文档:你创建一个PSI文件然后取出它的文档。如果你需要创建一个不需要绑定到任何对象上的文档实例,你可以使用EditorFactory.createDocument。

How do I get notified when it changes?

Document.addDocumentListener允许你接收指定文档实例的变更通知。

EditorFactory.getEventMulticaster().addDocumentListener允许你接收所有打开文档的变更通知。

FileDocumentManager.addFileDocumentManagerListener允许你接收任何文档保存到磁盘或从磁盘重新加载的通知。

What are the rules of working with it?

使用常规的读写操作规则。作为补充,任何修改文档内容的操作必须被封装成一个命令(CommandProcessor.getInstance().executeCommand())。executeCommand()调用可以被嵌套,而且最外层的executeCommand调用被添加到回退列表中。如果多个文档在一条命令中被修改了,回退这条命令将默认向用户展示一个确认对话框。

如果对应文档的文件是只读的(例如,未从版本控制系统中检出),文档修改将会失败。因此,在修改文档前,有必要调用ReadonlyStatusHandler.getInstance(project).ensureFilesWritable()来进行必要的文件检出。

所有传递给文档修改方法(setText, insertString, replaceString)的文本字符串必须使用\n作为换行符。

Samples

一个说明如何使用文档文件的示例插件项目可以在<%IDEA project directory%>/community/samples/textEditor文件夹下得到。更多信息,查阅Sample Text File Editor

PSI文件

一个PSI(Program Structure Interface)文件是将文件内容按照特定编程语言的元素层次结构相对应进行代理的根结构。PsiFile类是所有PSI文件的通用基类,而特定语言的文件常常由它的子类进行代理。例如,PsiJavaFile类代理一个Java文件,而XmlFile类代理一个XML文件。

与有application作用范围(一个即使在多个工程中打开,各展示实例也由同一个虚拟文件实例代理。注:此处原文直译:即使打开了多个工程,每个文件文件也由相同的虚拟文件实例代理。应该是原文表述有误)的虚拟文件和文档不同,PSI拥有project作用范围(如果一个文件所属的不同项目同时打开,这个文件会被不同的PsiFile实例代理)。

How do I get one?

  • 从action中:e.getData(LangDataKeys.PSI_FILE)
  • 从虚拟文件中:PsiManager.getInstance(project).findFile()
  • 从文档中:PsiDocumentManager.getInstance(project).getPsiFile()
  • 从文件中的一个元素:psiElement.getContainingFile()
  • 使用FilenameIndex.getFilesByName(project, name, scope)方法从项目任意位置找到一个有指定名称的文件

What can I do with one?

由于PSI是基于语言的,PSI文件是通过Language对象使用LanguageParserDefinitions.INSTANCE.forLanguage(language).createFile(fileViewProvider)方法创建的。

与文档相似,PSI文件是在访问特定文件时按需创建的。

How long does it live?

与文档相似,PSI文件被对应的虚拟文件实例微弱的引用,而当无人引用时会被垃圾回收。

How do I create one?

PsiFileFactory.getInstance(project).createFileFromText()方法使用指定内容创建一个内存PSI文件。使用PsiDirectory.add()方法将PSI文件保存到磁盘。

How do I get notified when it changes?

PsiManager.getInstance(project).addPsiTreeChangeListener()方法允许你接收项目中PSI树的所有变更通知。

How do I extend it?

PSI可以被在自定义语言插件中继承以支持额外的语言。开发这样的插件将在另一篇文章中展开讨论。

What are the rules for working with it?

所有对PSI文件内容的变更都作用到文档上,所以可以应用所有文档操作的规则(读写动作、命令、只读状态处理等)。

文件视图Provider

文件视图provider(参见类FileViewProvider)是IntelliJ IDEA 6.0版本中引入的一个新概念。它的主要目的是管理访问单一文件中不同的PSI树。例如,一个JSPX页面文件中包含:一个独立蝗Java代码PSI树(PsiJavaFile)、一个独立的XML代码结构树(XmlFile)、一个独立的全面的JSP结构树(JspFile)。每一个PSI树都覆盖了文件的全部内容,并且各自的视图中在包含不同语言的片段处都可以找到特别的“外部语言元素”。

一个FileViewProvider实例对应一个单独的虚拟文件、单独的文档,并用于取回多种PsiFile实例。

How do I get one?

  • 从虚拟文件中:PsiManager.getInstance(project).findViewProvider()
  • 从PSI文件中:psiFile.getViewProvider()

What can I do with one?

获取文件中存在的所有语言的PSI树列表:fileViewProvider.getLanguages()

获取指定语言的PSI树:fileViewProvider.getPsi(language),其中language参数可以取StdLanguages类中定义的Language类型候选值。例如,要获取XML的PSI树,使用代码:fileViewProvider.getPsi(StdLanguages.XML)。

要获取文件特定位置(偏移量)特定语言的元素:fileViewProvider.findElementAt(offset,language)

How do I extend it?

要创建一种内容穿插分布了不同语言结构树的文件类型,你的插件必需包含一个IntelliJ IDEA内核可访问的fileType.fileViewProviderFactory扩展点类型的扩展。这种扩展点使用FileTypeExtensionPoint类型的bean来声明。要访问声明的扩展点,创建一个Java类继承FileViewProviderFactory接口,在这个类中,重载(实现)createFileViewProvider方法。

要声明fileType.fileViewProviderFactory扩展点类型的扩展,在plugin.xml文件中的<extensions>小节增加如下语法化代码块:

 <fileType.fileViewProviderFactory filetype=%file type% implementationClass=%class name%>
 </fileType.fileViewProviderFactory>
其中%file type%指出要创建的文件类型(如JFS),然后%class name%指定你实现FileViewProviderFactory接口的Java类名称。

PSI元素

一个PSI文件代理PSI元素的层次结构(也被称作PSI树)。在一种特定的编程语言中,一个独立的PSI文件可能包含多个PSI树。一个PSI元素,反过来说,可能存在子PSI元素。

PSI元素和各个PSI元素层面上的操作被用来探索IntelliJ IDEA所解释的源代码的内部结构。例如,你可以用PSI元素进行像代码检查代码修正猜测这样的代码分析。

PsiElement类是PSI元素的通用基类。

How do I get a PSI element?

  • 从Action中:e.getData(LangDataKeys.PSI_ELEMENT)注意:如果一个编辑器正被打开(焦点状态)而且光标下的元素是一个引用,这将返回引用对象的结果。这可能不是你需要的。
  • 从文件的偏移量:PsiFile.findElementAt()。注意:这将返回指定偏移位置的最底层元素,这常常是一个语法块。你很可能需要使用PsiTreeUtil.getParentOfType()方法找出你真正想要的元素。
  • 通过遍历一个PSI文件:使用一个PsiRecursiveElementWalkingVisitor实例
  • 通过解析一个引用:PsiReference.resolve()
  • 另可参见:PSI Cookbook

What can I do with one?

参见:PSI Cookbook


原文待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值