diff --git a/platform/lang-impl/src/com/intellij/psi/impl/file/impl/PsiVFSListener.kt b/platform/lang-impl/src/com/intellij/psi/impl/file/impl/PsiVFSListener.kt index 84a67a68f2e9..40c87810c9b7 100644 --- a/platform/lang-impl/src/com/intellij/psi/impl/file/impl/PsiVFSListener.kt +++ b/platform/lang-impl/src/com/intellij/psi/impl/file/impl/PsiVFSListener.kt @@ -247,15 +247,19 @@ private class PsiVFSListener(private val project: Project) { } } else if (propertyName == VirtualFile.PROP_WRITABLE) { - // todo IJPL-339 implement proper event for multiple files - val psiFile = fileManager.getCachedPsiFileInner(vFile, anyContext()) ?: return@externalChangeAction - val treeEvent = PsiTreeChangeEventImpl(manager) - treeEvent.parent = parentDir - treeEvent.element = psiFile - treeEvent.propertyName = PsiTreeChangeEvent.PROP_WRITABLE - treeEvent.oldValue = event.oldValue - treeEvent.newValue = event.newValue - manager.beforePropertyChange(treeEvent) + val psiFiles = fileManager.getCachedPsiFilesInner(vFile).ifEmpty { + return@externalChangeAction + } + + for (psiFile in psiFiles) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.element = psiFile + treeEvent.propertyName = PsiTreeChangeEvent.PROP_WRITABLE + treeEvent.oldValue = event.oldValue + treeEvent.newValue = event.newValue + manager.beforePropertyChange(treeEvent) + } } }) } @@ -271,17 +275,17 @@ private class PsiVFSListener(private val project: Project) { val propertyName = event.propertyName val vFile = event.file - val oldFileViewProvider = fileManager.findCachedViewProvider(vFile) - if (oldFileViewProvider != null && FileContentUtilCore.FORCE_RELOAD_REQUESTOR == event.requestor) { + val oldFileViewProviders = fileManager.findCachedViewProviders(vFile) + if (oldFileViewProviders.isNotEmpty() && FileContentUtilCore.FORCE_RELOAD_REQUESTOR == event.requestor) { // there is no need to rebuild if there were no PSI in the first place fileManager.forceReload(vFile) return } - val oldPsiFile = fileManager.getCachedPsiFile(vFile) + val oldPsiFiles = fileManager.getCachedPsiFiles(vFile) val parentDir = run { val parent = vFile.parent - if (oldPsiFile != null && parent != null) + if (oldPsiFiles.isNotEmpty() && parent != null) fileManager.findDirectory(parent) else getCachedDirectory(parent) @@ -302,8 +306,6 @@ private class PsiVFSListener(private val project: Project) { val fileTypeManager = FileTypeManager.getInstance() ApplicationManager.getApplication().runWriteAction(ExternalChangeActionUtil.externalChangeAction { - val treeEvent = PsiTreeChangeEventImpl(manager) - treeEvent.parent = parentDir when (propertyName) { VirtualFile.PROP_NAME -> { if (vFile.isDirectory) { @@ -312,10 +314,14 @@ private class PsiVFSListener(private val project: Project) { if (fileTypeManager.isFileIgnored(vFile)) { fileManager.removeFilesAndDirsRecursively(vFile) + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir treeEvent.child = psiDir manager.childRemoved(treeEvent) } else { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir treeEvent.element = psiDir treeEvent.propertyName = PsiTreeChangeEvent.PROP_DIRECTORY_NAME treeEvent.oldValue = event.oldValue @@ -326,41 +332,75 @@ private class PsiVFSListener(private val project: Project) { else { val psiDir1 = fileManager.findDirectory(vFile) if (psiDir1 != null) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir treeEvent.child = psiDir1 manager.childAdded(treeEvent) } } } else { - val fileViewProvider = fileManager.createFileViewProvider(vFile, true) - val newPsiFile = fileViewProvider.getPsi(fileViewProvider.baseLanguage) - if (oldPsiFile != null) { + val newFileViewProvider = fileManager.createFileViewProvider(vFile, true) + val newPsiFile = newFileViewProvider.getPsi(newFileViewProvider.baseLanguage) + if (oldPsiFiles.isNotEmpty()) { if (newPsiFile == null) { clearViewProvider(vFile, "PSI renamed") - treeEvent.child = oldPsiFile - manager.childRemoved(treeEvent) - } - else if (!FileManagerImpl.areViewProvidersEquivalent(fileViewProvider, oldFileViewProvider!!)) { - fileManager.setViewProvider(vFile, fileViewProvider) - - treeEvent.oldChild = oldPsiFile - treeEvent.newChild = newPsiFile - manager.childReplaced(treeEvent) + for (oldPsiFile in oldPsiFiles) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.child = oldPsiFile + manager.childRemoved(treeEvent) + } } else { - FileManagerImpl.clearPsiCaches(oldFileViewProvider) + val firstOldViewProvider = oldFileViewProviders.first() // todo IJPL-339 do we want to select a preferred view provider instead of the first one??? + if (!FileManagerImpl.areViewProvidersEquivalent(newFileViewProvider, firstOldViewProvider)) { + // the file has changed its view provider factory + // we need to delete all old providers and create one new provider. - treeEvent.element = oldPsiFile - treeEvent.propertyName = PsiTreeChangeEvent.PROP_FILE_NAME - treeEvent.oldValue = event.oldValue - treeEvent.newValue = event.newValue - manager.propertyChanged(treeEvent) + fileManager.setViewProvider(vFile, newFileViewProvider) + + run { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.oldChild = firstOldViewProvider.getPsi(firstOldViewProvider.baseLanguage) + treeEvent.newChild = newPsiFile + manager.childReplaced(treeEvent) + } + + for (oldFileViewProvider in oldFileViewProviders.drop(1)) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.oldChild = oldFileViewProvider.getPsi(oldFileViewProvider.baseLanguage) + manager.childRemoved(treeEvent) + } + } + else { + // the file keeps the same view provider factory + // let's reuse all old view providers + + for (oldFileViewProvider in oldFileViewProviders) { + FileManagerImpl.clearPsiCaches(oldFileViewProvider) + } + + for (oldPsiFile in oldPsiFiles) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.element = oldPsiFile + treeEvent.propertyName = PsiTreeChangeEvent.PROP_FILE_NAME + treeEvent.oldValue = event.oldValue + treeEvent.newValue = event.newValue + manager.propertyChanged(treeEvent) + } + } } } else if (newPsiFile != null) { - fileManager.setViewProvider(vFile, fileViewProvider) + fileManager.setViewProvider(vFile, newFileViewProvider) if (parentDir != null) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir treeEvent.child = newPsiFile manager.childAdded(treeEvent) } @@ -368,22 +408,28 @@ private class PsiVFSListener(private val project: Project) { } } VirtualFile.PROP_WRITABLE -> { - if (oldPsiFile == null) { + if (oldPsiFiles.isEmpty()) { return@externalChangeAction } - treeEvent.element = oldPsiFile - treeEvent.propertyName = PsiTreeChangeEvent.PROP_WRITABLE - treeEvent.oldValue = event.oldValue - treeEvent.newValue = event.newValue - manager.propertyChanged(treeEvent) + for (oldPsiFile in oldPsiFiles) { + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.element = oldPsiFile + treeEvent.propertyName = PsiTreeChangeEvent.PROP_WRITABLE + treeEvent.oldValue = event.oldValue + treeEvent.newValue = event.newValue + manager.propertyChanged(treeEvent) + } } VirtualFile.PROP_ENCODING -> { - if (oldPsiFile == null) { + if (oldPsiFiles.isEmpty()) { return@externalChangeAction } - treeEvent.element = oldPsiFile + val treeEvent = PsiTreeChangeEventImpl(manager) + treeEvent.parent = parentDir + treeEvent.element = oldPsiFiles.first() // todo IJPL-339 update me treeEvent.propertyName = VirtualFile.PROP_ENCODING treeEvent.oldValue = event.oldValue treeEvent.newValue = event.newValue diff --git a/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/MultiversePsiEventTest.kt b/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/MultiversePsiEventTest.kt index 9e343c92fee6..a8b0dc659736 100644 --- a/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/MultiversePsiEventTest.kt +++ b/platform/lang-impl/testSources/com/intellij/psi/impl/file/impl/MultiversePsiEventTest.kt @@ -3,9 +3,12 @@ package com.intellij.psi.impl.file.impl import com.intellij.codeInsight.multiverse.CodeInsightContext import com.intellij.codeInsight.multiverse.ProjectModelContextBridge +import com.intellij.lang.fakeLang.registerFakeLanguage +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.readAction import com.intellij.openapi.application.writeAction import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled import com.intellij.psi.PsiFile @@ -16,10 +19,9 @@ import com.intellij.psi.impl.PsiManagerEx import com.intellij.testFramework.IndexingTestUtil import com.intellij.testFramework.common.timeoutRunBlocking import com.intellij.testFramework.junit5.TestApplication -import com.intellij.testFramework.junit5.fixture.disposableFixture -import com.intellij.testFramework.junit5.fixture.moduleFixture -import com.intellij.testFramework.junit5.fixture.projectFixture -import com.intellij.testFramework.junit5.fixture.virtualFileFixture +import com.intellij.testFramework.junit5.fixture.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.lang.ref.Reference @@ -34,6 +36,19 @@ internal class MultiversePsiEventTest { private val module2 = projectFixture.moduleFixture("m2") private val sourceRoot = sharedSourceRootFixture(module1, module2) + + @Suppress("unused") + private val registerFakeLang = testFixture { + val newDisposable = Disposer.newDisposable() + withContext(Dispatchers.EDT) { + registerFakeLanguage(newDisposable) + } + initialized(Unit) { + withContext(Dispatchers.EDT) { + Disposer.dispose(newDisposable) + } + } + } } private val testDisposable by disposableFixture() @@ -108,10 +123,72 @@ internal class MultiversePsiEventTest { expectedEventNumber = 2 ) + @Test + fun `test we receive 1 delete event on renaming file to another file type with 2 psi files`() = doChangeTest( + listenerFactory = { counter -> + object : PsiTreeChangeAdapter() { + override fun childRemoved(event: PsiTreeChangeEvent) { + counter.incrementAndGet() + } + } + }, + updateBlock = { file -> file.rename(this, "A.fake") }, + expectedEventNumber = 1 + ) + + @Test + fun `test we receive 1 replaced event on renaming file to another file type with 2 psi files`() = doChangeTest( + listenerFactory = { counter -> + object : PsiTreeChangeAdapter() { + override fun childReplaced(event: PsiTreeChangeEvent) { + counter.incrementAndGet() + } + } + }, + updateBlock = { file -> + file.rename(this, "A.fake") + }, + expectedEventNumber = 1 + ) + + @Test + fun `test we receive 2 writeable event on changing writable status of a file with 2 psi files`() = doChangeTest( + listenerFactory = { counter -> + object : PsiTreeChangeAdapter() { + override fun propertyChanged(event: PsiTreeChangeEvent) { + if (event.propertyName == PsiTreeChangeEvent.PROP_WRITABLE) { + counter.incrementAndGet() + } + } + } + }, + updateBlock = { file -> + file.isWritable = false + }, + expectedEventNumber = 2 + ) + + @Test + fun `test we receive 2 before-writeable event on changing writable status of a file with 2 psi files`() = doChangeTest( + listenerFactory = { counter -> + object : PsiTreeChangeAdapter() { + override fun beforePropertyChange(event: PsiTreeChangeEvent) { + if (event.propertyName == PsiTreeChangeEvent.PROP_WRITABLE) { + counter.incrementAndGet() + } + } + } + }, + updateBlock = { file -> + file.isWritable = false + }, + expectedEventNumber = 2 + ) + private fun doChangeTest( listenerFactory: (AtomicInteger) -> PsiTreeChangeListener, updateBlock: (file: VirtualFile) -> Unit, - @Suppress("SameParameterValue") expectedEventNumber: Int + @Suppress("SameParameterValue") expectedEventNumber: Int, ) = runTest { val counter = AtomicInteger(0) val listener = listenerFactory(counter)