IJPL-196231 Fix launch(Once)OnShow for unconfined coroutines

Dispatchers.Unconfined doesn't immediately invoke
the coroutine if already running an unconfined coroutine.
If the current coroutine is blocked on dialog.show(),
it means that no unconfined jobs will be executed
while the dialog is showing.

To work around it, we replace Dispatchers.Unconfined
with the appropriate UI dispatcher that is guaranteed
to not dispatch if used with any() modality.

Implement the fix under a registry key,
disabled for now, so we can safely pick it
to the release branches to have this as a possible
manual fallback for the users who experience this issue.

Reviewed in IJ-CR-168711 for 252 and release.

(cherry picked from commit 3cfd1a9e46d12d942c7ab6f30f158b71cd7e52d8)

GitOrigin-RevId: c700503bf1f2c328d4be4eb088210a6deed536de
This commit is contained in:
Sergei Tachenov
2025-07-09 15:17:56 +03:00
committed by intellij-monorepo-bot
parent c95451c5d7
commit 26b320e5d1
2 changed files with 29 additions and 9 deletions

View File

@@ -1,10 +1,8 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util.ui
import com.intellij.openapi.application.AccessToken
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.UI
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.*
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.kernel.withKernel
import com.intellij.ui.ComponentUtil
import com.intellij.util.BitUtil
@@ -155,11 +153,30 @@ fun <C : Component> C.launchOnShow(
* Launches the given task in the global scope without dispatching.
*/
private fun launchUnconfined(debugName: String, block: suspend CoroutineScope.() -> Unit): Job {
if (Registry.`is`("ide.ui.coroutine.scopes.unconfined.fix", false)) {
// The whole point here is to launch the coroutine in-place without dispatching,
// and to make sure it doesn't dispatch later when called on the EDT from the hierarchy listener.
// This is why Dispatchers.ui(UiDispatcherKind.RELAX, immediate = true) is used with ModalityState.any().
// The modality is obvious: using any() ensures that the current modality is ignored when processing hierarchy events.
// The task itself will then check the modality before executing, but events should be processed immediately.
// The dispatcher is a bit more tricky: using EDT will dispatch if the WIL is forbidden (when already running under UI),
// and using UI will dispatch if the WIL is currently locked.
// But RELAX doesn't care about the WIL and whether it's allowed, so it will not dispatch if immediate = true.
// Note that using Dispatchers.Unconfined here seems the obvious choice,
// but it doesn't work when already running an unconfined coroutine (IJPL-196231, CPP-45385).
@OptIn(DelicateCoroutinesApi::class)
return GlobalScope.launch(
context = Dispatchers.ui(UiDispatcherKind.RELAX, immediate = true) + ModalityState.any().asContextElement() + CoroutineName(debugName),
block = block,
)
}
else {
@OptIn(DelicateCoroutinesApi::class)
return GlobalScope.launch(
context = Dispatchers.Unconfined + CoroutineName(debugName),
block = block,
)
}
}
private suspend fun showingAsChannel(component: Component, block: suspend (ReceiveChannel<Boolean>) -> Unit) {

View File

@@ -2507,6 +2507,9 @@ coroutine.scope.model.description=Use the new experimental coroutine-based model
ide.treat.project.modality.as.application=true
ide.treat.project.modality.as.application.description=Treat DialogWrapper.IdeModalityType.PROJECT as Dialog.ModalityType.APPLICATION_MODAL
ide.ui.coroutine.scopes.unconfined.fix=false
ide.ui.coroutine.scopes.unconfined.fix.description=Enable the fix for launchOnShow/launchOnceOnShow issue that prevented these from working in modal dialogs sometimes.
editor.show.sticky.lines.debug=false
editor.show.sticky.lines.debug.description=Show editor sticky lines in debug mode
editor.show.sticky.lines.shadow=true