What is the best way to setup a file watcher in a plugin?

My use case - an lsp for the language that I’m building a plugin for requires a json file with all the available source files that is generated by another cli tool. So I setup a service that uses AsyncFileListener to regenerate the json when files are moved and deleted, but the create event is not tracked.
Should I use BulkFileListener instead and do something like

project.messageBus.connect(fileListenerDisposable).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
            override fun after(events: MutableList<out VFileEvent>) {
                val index = ProjectFileIndex.getInstance(project)
                val luaFileEvents =
                    events
                        .filter { it is VFileMoveEvent || it is VFileCopyEvent || it is VFileCreateEvent || it is VFileDeleteEvent }
                        .filter {
                            it.file?.let { file -> file.fileType == LuauFileType && index.isInContent(file) } ?: false
                        }
                if (luaFileEvents.isEmpty()) return
                queueSourcemapRegeneration()
            }
        })

Or there is a better way?
I also saw there is PluggableFileWatcher, but given its API documentation I doubt it’s the thing I need.

From AsyncFileListener javadoc:

Note that this listener can only observe the state of the system before VFS events, and so
it can’t work with anything that would be after them, e.g. there will be no file in
VFileCreateEvent, com.intellij.openapi.roots.FileIndexFacade
won’t know anything about the updated state after VFS change, and so on.

  1. Do you not receive any VFileCreateEvent in your listener?
  2. Or does index.isInContent(file) fail here?

I don’t get any VFileCreateEvent in the AsyncFileListener at all, and it’s according to the docs as far as I understand, so AsyncFileListener doesn’t suit my needs. The question is: is the BulkFileListener a good enough solution here, or there is a better one?

BulkFileListener and AsyncFileListener operate on the same set of events, the difference is only in when they are called. For your case, the latter suits just fine.

The most probable reason for missing VFileCreateEvent is that no one has called VirtualFile#getChildren on the parent directory yet.

Better avoid calling VFileEvent#getFile on VFileCreateEvent – it is time-consuming and might return null. I’d suggest using VFileEvent#getFile together with FileTypeRegistry#getFileTypeByFileName.

You also need to handle renames (VFilePropertyChangeEvent#getPropertyName == VirtualFile#PROP_NAME).

If you ask me, Lua is such a simple language that coding a proper language support is simpler than dealing with LSP and that another CLI tool :slight_smile:

I doublechecked and I see the VFCreateEvent, indeed. The problem was that file doesn’t exist, and this is exactly what is said in the docs, but I’ve read it as “the event doesn’t exist” :man_facepalming: It’s time to learn how to read.
I found that renames are missing today, so that’s also fixed, thank you for mentioning.

I did this in the end, because all the CLI stuff doesn’t support complex setups anyway and just path checks should be fine

val eventsOfInterest =
    events
        .filter { it is VFileMoveEvent || it is VFileCopyEvent || it is VFileCreateEvent || it is VFileDeleteEvent || (it is VFilePropertyChangeEvent && it.propertyName == VirtualFile.PROP_NAME)  }
        .filter {
            // Should be fine to feed the whole path into getFileTypeByFileName, it searches for the last extension inside
            FileTypeRegistry.getInstance().getFileTypeByFileName(it.path) == LuauFileType && it.path.startsWith(projectDir.path)
        }

As for the lua simplicity, I kinda agree, but while I dive into the full language support the quick one via lsp makes sense. And the plugin is for luau :smiley: it’s almost lua, but with types, and building the type inference myself is not that straightforward.

Thank you so much for the help!

1 Like