{"id":2678,"date":"2026-06-04T03:32:35","date_gmt":"2026-06-04T03:32:35","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/06\/04\/async-vfs-content-writes-what-plugin-authors-need-to-know\/"},"modified":"2026-06-04T03:32:35","modified_gmt":"2026-06-04T03:32:35","slug":"async-vfs-content-writes-what-plugin-authors-need-to-know","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/06\/04\/async-vfs-content-writes-what-plugin-authors-need-to-know\/","title":{"rendered":"Async VFS Content Writes \u2013 What Plugin Authors Need to Know"},"content":{"rendered":"<div>\n<div>\n<section data-clarity-region=\"article\">\n<div>\n<p><a href=\"\/platform\/category\/intellij-platform\/\">IntelliJ Platform<\/a> <a href=\"\/platform\/category\/plugins\/\">Plugins<\/a><\/p>\n<h2 id=\"major-updates\">Async VFS Content Writes \u2013 What Plugin Authors Need to Know<\/h2>\n<p>Some plugin code follows this pattern:<\/p>\n<ol>\n<li>Save open documents.<\/li>\n<li>Get a file or directory path.<\/li>\n<li>Pass that path to something outside the IDE, such as a formatter, linter, compiler, VCS command, language server, or custom CLI tool.<\/li>\n<\/ol>\n<p>Historically, it was reasonable to assume that once the save finishes, the file on disk already contains the latest editor text.<\/p>\n<p>That is no longer guaranteed.<\/p>\n<p>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 <code>Path<\/code>, <code>File<\/code>, <code>Files.*<\/code>, or an external process may need an explicit flush before the handoff.<\/p>\n<p>The official SDK docs cover that contract in<a href=\"https:\/\/plugins.jetbrains.com\/docs\/intellij\/virtual-file.html#when-are-virtualfile-changes-persisted-on-disk-and-loaded-from-disk-to-vfs\" target=\"_blank\" rel=\"noopener\"> When are <code>VirtualFile<\/code> changes persisted on disk and loaded from disk to VFS?<\/a>.<\/p>\n<h2><strong>Why This Exists<\/strong><\/h2>\n<p>Writes to <a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/core-api\/src\/com\/intellij\/openapi\/vfs\/VirtualFile.java\" target=\"_blank\" rel=\"noopener\"><code>VirtualFile<\/code><\/a> 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.<\/p>\n<p>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.<\/p>\n<h2><strong>The Rule<\/strong><\/h2>\n<p>If your plugin saves and reads files using IntelliJ Platform file APIs, you probably do not need to change anything. This is fine:<\/p>\n<ul>\n<li>save a document through<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/core-api\/src\/com\/intellij\/openapi\/fileEditor\/FileDocumentManager.java#L94-L116\" target=\"_blank\" rel=\"noopener\"> <code>FileDocumentManager<\/code><\/a><\/li>\n<li>read it later through<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/core-api\/src\/com\/intellij\/openapi\/vfs\/VirtualFile.java\" target=\"_blank\" rel=\"noopener\"> <code>VirtualFile<\/code><\/a><\/li>\n<li>use VFS APIs such as <code>contentsToByteArray<\/code>, <code>getInputStream<\/code>, or<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/analysis-api\/src\/com\/intellij\/openapi\/vfs\/VfsUtil.java\" target=\"_blank\" rel=\"noopener\"> <code>VfsUtil<\/code><\/a><\/li>\n<\/ul>\n<p>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.<\/p>\n<p>If your code is about to read the physical file directly, or pass the path to another process, flush pending VFS writes first with<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/analysis-api\/src\/com\/intellij\/openapi\/vfs\/newvfs\/ManagingFS.java#L92-L112\" target=\"_blank\" rel=\"noopener\"> <code>ManagingFS<\/code><\/a>:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">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()<\/pre>\n<p>If you know the exact file, use the narrower version:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">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())<\/pre>\n<p>The throwing variants can wait for I\/O and can throw <code>IOException<\/code>, 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.<\/p>\n<p>For user-triggered actions where an IDE notification is more appropriate than handling an exception in your own code, use:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">ManagingFS.getInstance().flushPendingUpdatesOrNotify()<\/pre>\n<p>For example, an action that opens a generated or saved file in a browser can flush before<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/platform-api\/src\/com\/intellij\/ide\/browsers\/BrowserLauncher.kt\" target=\"_blank\" rel=\"noopener\"> <code>BrowserLauncher<\/code><\/a> hands it to the browser:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FileDocumentManager.getInstance().saveAllDocuments() ManagingFS.getInstance().flushPendingUpdatesOrNotify() BrowserLauncher.instance.browse(url, browser, project)<\/pre>\n<p>If saving happened earlier, keep the same idea: flush immediately before the external reader touches the file system.<\/p>\n<h2><strong>Places Worth Checking<\/strong><\/h2>\n<p>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.<\/p>\n<p>The platform codebase has been adjusted for many of these transitions, but plugins may still have their own cases. Common examples:<\/p>\n<ul>\n<li>launching formatters, linters, compilers, test runners, VCS commands, or language servers<\/li>\n<li>reading through <code>Files.readString<\/code>, <code>Files.newInputStream<\/code>, <code>Path<\/code>, or <code>File<\/code><\/li>\n<li>passing a project directory or file path to a CLI tool<\/li>\n<li>tests that write through VFS and assert through NIO<\/li>\n<li>VFS listeners that schedule later disk I\/O<\/li>\n<\/ul>\n<p>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.<\/p>\n<p><a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/platform-impl\/src\/com\/intellij\/openapi\/vfs\/impl\/local\/AsyncableLocalFileSystemImpl.kt#L324-L335\" target=\"_blank\" rel=\"noopener\">Current platform code<\/a> may flush pending writes from some<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/core-api\/src\/com\/intellij\/openapi\/vfs\/VirtualFile.java#L147\" target=\"_blank\" rel=\"noopener\"> <code>VirtualFile.toNioPath()<\/code><\/a> 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.<\/p>\n<h2><strong>Opt-In and Troubleshooting<\/strong><\/h2>\n<p>The feature is<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/platform-impl\/src\/com\/intellij\/openapi\/vfs\/impl\/async\/AsyncableFileSystemWrapper.kt#L34\" target=\"_blank\" rel=\"noopener\"> enabled by default<\/a>, but not every <code>getOutputStream() <\/code>call automatically becomes async.<\/p>\n<p>The requestor passed to<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/core-api\/src\/com\/intellij\/openapi\/vfs\/VirtualFile.java#L542-L561\" target=\"_blank\" rel=\"noopener\"> <code>VirtualFile.getOutputStream(requestor)<\/code><\/a> has to opt in. Today, the important path is editor saves:<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/platform-impl\/src\/com\/intellij\/openapi\/fileEditor\/impl\/FileDocumentManagerImpl.java#L121\" target=\"_blank\" rel=\"noopener\"> <code>FileDocumentManagerImpl<\/code><\/a> opts in, so files saved from the editor can go through the new branch.<\/p>\n<p>The opt-in marker itself,<a href=\"https:\/\/github.com\/JetBrains\/intellij-community\/blob\/master\/platform\/platform-impl\/src\/com\/intellij\/openapi\/vfs\/newvfs\/persistent\/executor\/AsyncFileContentWriteRequestor.java\" target=\"_blank\" rel=\"noopener\"> <code>AsyncFileContentWriteRequestor<\/code><\/a>, 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 <code>saveAllDocuments()<\/code> and direct disk access.<\/p>\n<p>To check whether a problem is related to this behavior, temporarily disable it with:<\/p>\n<pre data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">-Dvfs.async-content-write.enabled=false<\/pre>\n<p>When running a plugin with the IntelliJ Platform Gradle Plugin, pass the flag to the IDE process through the <code>runIde<\/code> task:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import org.gradle.process.CommandLineArgumentProvider tasks { &nbsp;&nbsp;runIde { &nbsp;&nbsp;&nbsp;&nbsp;jvmArgumentProviders += CommandLineArgumentProvider { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;listOf(\"-Dvfs.async-content-write.enabled=false\") &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;} }<\/pre>\n<h2><strong>Test Failures You May See<\/strong><\/h2>\n<p>This kind of test can become flaky:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">writeThroughVfs(virtualFile) assertEquals(\"expected\", Files.readString(virtualFile.toNioPath()))<\/pre>\n<p>The test writes through one view of the file system and reads through another. Make the boundary explicit:<\/p>\n<pre data-enlighter-language=\"kotlin\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">writeThroughVfs(virtualFile) ManagingFS.getInstance().flushPendingUpdates(virtualFile) assertEquals(\"expected\", Files.readString(virtualFile.toNioPath()))<\/pre>\n<p>If the assertion reads through VFS, no flush should be needed.<\/p>\n<\/p><\/div>\n<p> <a href=\"#\"><\/a> <\/section>\n<div>\n<p><h2>Discover more<\/h2>\n<\/p><\/div>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/blog.jetbrains.com\/platform\/2026\/06\/async-vfs-content-writes-what-plugin-authors-need-to-know\/\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>IntelliJ Platform Plugins Async VFS Content Writes \u2013 What Plugin Authors Need to Know Some plugin code follows this pattern: Save open documents. Get a file or directory path. 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2648,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[46],"tags":[],"class_list":["post-2678","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-jetbrain"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2678","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=2678"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2678\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/2648"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=2678"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=2678"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=2678"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}