IDEA插件开发——VFS
2024.5.31
VFS(Virtual File System, 虚拟文件系统)是IntelliJ平台中的一个抽象概念, 它允许访问计算机上的文件(本地文件系统上的文件, 甚至是JAR文件中的文件), 仓库中的文件或网络上的文件. 有多种方法可以访问集成开发环境中的文件内容:
- VFS 允许访问最底层的文件(最接近实际文件系统, 而不是像 PSI 这样的抽象文件系统).
- Document提供了一个对象模型, 可以以纯文本的形式访问文件内容(因此它介于 VFS 和 PSI 之间).
- PSI(程序结构接口)允许以分层对象模型访问文件内容, 该模型考虑了特定语言的语法和语义(有点像 DOM 在网页浏览器中表示 HTML 和 CSS 内容的方式).
VFS
VFS 是 IntelliJ 平台用于处理文件的工具. 它提供:
- 用于处理文件的通用 API, 无论文件位于何处(磁盘, JAR 文件, HTTP 服务器, VCS 等).
- 用于跟踪文件修改的信息, 并在检测到文件发生变化时提供文件的新旧版本.
- 能力用于在 VFS 中将附加的持久数据与文件提供关联.
VFS 管理通过集成开发环境访问的文件的持久快照. 这些快照只存储那些通过 VFS API 请求过至少一次的文件. 关于 VFS 的性质, 以下是一些需要注意的重要事项:
- 缓存的内容是异步刷新的, 以匹配磁盘上的任何变化.
- 请记住, 快照是存储在应用程序级别的, 正如你所期望的那样, 因此在多个项目中打开的文件将只有一个快照.
- 快照在刷新操作过程中从磁盘更新, 刷新操作通常是异步进行的. 通过 VFS 进行的所有写入操作都是同步的, 内容会立即保存到磁盘上.
从官方文档 阅读有关 VFS 的更多信息.
使用 VFS 处理插件所需的文件时, 会遇到很多常见情况.
获取项目中所有虚拟文件的列表
按名称Lambdas.kt获取虚拟文件列表的代码段.
fun getListOfProjectVirtualFilesByName(project: Project,
caseSensitivity: Boolean = true,
fileName: String = "Lambdas.kt"
): MutableCollection<VirtualFile> {
val scope = GlobalSearchScope.projectScope(project)
return FilenameIndex.getVirtualFilesByName(
project, fileName, caseSensitivity, scope)
}
获取扩展名为kt的虚拟文件列表的代码段.
fun getListOfProjectVirtualFilesByExt(project: Project,
caseSensitivity: Boolean = true,
extName: String = "kt"
): MutableCollection<VirtualFile> {
val scope = GlobalSearchScope.projectScope(project)
return FilenameIndex.getAllFilesByExt(project, extName, scope)
}
获取项目中所有虚拟文件列表的代码段.
fun getListOfAllProjectVFiles(project: Project): MutableCollection<VirtualFile> {
val collection = mutableListOf<VirtualFile>()
ProjectFileIndex.getInstance(project).iterateContent {
collection += it
// Return true to process all the files (no early escape).
true
}
return collection
}
以编程方式附加监听器以查看虚拟文件的变化
可以通过编程或声明的方式附加监听器.
这是以编程方式附加VFS变化监听器的过时方法:
VirtualFileManager.getInstance().addVirtualFileListener()
以编程方式添加监听器的新方法是监听总线(又称主题)上的VirtualFileManager.VFS_CHANGES事件.
由于无法按路径或文件名过滤这些更改事件, 因此需要在监听器中加入过滤不需要的事件的逻辑.
下面的函数展示了如何在代码中进行注册. 请注意, 该函数在 EDT 中运行.
private fun attachListenerForProjectVFileChanges(): Unit {
println("MyPlugin: attachListenerForProjectFileChanges()")
val connection = project.messageBus.connect(/*parentDisposable=*/ project)
connection.subscribe(
VirtualFileManager.VFS_CHANGES,
object : BulkFileListener {
override fun after(events: List<VFileEvent>) = doAfter(events)
})
}
fun handleEvent(event: VFileEvent) {
when (event) {
is VFilePropertyChangeEvent -> {
println("VFile property change event: $event")
}
is VFileContentChangeEvent -> {
println("VFile content change event: $event")
}
}
}
fun doAfter(events: List<VFileEvent>) {
println("VFS_CHANGES: #events: ${events.size}")
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
events.withIndex().forEach { (index, event) ->
println("$index. VFile event: $event")
// Filter out file events that are not in the project's content.
events
.filter { it.file != null && projectFileIndex.isInContent(it.file!!) }
.forEach { handleEvent(it) }
}
}
使用这种方法, 必须在某个时刻运行附加监听器本身的代码. 因此, 如果这是一个应在项目打开时运行的监听器, 则可能需要添加一个postStartupActivity, 这只是一个由插件提供的类, 将在 IDE 打开项目后运行. 请在动态插件部分阅读所有相关内容.
💡 如果你希望引用当前打开的项目, 你可能希望使用这种方法, 而不是下图所示的声明式方法.
以声明方式附加监听器查看虚拟文件的更改
你也可以在plugin.xml中声明式地注册监听器. 使用这种方法与上述代码方法有一些不同之处. 你可以在plugin.xml中为特定事件类注册一个监听器, 而不是订阅一个主题.
下面的代码段与上面的代码类似. 因此, VirtualFileManager.VFS_CHANGES主题相当于com.intellij.openapi.vfs.newvfs.BulkFileListener类.
<applicationListeners>
<listener class="MyListener" topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>
代码在这里.
class MyVfsListener : BulkFileListener {
@Override
fun after(@NotNull events: List<VFileEvent?>?) {
// handle the events
}
}
官方文档 中有关注册 VFS 监听器的更多信息.
💡 也可以声明地附加项目级的监听器. 不过, 这要求你将在 XML 中注册的接口/类可以将项目对象作为参数. 就 VFS 监听器而言, 它不接受项目参数, 因为 VFS 操作是应用级的. 因此, 如果要获取当前打开的项目, 就必须使用编程方法.
异步处理文件系统事件
可以异步(在后台线程中)获取这些文件系统事件. 请查看AsyncFileListener.java类中的示例, 了解如何做到这一点. 异步版本不像在UI线程上运行的版本那样容易实现. 下面是一些相关说明:
- 你可以为
vfs.asyncListener注册一个扩展, 在plugin.xml中注册你的异步监听器. - 或者可以调用
VirtualFileManager.java的addVirtualFileListener()方法.
在保存当前打开的文件时进行拦截
这是一个代码片段, 可使用AppTopics.FILE_DOCUMENT_SYNC主题在保存文件前获取事件. 请阅读有关Document 的更多信息, 以了解此事件的作用和来源.
请注意, 该监听器将触发任何正在保存的文件, 而不仅仅是当前正在编辑的文件.
- 因此, 如果你要依靠它来触发当前打开文件的 JUST, 那就不能使用这个方法.
- 然而, 如果你想在编辑器中打开的文件被保存之前做一些事情, 那么这里就是捕获这些事件的地方.
/**
* - [Tutorial](http://arhipov.blogspot.com/2011/04/code-snippet-intercepting-on-save.html)
* - [FileDocumentManagerListener]
* - [AppTopics]
*/
private fun attachFileSaveListener() {
printHeader()
val connection = project.messageBus.connect(/*parentDisposable=*/ project)
connection.subscribe(
AppTopics.FILE_DOCUMENT_SYNC,
object : FileDocumentManagerListener {
override fun beforeDocumentSaving(document: Document) {
val vFile = FileDocumentManager.getInstance().getFile(document)
println(StringBuilder().apply {
append("A VirtualFile is about to be saved\n")
append("\tvFile: $vFile\n")
append("\tdocument: $document\n")
})
}
})
}
Document
Document API 是一种将文件作为简单文本文件从IDE中访问的方法. 在加载或保存文件时, IntelliJ 平台会透明地处理编码和换行. 有几种获取Document对象的方法.
- 使用
e.getRequiredData(CommonDataKeys.EDITOR).document从操作中获取. - 使用
FileDocumentManager.document从虚拟文件获取. - 使用
PsiDocumentManager.getInstance().document从 PSI 文件获取.
使用Document API 的操作示例
下面是一个使用 Document API 将 IDE 中的某些选定文本替换为另一个字符串的操作示例.
class EditorReplaceTextAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val editor: Editor = e.getRequiredData(CommonDataKeys.EDITOR)
val project: Project = e.getRequiredData(CommonDataKeys.PROJECT)
val document: Document = editor.document
val caretModel: CaretModel = editor.caretModel
val primaryCaret = caretModel.primaryCaret
// start and end offsets of the selected text (based on the primaryCaret).
val selection =
Pair<Int, Int>(primaryCaret.selectionStart, primaryCaret.selectionEnd)
// Actual content that is selected at the caret.
val selectedText: String = primaryCaret.selectedText!!
// Change the document in a write action in a command (for undo).
WriteCommandAction.runWriteCommandAction(project) {
document.replaceString(selection.first, selection.second, ">> $selectedText <<")
}
// Deselect the selection of the text that that was just replaced.
primaryCaret.removeSelection()
}
override fun update(e: AnActionEvent) {
val project: Project? = e.project
val editor: Editor? = e.getData(CommonDataKeys.EDITOR)
// Action visible only if the editor in the open project has text selected.
e.presentation.isEnabledAndVisible =
project != null
&& editor != null
&& editor.selectionModel.hasSelection()
}
}