Skip to content

Cursive freezes the IDE UI: FileContentWatcherService blocks the EDT via runBlocking{} inside a beforeRootsChange listener #3103

@nhooey

Description

@nhooey

Summary

Cursive's cursive.files.FileContentWatcherService registers a ProjectRootManager beforeRootsChange listener that, when a roots/model change fires, calls stopDocumentProcessing which invokes kotlinx.coroutines.runBlocking { ... } directly on the AWT Event Dispatch Thread (EDT).

runBlocking parks the EDT until a coroutine running on Dispatchers.Default completes. This violates the IntelliJ Platform rule that the EDT must never be blocked — and especially must never block awaiting a coroutine on another dispatcher. The result is a full, input-ignoring IDE UI freeze on every project open and on every module roots/library change (e.g. Maven/Gradle import/sync) while the Cursive plugin is enabled.

The freeze was confirmed sustained — the EDT was observed stuck in this exact runBlocking across four consecutive JetBrains auto-captured freeze thread dumps spanning roughly 76 seconds, not a momentary sampling artifact.

Environment

Item Value
IDE IntelliJ IDEA Ultimate, build IU-261.25134.95 (2026.1)
JBR / JVM Java 25.0.3
Cursive plugin version 2026.1-261 (bundled as plugins/clojure-plugin/lib/cursive.jar)
OS macOS (Darwin 24.6.0)
Rendering Apple / OpenGL Java2D pipeline
Notable state 13 project windows open simultaneously at time of freeze

Steps to Reproduce

  1. Have the Cursive (Clojure) plugin enabled.
  2. Open a project. (Reproduced reliably by opening a project.)
  3. Observe the IDE during the model/roots-update phase — for example while the status bar shows "Downloading repository indexes" during a Maven/Gradle import/sync.

The IDE UI freezes and ignores all input during this phase.

Notes on the trigger:

  • beforeRootsChange fires whenever module roots/libraries change — notably during Maven/Gradle project import/sync (the model-update / "Downloading repository indexes" phase) and on project open.
  • Deleting .idea/ and .iml files before reopening does not help, because the listener is registered globally by the plugin and is independent of any per-project configuration.

Expected Behavior

A roots/model change (project open, Maven/Gradle sync) should not block the EDT. The IDE should remain fully responsive to user input while Cursive tears down or restarts its document-processing work.

Actual Behavior

The EDT (AWT-EventQueue-0) parks inside kotlinx.coroutines.runBlocking, called synchronously from Cursive's beforeRootsChange listener, until a Dispatchers.Default coroutine completes. The entire IDE UI freezes and ignores all input for the duration of the block.

Root Cause Analysis

The defect is in cursive.files.FileContentWatcherService:

  • It registers a ProjectRootManager beforeRootsChange listener (setupDocumentListeners$1.beforeRootsChange, watcher.kt:172).
  • That listener calls stopDocumentProcessing (watcher.kt:151), which invokes kotlinx.coroutines.runBlocking { ... } directly on the EDT.
  • runBlocking parks the EDT (BlockingCoroutine.joinBlocking) until a Dispatchers.Default coroutine completes.

The coroutine the EDT is waiting on is cursive.files.FileContentWatcherService$startDocumentProcessing$1 (watcher.kt:142) — a ProducerCoroutine running on Dispatchers.Default, in state SUSPENDED. Because the EDT cannot proceed until that producer is torn down, and the producer runs on a contended dispatcher, the EDT stays parked.

The beforeRootsChange event itself is delivered synchronously via the platform MessageBus, dispatched from the workspace-model commit (WorkspaceModelImpl.onBeforeChangedProjectRootManagerImpl.fireBeforeRootsChangedProjectRootManagerComponent.fireBeforeRootsChangeEvent), so any blocking inside the listener directly blocks the thread that fired it — here, the EDT.

EDT stack trace (captured on AWT-EventQueue-0, state TIMED_WAITING on kotlinx.coroutines.BlockingCoroutine)

"AWT-EventQueue-0" prio=0 tid=0x0 nid=0x0 waiting on condition
   java.lang.Thread.State: TIMED_WAITING
 on kotlinx.coroutines.BlockingCoroutine@3631388b
	at java.base@25.0.3/jdk.internal.misc.Unsafe.park(Native Method)
	at java.base@25.0.3/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:271)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:121)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$BuildersKt__BuildersKt(Builders.kt:87)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:55)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:50)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at cursive.files.FileContentWatcherService.stopDocumentProcessing(watcher.kt:151)
	at cursive.files.FileContentWatcherService.access$stopDocumentProcessing(watcher.kt:44)
	at cursive.files.FileContentWatcherService$setupDocumentListeners$1.beforeRootsChange(watcher.kt:172)
	at com.intellij.util.messages.impl.MessageBusImplKt.invokeMethod(MessageBusImpl.kt:831)
	at com.intellij.util.messages.impl.MessageBusImplKt.invokeListener(MessageBusImpl.kt:775)
	at com.intellij.util.messages.impl.MessageBusImplKt.deliverMessage(MessageBusImpl.kt:514)
	at com.intellij.util.messages.impl.MessageBusImplKt.pumpWaiting(MessageBusImpl.kt:493)
	at com.intellij.util.messages.impl.MessagePublisher.invoke(MessageBusImpl.kt:556)
	at jdk.proxy2/jdk.proxy2.$Proxy269.beforeRootsChange(Unknown Source)
	at com.intellij.openapi.roots.impl.ProjectRootManagerComponent.fireBeforeRootsChangeEvent(ProjectRootManagerComponent.kt:264)
	at com.intellij.openapi.roots.impl.ProjectRootManagerImpl.fireBeforeRootsChanged(ProjectRootManagerImpl.kt:430)
	at com.intellij.openapi.roots.impl.ProjectRootManagerImpl$BatchSession.beforeRootsChanged(ProjectRootManagerImpl.kt:127)
	at com.intellij.workspaceModel.ide.impl.legacyBridge.project.ModuleRootListenerBridgeImpl.fireBeforeRootsChanged(ModuleRootListenerBridgeImpl.kt:42)
	at com.intellij.workspaceModel.ide.impl.legacyBridge.module.LegacyProjectModelListenersBridge.beforeChanged(LegacyProjectModelListenersBridge.kt:50)
	at com.intellij.workspaceModel.ide.impl.WorkspaceModelImpl.onBeforeChanged$lambda$0$0(WorkspaceModelImpl.kt:493)
	...

The coroutine being awaited (separately reported in the same dumps) is cursive.files.FileContentWatcherService$startDocumentProcessing$1 (watcher.kt:142), a ProducerCoroutine on Dispatchers.Default, state SUSPENDED.

Frequency / Severity

Severity: high — full IDE UI freeze. The IDE becomes completely unresponsive to input on every project open / roots change while the plugin is enabled.

Evidence that this is a sustained hang and not a one-off sampling blip:

  • The EDT was observed stuck in this exact runBlocking across four consecutive thread dumps taken 15 seconds apart, spanning roughly 76 seconds (14:50:13, 14:50:29, 14:50:44, 14:51:29 on 2026-06-15).
  • idea.log shows repeated "400–560 ms to grab EDT" warnings in the lead-up to the freeze.

Aggravating factor: with 13 open projects, each running Cursive's full activity set on Dispatchers.Default, the Default dispatcher is under contention. The coroutine the EDT blocks on is therefore slow to be scheduled, lengthening each stall.

Workaround

Disable the Cursive / Clojure plugin. This was confirmed to eliminate the freeze, tying the hang directly to Cursive.

Suggested Fix

(Offered as a suggestion.) Do not call runBlocking on the EDT inside beforeRootsChange. Any of the following would resolve the EDT block:

  • (a) Make stopDocumentProcessing non-blocking — launch the teardown on a background coroutine and let the document-processing producer be cancelled asynchronously rather than awaited on the EDT.
  • (b) Move the teardown out of the synchronous root-change callback entirely.
  • (c) If synchronous teardown is genuinely required, ensure it completes without dispatching to and awaiting Dispatchers.Default from the EDT.

Appendix: Source thread-dump files

All four files are in the directory:

~/Library/Logs/JetBrains/IntelliJIdea2026.1/threadDumps-Dispatchers.Default-20260615-144713-IU-261.25134.95/

The following sequential dumps all show the same EDT block in the exact runBlocking described above:

  • threadDump-20260615-145013-1781509813764.txt (14:50:13)
  • threadDump-20260615-145029-1781509829105.txt (14:50:29)
  • threadDump-20260615-145044-1781509844381.txt (14:50:44)
  • threadDump-20260615-145129-1781509889818.txt (14:51:29)

Let me know if you need the thread dump files

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions