KMT-1445 [compose resources] Trigger generation for files that are outside compose resources

- This commit doesn't handle the case for last deletion/moving of resource, that's a separate issue ( see KMT-2118)

Space-RevId: 1804a3fff87071471afeaa76b45c533ef845ef24
Space-RevId: 1b928a19b178e4ea8e02704821fb71f49d5a40db
Space-RevId: 4febdb40932f7ddf4e406a095d6471606e2a4b09
Space-RevId: 78a2d394764ed85ccfdb9285bf24d141dbcabae3

GitOrigin-RevId: 72dfecd5c0fb49f4aac03fa3e64bc9c227297396
This commit is contained in:
Filip Petrović
2026-02-05 12:58:44 +00:00
committed by intellij-monorepo-bot
parent 096d52cf89
commit 58000004ea
2 changed files with 61 additions and 13 deletions
@@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiTreeChangeAdapter
@@ -20,6 +21,7 @@ import com.intellij.psi.xml.XmlComment
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlText
import com.intellij.psi.xml.XmlToken
import com.intellij.psi.impl.PsiTreeChangeEventImpl
import org.jetbrains.kotlin.asJava.namedUnwrappedElement
import org.jetbrains.kotlin.idea.KotlinLanguage
@@ -30,7 +32,10 @@ private val PsiTreeChangeEvent.isIgnorable: Boolean
get() {
if (file?.language == KotlinLanguage.INSTANCE) return true
val parentName = getParentName() ?: return true
if (!parentName.isValidInnerComposeResourcesDirName) return true
val oldParentValidComposeResourceDir = oldParent?.namedUnwrappedElement?.name?.isValidInnerComposeResourcesDirName ?: false
if (!parentName.isValidInnerComposeResourcesDirName && !oldParentValidComposeResourceDir) return true
// We can ignore edits in whitespace, XML error nodes, and modification in comments.
// (Note that editing text in an attribute value, including whitespace characters,
@@ -53,6 +58,9 @@ private val PsiTreeChangeEvent.isIgnorable: Boolean
class ComposeResourcesPsiChangesListener(private val project: Project) : PsiTreeChangeAdapter() {
private var ignoreChildrenChanged = false
private val composeResourcesGenerationService by lazy { project.service<ComposeResourcesGenerationService>() }
/** propagating file rename events */
override fun propertyChanged(event: PsiTreeChangeEvent) {
ignoreChildrenChanged = true
@@ -138,17 +146,29 @@ class ComposeResourcesPsiChangesListener(private val project: Project) : PsiTree
private fun notice(event: PsiTreeChangeEvent) {
if (project.isDisposed) return
if (event.isIgnorable) return
val virtualFile = event.getVirtualFile() ?: return
val composeResourcesGenerationService = project.service<ComposeResourcesGenerationService>()
val path = virtualFile.toNioPathOrNull() ?: return
val composeResourcesDir = project.findComposeResourcesDirFor(path) ?: return
composeResourcesGenerationService.tryEmit(composeResourcesDir)
}
private fun PsiTreeChangeEvent.getVirtualFile(): VirtualFile? = file?.virtualFile /* for string values */
?: (this.parent as? PsiDirectory)?.virtualFile /* for files */
?: (this.child as? PsiFile)?.virtualFile /* CHILD_MOVED case */
private fun PsiTreeChangeEvent.getVirtualFile(): VirtualFile? {
val code = (this as? PsiTreeChangeEventImpl)?.code ?: return null
return when (code) {
PsiTreeChangeEventImpl.PsiEventType.CHILD_MOVED -> {
oldParent.getComposeResourcesVirtualFile() ?: newParent.getComposeResourcesVirtualFile()
}
else -> {
file?.virtualFile /* for string values */ ?: (parent as? PsiDirectory)?.virtualFile /* for files */
}
}
}
private fun PsiElement.getComposeResourcesVirtualFile(): VirtualFile? = (this as? PsiDirectory)
?.takeIf { it.name.isValidInnerComposeResourcesDirName }
?.virtualFile
}
private fun PsiTreeChangeEvent.getParentName(): String? = file?.parent?.name /* for string values */
@@ -7,6 +7,7 @@ import com.intellij.compose.ide.plugin.resources.psi.ComposeResourcesPsiChangesL
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.service
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.impl.PsiManagerEx
import com.intellij.psi.impl.file.impl.FileManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,23 +52,50 @@ class ComposeResourcesPsiChangesTest : ComposeResourcesTestCase() {
assertEquals(10, values.size)
}
private data class MovingContext(
val root: PsiDirectory,
val composeResourcesDir: VirtualFile,
val composeResourcesDrawableDir: VirtualFile,
)
@TargetVersions(TARGET_GRADLE_VERSION)
@Test
@TestMetadata("ComposeResources")
fun `test moving resource files from outside composeResources inner directories`() = doTest { files, fileManager, values ->
val composeResourcesDrawableDir = files.find { it.name.endsWith("compose-multiplatform.xml") }!!.parent
val composeResourcesDir = composeResourcesDrawableDir.parent
val root = runReadAction { fileManager.findDirectory(composeResourcesDir.parent)!! }
val file = runWriteAction { root.virtualFile.createChildData(root, "root.png") }
fun `test moving resource files from outside composeResources inner directories`() = doMovingTest { movingContext, values ->
val file = runWriteAction { movingContext.root.virtualFile.createChildData(movingContext.root, "root.png") }
assertEquals(0, values.size) // outside composeResources dirs, no change
runWriteAction { file.move(file, composeResourcesDrawableDir) }
runWriteAction { file.move(file, movingContext.composeResourcesDrawableDir) }
assertEquals(1, values.size)
runWriteAction { file.move(file, composeResourcesDir.parent) }
assertEquals(1, values.size) // outside composeResources dirs, no change
runWriteAction { file.move(file, movingContext.composeResourcesDir.parent) }
assertEquals(2, values.size)
}
@TargetVersions(TARGET_GRADLE_VERSION)
@Test
@TestMetadata("ComposeResources")
fun `test moving resource files from composeResources to outer directory`() = doMovingTest { movingContext, values ->
val file = runWriteAction { movingContext.composeResourcesDrawableDir.createChildData(movingContext.composeResourcesDrawableDir, "test.png") }
assertEquals(0, values.size)
runWriteAction { file.move(file, movingContext.root.virtualFile) }
assertEquals(1, values.size)
runWriteAction { file.move(file, movingContext.composeResourcesDrawableDir) }
assertEquals(2, values.size)
}
private fun doMovingTest(body: TestScope.(MovingContext, List<ComposeResourcesDir>) -> Unit) =
doTest { files, fileManager, values ->
val composeResourcesDrawableDir = files.find { it.name.endsWith("compose-multiplatform.xml") }!!.parent
val composeResourcesDir = composeResourcesDrawableDir.parent
val root = runReadAction { fileManager.findDirectory(composeResourcesDir.parent)!! }
val context = MovingContext(root, composeResourcesDir, composeResourcesDrawableDir)
body(context, values)
}
private fun doTest(body: suspend TestScope.(List<VirtualFile>, FileManager, List<ComposeResourcesDir>) -> Unit) = runTest {
val files = importProjectFromTestData()
val psiManager = PsiManagerEx.getInstanceEx(myProject)