mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-196089 Subscribe PsiVFSModuleRootListener to WorkspaceModelChangeListener
New listener is enabled by default. Use Registry `psi.vfs.listener.over.wsm` to enable legacy behavior. Legacy behavior breaks ExternalAnnotationsTest.testExternalAnnotationsRootRemoved, because PsiVFSModuleRootListener will not receive changes in custom iml entities, therefore, does not invalidate PSI caches on changes to external annotations (see AnnotationUtil.NON_CODE_ANNOTATIONS_PROVIDER) GitOrigin-RevId: a75670f1e915a0decc13bfa9d5ec6b75be6b21ce
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8ae6643d50
commit
eeab79bb12
@@ -14,6 +14,7 @@ import com.intellij.openapi.components.serviceAsync
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.diagnostic.trace
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
|
||||
import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl
|
||||
import com.intellij.openapi.fileTypes.FileTypeEvent
|
||||
@@ -31,6 +32,9 @@ import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.openapi.vfs.isTooLarge
|
||||
import com.intellij.openapi.vfs.newvfs.BulkFileListener
|
||||
import com.intellij.openapi.vfs.newvfs.events.*
|
||||
import com.intellij.platform.backend.workspace.WorkspaceModelChangeListener
|
||||
import com.intellij.platform.workspace.storage.VersionedStorageChange
|
||||
import com.intellij.platform.workspace.storage.impl.VersionedStorageChangeInternal
|
||||
import com.intellij.project.stateStore
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.impl.DebugUtil
|
||||
@@ -578,13 +582,62 @@ class PsiVFSListener internal constructor(private val project: Project) {
|
||||
}
|
||||
}
|
||||
|
||||
internal class PsiVFSModuleRootListener(private val listenerProject: Project) : ModuleRootListener {
|
||||
/**
|
||||
* We use [WorkspaceModelChangeListener] in **addition** to [ModuleRootListener], because [ModuleRootListener] may generate events
|
||||
* not sourced by the workspace model (see Javadoc for [ModuleRootListener]).
|
||||
* If the same event should trigger both [WorkspaceModelChangeListener] event and [PsiVFSModuleRootListener], these listener invocations
|
||||
* will be nested and deduplicated inside [PsiVFSModuleRootListenerImpl], so eventually only one [PsiTreeChangeEvent] will be published.
|
||||
*
|
||||
* With this listener, we mostly want to invalidate psi caches when workspace model changes.
|
||||
*/
|
||||
// @Suppress: Don't use flow instead of [WorkspaceModelChangeListener]. We need to invalidate caches in the same WA as the event.
|
||||
@Suppress("UsagesOfObsoleteApi")
|
||||
internal class PsiWsmListener(listenerProject: Project) : WorkspaceModelChangeListener {
|
||||
private val service = listenerProject.service<PsiVFSModuleRootListenerImpl>()
|
||||
|
||||
init {
|
||||
if (!Registry.`is`("psi.vfs.listener.over.wsm", true)) {
|
||||
LOG.debug("PsiWsmListener is disabled by registry key")
|
||||
throw ExtensionNotApplicableException.create()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotEmptyChange(event: VersionedStorageChange): Boolean {
|
||||
return (event as? VersionedStorageChangeInternal)?.getAllChanges()?.firstOrNull() != null
|
||||
}
|
||||
|
||||
override fun beforeChanged(event: VersionedStorageChange) {
|
||||
if (isNotEmptyChange(event)) {
|
||||
service.beforeRootsChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun changed(event: VersionedStorageChange) {
|
||||
if (isNotEmptyChange(event)) {
|
||||
service.rootsChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class PsiVFSModuleRootListener(listenerProject: Project) : ModuleRootListener {
|
||||
private val service = listenerProject.service<PsiVFSModuleRootListenerImpl>()
|
||||
override fun beforeRootsChange(event: ModuleRootEvent) {
|
||||
service.beforeRootsChange(event.isCausedByFileTypesChange)
|
||||
}
|
||||
|
||||
override fun rootsChanged(event: ModuleRootEvent) {
|
||||
service.rootsChanged(event.isCausedByFileTypesChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
private class PsiVFSModuleRootListenerImpl(private val listenerProject: Project) {
|
||||
// accessed from within write action only
|
||||
private var depthCounter = 0
|
||||
|
||||
override fun beforeRootsChange(event: ModuleRootEvent) {
|
||||
fun beforeRootsChange(isCausedByFileTypesChange: Boolean) {
|
||||
LOG.trace { "beforeRootsChanged call" }
|
||||
if (event.isCausedByFileTypesChange) {
|
||||
if (isCausedByFileTypesChange) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -603,13 +656,13 @@ internal class PsiVFSModuleRootListener(private val listenerProject: Project) :
|
||||
})
|
||||
}
|
||||
|
||||
override fun rootsChanged(event: ModuleRootEvent) {
|
||||
fun rootsChanged(isCausedByFileTypesChange: Boolean) {
|
||||
LOG.trace { "rootsChanged call" }
|
||||
val psiManager = PsiManagerEx.getInstanceEx(listenerProject)
|
||||
val fileManager = psiManager.fileManager as FileManagerEx
|
||||
fileManager.dispatchPendingEvents()
|
||||
|
||||
if (event.isCausedByFileTypesChange) {
|
||||
if (isCausedByFileTypesChange) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+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 com.intellij.psi.impl.file.impl
|
||||
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.roots.ModuleRootEvent
|
||||
import com.intellij.openapi.roots.ModuleRootListener
|
||||
|
||||
class ModuleRootTestListener: ModuleRootListener {
|
||||
private val myBuffer: StringBuffer = StringBuffer()
|
||||
private val logger = thisLogger()
|
||||
|
||||
private fun logEvent(message: String){
|
||||
myBuffer.append(message)
|
||||
logger.debug(Throwable(message))
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
logger.debug("reset")
|
||||
myBuffer.setLength(0)
|
||||
}
|
||||
|
||||
val eventsString: String
|
||||
get() = myBuffer.toString()
|
||||
|
||||
override fun beforeRootsChange(event: ModuleRootEvent) {
|
||||
logEvent("beforeRootsChange\n")
|
||||
}
|
||||
|
||||
override fun rootsChanged(event: ModuleRootEvent) {
|
||||
logEvent("rootsChanged\n")
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// 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.psi.impl.file.impl
|
||||
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.psi.PsiTreeChangeEvent
|
||||
import com.intellij.psi.PsiTreeChangeListener
|
||||
|
||||
|
||||
internal class PsiEventsTestListener : PsiTreeChangeListener {
|
||||
private val myBuffer: StringBuffer = StringBuffer()
|
||||
private val logger = thisLogger()
|
||||
|
||||
fun reset() {
|
||||
logger.debug("reset")
|
||||
myBuffer.setLength(0)
|
||||
}
|
||||
|
||||
val eventsString: String
|
||||
get() = myBuffer.toString()
|
||||
|
||||
private fun logEvent(message: String){
|
||||
myBuffer.append(message)
|
||||
logger.debug(Throwable(message))
|
||||
}
|
||||
|
||||
override fun beforeChildAddition(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforeChildAddition\n")
|
||||
}
|
||||
|
||||
override fun beforeChildRemoval(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforeChildRemoval\n")
|
||||
}
|
||||
|
||||
override fun beforeChildReplacement(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforeChildReplacement\n")
|
||||
}
|
||||
|
||||
override fun beforeChildMovement(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforeChildMovement\n")
|
||||
}
|
||||
|
||||
override fun beforeChildrenChange(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforeChildrenChange\n")
|
||||
}
|
||||
|
||||
override fun beforePropertyChange(event: PsiTreeChangeEvent) {
|
||||
logEvent("beforePropertyChange ${event.propertyName}\n")
|
||||
}
|
||||
|
||||
override fun childAdded(event: PsiTreeChangeEvent) {
|
||||
logEvent("childAdded\n")
|
||||
}
|
||||
|
||||
override fun childRemoved(event: PsiTreeChangeEvent) {
|
||||
logEvent("childRemoved\n")
|
||||
}
|
||||
|
||||
override fun childReplaced(event: PsiTreeChangeEvent) {
|
||||
logEvent("childReplaced\n")
|
||||
}
|
||||
|
||||
override fun childrenChanged(event: PsiTreeChangeEvent) {
|
||||
logEvent("childrenChanged\n")
|
||||
}
|
||||
|
||||
override fun childMoved(event: PsiTreeChangeEvent) {
|
||||
logEvent("childMoved\n")
|
||||
}
|
||||
|
||||
override fun propertyChanged(event: PsiTreeChangeEvent) {
|
||||
logEvent("propertyChanged ${event.propertyName}\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.psi.impl.file.impl
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.project.RootsChangeRescanningInfo
|
||||
import com.intellij.openapi.roots.ModuleRootListener
|
||||
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
|
||||
import com.intellij.openapi.util.EmptyRunnable
|
||||
import com.intellij.platform.backend.workspace.WorkspaceModel
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleCustomImlDataEntity
|
||||
import com.intellij.platform.workspace.jps.entities.ModuleEntity
|
||||
import com.intellij.platform.workspace.storage.EntitySource
|
||||
import com.intellij.psi.impl.PsiManagerEx
|
||||
import com.intellij.testFramework.IndexingTestUtil
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
private object TestEntitySource : EntitySource
|
||||
|
||||
@TestApplication
|
||||
class PsiVFSListenerTest {
|
||||
|
||||
val projectFixture = projectFixture(openAfterCreation = true)
|
||||
|
||||
@Test
|
||||
fun testWsmEventGeneratesExactlyOnePsiEvent(@TestDisposable testDisposable: Disposable) = runBlocking {
|
||||
val project = projectFixture.get()
|
||||
IndexingTestUtil.suspendUntilIndexesAreReady(project)
|
||||
|
||||
val psiListener = PsiEventsTestListener()
|
||||
val rootListener = ModuleRootTestListener()
|
||||
|
||||
project.messageBus.connect(testDisposable).subscribe(ModuleRootListener.TOPIC, rootListener)
|
||||
PsiManagerEx.getInstanceEx(project).addPsiTreeChangeListener(psiListener, testDisposable)
|
||||
|
||||
val wsm = WorkspaceModel.getInstance(project)
|
||||
val module = ModuleEntity("Test", emptyList(), TestEntitySource)
|
||||
wsm.update("add module in test") {
|
||||
it.addEntity(module)
|
||||
}
|
||||
|
||||
assertEquals("beforeRootsChange\nrootsChanged\n", rootListener.eventsString)
|
||||
assertEquals("beforePropertyChange roots\npropertyChanged roots\n", psiListener.eventsString)
|
||||
|
||||
IndexingTestUtil.suspendUntilIndexesAreReady(project) // index just added module (dumb mode may generate psi events)
|
||||
|
||||
val imlData = ModuleCustomImlDataEntity(emptyMap(), TestEntitySource) {
|
||||
this.module = module
|
||||
}
|
||||
rootListener.reset()
|
||||
psiListener.reset()
|
||||
|
||||
// intention: check PsiVFSListener generates PSI events correctly when WSM event is triggered, but RootsChangeEvent is not
|
||||
wsm.update("add custom iml data in test") {
|
||||
it.addEntity(imlData)
|
||||
}
|
||||
|
||||
assertEquals("", rootListener.eventsString) // check that RootsChangeEvent is indeed not triggered
|
||||
assertEquals("beforePropertyChange roots\npropertyChanged roots\n", psiListener.eventsString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMakeRootsChangeTotalRescanGeneratesExactlyOnePsiEvent(@TestDisposable testDisposable: Disposable) {
|
||||
testMakeRootsChangeGeneratesExactlyOnePsiEvent(testDisposable, RootsChangeRescanningInfo.TOTAL_RESCAN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMakeRootsChangeNoRescanGeneratesExactlyOnePsiEvent(@TestDisposable testDisposable: Disposable) {
|
||||
testMakeRootsChangeGeneratesExactlyOnePsiEvent(testDisposable, RootsChangeRescanningInfo.NO_RESCAN_NEEDED)
|
||||
}
|
||||
|
||||
private fun testMakeRootsChangeGeneratesExactlyOnePsiEvent(testDisposable: Disposable, changes: RootsChangeRescanningInfo): Unit = runBlocking {
|
||||
val project = projectFixture.get()
|
||||
IndexingTestUtil.suspendUntilIndexesAreReady(project)
|
||||
|
||||
val psiListener = PsiEventsTestListener()
|
||||
val rootListener = ModuleRootTestListener()
|
||||
|
||||
project.messageBus.connect(testDisposable).subscribe(ModuleRootListener.TOPIC, rootListener)
|
||||
PsiManagerEx.getInstanceEx(project).addPsiTreeChangeListener(psiListener, testDisposable)
|
||||
|
||||
writeAction {
|
||||
ProjectRootManagerEx.getInstanceEx(project).makeRootsChange(EmptyRunnable.getInstance(), changes)
|
||||
}
|
||||
|
||||
assertEquals(rootListener.eventsString, "beforeRootsChange\nrootsChanged\n")
|
||||
assertEquals(psiListener.eventsString, "beforePropertyChange roots\npropertyChanged roots\n")
|
||||
}
|
||||
}
|
||||
@@ -127,12 +127,16 @@
|
||||
<registryKey key="unknown.sdk.auto" defaultValue="true" description="Checks and resolves unknown SDKs automatically on start"/>
|
||||
<registryKey key="unknown.sdk.modal.jps" defaultValue="true" description="Run unknown JDK test before JPS build is started"/>
|
||||
<registryKey key="unknown.sdk.show.editor.actions" defaultValue="true" description="Show editor suggestions to fix missing SKDs"/>
|
||||
<registryKey key="psi.vfs.listener.over.wsm" defaultValue="true" restartRequired="true"
|
||||
description="Use WSM listeners to invalidate PSI caches (new) in addition to legacy ModuleRootListener"/>
|
||||
|
||||
<editorNotificationProvider implementation="com.intellij.openapi.projectRoots.impl.UnknownSdkEditorNotificationsProvider"/>
|
||||
</extensions>
|
||||
<projectListeners>
|
||||
<listener class="com.intellij.psi.impl.file.impl.PsiVFSModuleRootListener"
|
||||
topic="com.intellij.openapi.roots.ModuleRootListener"/>
|
||||
<listener class="com.intellij.psi.impl.file.impl.PsiWsmListener"
|
||||
topic="com.intellij.platform.backend.workspace.WorkspaceModelChangeListener"/>
|
||||
<listener class="com.intellij.workspaceModel.ide.impl.legacyBridge.facet.FacetEntityChangeListener$WorkspaceModelListener"
|
||||
topic="com.intellij.platform.backend.workspace.WorkspaceModelChangeListener"/>
|
||||
<listener class="com.intellij.workspaceModel.ide.impl.legacyBridge.module.ModuleBridgeCleaner"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.roots;
|
||||
|
||||
import com.intellij.openapi.project.RootsChangeRescanningInfo;
|
||||
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
|
||||
import com.intellij.util.messages.Topic;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -10,8 +12,17 @@ import java.util.EventListener;
|
||||
/**
|
||||
* Project root changes.
|
||||
* <p>
|
||||
* Instead of events with {{@link ModuleRootEvent#isCausedByWorkspaceModelChangesOnly()}} one may use
|
||||
* {@link com.intellij.platform.backend.workspace.WorkspaceModelChangeListener} and get more fine-grained incremental events.
|
||||
* Instead of events with {@link ModuleRootEvent#isCausedByWorkspaceModelChangesOnly()} one may use
|
||||
* {@link com.intellij.platform.backend.workspace.WorkspaceModelChangeListener} and get more fine-grained incremental events.
|
||||
* <p>
|
||||
* {@link ModuleRootEvent#isCausedByWorkspaceModelChangesOnly()} is always {@code false} in the {@linkplain #beforeRootsChange(ModuleRootEvent)}.
|
||||
* This is because {@linkplain ModuleRootListener} will de-duplicate nested {@linkplain ModuleRootEvent} events, and at the moment of the
|
||||
* {@linkplain #beforeRootsChange(ModuleRootEvent)} invocation we don't know if all the events (including de-duplicated) are from WSM or not.
|
||||
* <p>
|
||||
* {@linkplain com.intellij.platform.backend.workspace.WorkspaceModelChangeListener} is not a direct replacement for {@linkplain ModuleRootListener},
|
||||
* because {@linkplain ModuleRootListener} may generate events that are not related to the workspace model. For example, there will be an
|
||||
* event on a filetype change, roots validity change (VirtualFileUrl validity change like when a jar file is downloaded), and
|
||||
* out of thin air events caused by {@link ProjectRootManagerEx#makeRootsChange(Runnable, RootsChangeRescanningInfo)}
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
public interface ModuleRootListener extends EventListener {
|
||||
|
||||
Reference in New Issue
Block a user