mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[JEWEL-1359] Fix Compose paste semantics traversal threading
Compose paste action updates could traverse the Compose semantics tree from BGT even though Compose UI tree access is EDT-confined. This made action updates vulnerable to transient semantics tree inconsistencies while Compose was mutating layout state. Run the paste provider update on EDT and make focused semantics lookup fail closed for the observed transient nullability failure. The lookup still behaves as no focused Compose component being available, avoiding noisy action-update errors. Add focused coverage for the provider update thread and fail-closed semantics lookup behavior. closes https://github.com/JetBrains/intellij-community/pull/3570 (cherry picked from commit 27c0d0d293c6564330177a14513119ba8b37ac36) (cherry picked from commit dbf73d9a52504935b63530ce5ed8450d4833bfb4) IJ-MR-215743 GitOrigin-RevId: 5be520dc5115066833eaab6196f5bf430ed6081a
This commit is contained in:
committed by
intellij-monorepo-bot
parent
b0fb37244d
commit
663e2d1864
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.bridge.actionSystem
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import org.jetbrains.jewel.bridge.ComposeSemanticsTreeUtils
|
||||
import org.jetbrains.jewel.foundation.InternalJewelApi
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(InternalJewelApi::class)
|
||||
public class ComposePasteProviderTest {
|
||||
@Test
|
||||
public fun `paste provider updates on EDT`() {
|
||||
assertSame(ActionUpdateThread.EDT, ComposePasteProvider().actionUpdateThread)
|
||||
}
|
||||
|
||||
@Test
|
||||
public fun `focused component lookup returns null when semantics traversal fails`() {
|
||||
assertNull(
|
||||
ComposeSemanticsTreeUtils.findFocusedComponent {
|
||||
throw NullPointerException("transient Compose semantics tree mutation")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = ProcessCanceledException::class)
|
||||
public fun `focused component lookup does not swallow cancellation`() {
|
||||
ComposeSemanticsTreeUtils.findFocusedComponent { throw ProcessCanceledException() }
|
||||
}
|
||||
}
|
||||
+22
-5
@@ -5,6 +5,7 @@ import androidx.compose.ui.awt.ComposePanel
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.semantics.SemanticsNode
|
||||
import androidx.compose.ui.semantics.SemanticsOwner
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getAllSemanticsNodes
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
@@ -14,13 +15,29 @@ import org.jetbrains.jewel.foundation.InternalJewelApi
|
||||
@Internal
|
||||
@InternalJewelApi
|
||||
public object ComposeSemanticsTreeUtils {
|
||||
public fun ComposePanel.findFocusedComponent(): SemanticsNode? {
|
||||
return semanticsOwners.firstNotNullOfOrNull { o ->
|
||||
o.getAllSemanticsNodes(mergingEnabled = true).firstOrNull {
|
||||
it.config.getOrNull(SemanticsProperties.Focused) == true
|
||||
/**
|
||||
* Returns the currently focused semantics node from this [ComposePanel], or `null` if there is none.
|
||||
*
|
||||
* This lookup fail-closes when Compose reports a transiently inconsistent semantics tree during mutation.
|
||||
*/
|
||||
public fun ComposePanel.findFocusedComponent(): SemanticsNode? = findFocusedComponent { semanticsOwners }
|
||||
|
||||
/**
|
||||
* Returns the currently focused semantics node from [semanticsOwners].
|
||||
*
|
||||
* The Compose semantics tree can be transiently inconsistent while it is being mutated, so this lookup fail-closes
|
||||
* on [NullPointerException] and lets callers behave as if no focused Compose component is available.
|
||||
*/
|
||||
internal fun findFocusedComponent(semanticsOwners: () -> Iterable<SemanticsOwner>): SemanticsNode? =
|
||||
try {
|
||||
semanticsOwners().firstNotNullOfOrNull { o ->
|
||||
o.getAllSemanticsNodes(mergingEnabled = true).firstOrNull {
|
||||
it.config.getOrNull(SemanticsProperties.Focused) == true
|
||||
}
|
||||
}
|
||||
} catch (_: NullPointerException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
public fun SemanticsNode.isEditableTextField(): Boolean {
|
||||
// Check if the node has editable text or supports setting text
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ public class ComposePasteProvider : PasteProvider {
|
||||
|
||||
override fun isPasteEnabled(dataContext: DataContext): Boolean = isPastePossible(dataContext)
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
// This needs to run on the EDT, or we risk synchronization issues like JEWEL-1359
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
|
||||
|
||||
private fun getPasteAction(dataContext: DataContext): AccessibilityAction<() -> Boolean>? {
|
||||
val contextComponent = dataContext.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT)
|
||||
|
||||
Reference in New Issue
Block a user