IJPL-218636 [psi] send property-change events on write access update

GitOrigin-RevId: 7847dace0cd31e31a4b05af5171f3e925cb8f56b
This commit is contained in:
Max Medvedev
2025-11-18 14:32:05 +00:00
committed by intellij-monorepo-bot
parent 7a7ead7ff0
commit d3e3f2815c
2 changed files with 170 additions and 47 deletions
@@ -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
@@ -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)