Platform logo

JetBrains Platform

Plugin and extension development for JetBrains products.

IntelliJ Platform Plugins

Async VFS Content Writes – What Plugin Authors Need to Know

Some plugin code follows this pattern:

  1. Save open documents.
  2. Get a file or directory path.
  3. Pass that path to something outside the IDE, such as a formatter, linter, compiler, VCS command, language server, or custom CLI tool.

Historically, it was reasonable to assume that once the save finishes, the file on disk already contains the latest editor text.

That is no longer guaranteed.

The IntelliJ Platform can now update the VFS first and finish the disk write in the background a bit later. Code that reads the file through IntelliJ Platform file APIs still sees the new content immediately. Code that reads the same file through Path, File, Files.*, or an external process may need an explicit flush before the handoff.

The official SDK docs cover that contract in When are VirtualFile changes persisted on disk and loaded from disk to VFS?.

Why This Exists

Writes to VirtualFile must happen under a write action. Until now, saving a file often meant doing the actual file-system write while that write action was still open.

That is expensive when the file system is slow, remote, or mounted through WSL or Docker. Moving the disk write out of the write action is meant to reduce freezes during document saves.

The Rule

If your plugin saves and reads files using IntelliJ Platform file APIs, you probably do not need to change anything. This is fine:

VFS behaves as if the write has already happened. For example, a read action started after the write action should see the new content when it reads through VFS.

If your code is about to read the physical file directly, or pass the path to another process, flush pending VFS writes first with ManagingFS:

import com.intellij.openapi.vfs.newvfs.ManagingFS

FileDocumentManager.getInstance().saveAllDocuments()

// Flush outside a write action; this may wait for disk I/O.

ManagingFS.getInstance().flushPendingUpdates()

commandLine.createProcess()

If you know the exact file, use the narrower version:

FileDocumentManager.getInstance().saveDocument(document)

// Flush outside a write action; this may wait for disk I/O.

ManagingFS.getInstance().flushPendingUpdates(virtualFile)

val textOnDisk = Files.readString(virtualFile.toNioPath())

The throwing variants can wait for I/O and can throw IOException, so call them at the boundary where disk access is about to happen. Do not add a flush after every save just to be safe.

For user-triggered actions where an IDE notification is more appropriate than handling an exception in your own code, use:

ManagingFS.getInstance().flushPendingUpdatesOrNotify()

For example, an action that opens a generated or saved file in a browser can flush before BrowserLauncher hands it to the browser:

FileDocumentManager.getInstance().saveAllDocuments()

ManagingFS.getInstance().flushPendingUpdatesOrNotify()

BrowserLauncher.instance.browse(url, browser, project)

If saving happened earlier, keep the same idea: flush immediately before the external reader touches the file system.

Places Worth Checking

The fragile spots are handoffs from VFS-written files to direct disk readers. These can show up as stale reads, external tools seeing old content, or tests that become flaky because they write through VFS and assert through NIO.

The platform codebase has been adjusted for many of these transitions, but plugins may still have their own cases. Common examples:

  • launching formatters, linters, compilers, test runners, VCS commands, or language servers
  • reading through Files.readString, Files.newInputStream, Path, or File
  • passing a project directory or file path to a CLI tool
  • tests that write through VFS and assert through NIO
  • VFS listeners that schedule later disk I/O

For VFS listeners, flush where the disk access actually happens. If the listener only enqueues work, do not flush inside the synchronous listener. That puts waiting back under the write action.

Current platform code may flush pending writes from some VirtualFile.toNioPath() paths, because path conversion is often followed by NIO access or process launch. Do not use path conversion as the synchronization point in plugin code. If disk visibility matters, call the flush API explicitly.

Opt-In and Troubleshooting

The feature is enabled by default, but not every getOutputStream() call automatically becomes async.

The requestor passed to VirtualFile.getOutputStream(requestor) has to opt in. Today, the important path is editor saves: FileDocumentManagerImpl opts in, so files saved from the editor can go through the new branch.

The opt-in marker itself, AsyncFileContentWriteRequestor, is currently internal, so most third-party plugins should not rush to adopt async writes directly. The more immediate task is to audit assumptions around saveAllDocuments() and direct disk access.

To check whether a problem is related to this behavior, temporarily disable it with:

-Dvfs.async-content-write.enabled=false

When running a plugin with the IntelliJ Platform Gradle Plugin, pass the flag to the IDE process through the runIde task:

import org.gradle.process.CommandLineArgumentProvider
tasks {
  runIde {
    jvmArgumentProviders += CommandLineArgumentProvider {
      listOf("-Dvfs.async-content-write.enabled=false")
    }
  }
}

Test Failures You May See

This kind of test can become flaky:

writeThroughVfs(virtualFile)

assertEquals("expected", Files.readString(virtualFile.toNioPath()))

The test writes through one view of the file system and reads through another. Make the boundary explicit:

writeThroughVfs(virtualFile)

ManagingFS.getInstance().flushPendingUpdates(virtualFile)

assertEquals("expected", Files.readString(virtualFile.toNioPath()))

If the assertion reads through VFS, no flush should be needed.