IJPL-218636 [psi] send all corresponding events on file move

GitOrigin-RevId: 745ad1d400aa37f763db6fbcf3fb41ece68bed4f
This commit is contained in:
Max Medvedev
2025-11-18 14:32:05 +00:00
committed by intellij-monorepo-bot
parent bf54663a43
commit 7e35a5675f
3 changed files with 89 additions and 28 deletions
@@ -677,13 +677,13 @@ public final class FileManagerImpl implements FileManagerEx {
myVFileToViewProviderMap.clear();
}
// When some file is moved to another directory, its contexts might change
// So, view providers associated with irrelevant contexts should be processed
// When some file is moved to another directory, its contexts might change.
// So, view providers associated with irrelevant contexts should be processed.
// The best way to deal with such view providers is just to invalidate them.
// But PSI clients are not usually ready for that.
// So we try to preserve one of them, clearing the context of this lucky view provider and setting its context to `any`.
// So we try to preserve one of the view providers, clearing the context of this lucky view provider and setting its context to `any`.
// It is possible to do that if there are no other relevant view providers for this file left.
// Also, if there's a view provider with `any` context, there can be no other view providers for this file, so we can silently accept them here.
// Also, if there's a view provider with `any` context, there can be no other view providers for this file, so we don't need to do anything for this file.
Set<VirtualFile> filesHavingRelevantViewProviders = new HashSet<>();
Map<VirtualFile, Entry> irrelevantViewProviders = new LinkedHashMap<>();
@@ -1,7 +1,6 @@
// 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.codeInsight.multiverse.anyContext
import com.intellij.ide.PsiCopyPasteManager
import com.intellij.ide.impl.ProjectUtilCore
import com.intellij.ide.plugins.DynamicPluginListener
@@ -491,7 +490,7 @@ private class PsiVFSListener(private val project: Project) {
// optimization: call fileManager.removeInvalidFilesAndDirs() once for a group of move events, instead of once for each event
private fun filesMoved(events: List<VFileEvent>) {
val allOldElements = ArrayList<PsiElement?>(events.size)
val allOldElements = ArrayList<List<PsiElement>>(events.size)
val allOldParentDirs = ArrayList<PsiDirectory?>(events.size)
val allNewParentDirs = ArrayList<PsiDirectory?>(events.size)
@@ -503,12 +502,11 @@ private class PsiVFSListener(private val project: Project) {
var oldParentDir = fileManager.findDirectory(event.oldParent)
var newParentDir = fileManager.findDirectory(event.newParent)
var oldElement: PsiElement? = if (vFile.isDirectory) {
fileManager.getCachedDirectory(vFile)
var oldElements = if (vFile.isDirectory) {
listOfNotNull(fileManager.getCachedDirectory(vFile))
}
else {
// todo IJPL-339 implement proper event for multiple files
fileManager.getCachedPsiFileInner(vFile, anyContext())
fileManager.getCachedPsiFilesInner(vFile)
}
val oldProject = ProjectLocator.getInstance().guessProjectForFile(vFile)
if (oldProject != null && oldProject !== project) {
@@ -516,11 +514,11 @@ private class PsiVFSListener(private val project: Project) {
fileManager.removeFilesAndDirsRecursively(vFile)
// avoiding crashes in filePointer.getElement()
PsiCopyPasteManager.getInstance().fileMovedOutsideProject(vFile)
oldElement = null
oldElements = emptyList()
oldParentDir = null
newParentDir = null
}
allOldElements.add(oldElement)
allOldElements.add(oldElements)
allOldParentDirs.add(oldParentDir)
allNewParentDirs.add(newParentDir)
}
@@ -535,7 +533,7 @@ private class PsiVFSListener(private val project: Project) {
continue
}
val oldElement = allOldElements[i]
val oldElements = allOldElements[i]
var newElement: PsiElement?
var newViewProvider: FileViewProvider?
if (vFile.isDirectory) {
@@ -547,16 +545,16 @@ private class PsiVFSListener(private val project: Project) {
newElement = newViewProvider.getPsi(fileManager.findViewProvider(vFile).baseLanguage)
}
if (oldElement == null && newElement == null) {
if (oldElements.isEmpty() && newElement == null) {
continue
}
ApplicationManager.getApplication().runWriteAction(ExternalChangeActionUtil.externalChangeAction {
val treeEvent = PsiTreeChangeEventImpl(manager)
if (oldElement == null) {
if (oldElements.isEmpty()) {
if (newViewProvider != null) {
fileManager.setViewProvider(vFile, newViewProvider)
}
val treeEvent = PsiTreeChangeEventImpl(manager)
treeEvent.parent = newParentDir
treeEvent.child = newElement
manager.childAdded(treeEvent)
@@ -564,24 +562,41 @@ private class PsiVFSListener(private val project: Project) {
else {
if (newElement == null) {
clearViewProvider(vFile, "PSI moved")
treeEvent.parent = oldParentDir
treeEvent.child = oldElement
manager.childRemoved(treeEvent)
for (oldElement in oldElements) {
val treeEvent = PsiTreeChangeEventImpl(manager)
treeEvent.parent = oldParentDir
treeEvent.child = oldElement
manager.childRemoved(treeEvent)
}
}
else {
if (newElement is PsiDirectory ||
FileManagerImpl.areViewProvidersEquivalent(newViewProvider!!, (oldElement as PsiFile).viewProvider)) {
treeEvent.oldParent = oldParentDir
treeEvent.newParent = newParentDir
treeEvent.child = oldElement
manager.childMoved(treeEvent)
FileManagerImpl.areViewProvidersEquivalent(newViewProvider!!, (oldElements.first() as PsiFile).viewProvider)) {
for (oldElement in oldElements) {
val treeEvent = PsiTreeChangeEventImpl(manager)
treeEvent.oldParent = oldParentDir
treeEvent.newParent = newParentDir
treeEvent.child = oldElement
if (oldElement.isValid) { // fileManager.removeInvalidFilesAndDirs(true) must have already invalided all old elements that must die.
manager.childMoved(treeEvent)
}
else {
manager.childRemoved(treeEvent)
}
}
}
else {
fileManager.setViewProvider(vFile, newViewProvider)
val treeRemoveEvent = PsiTreeChangeEventImpl(manager)
treeRemoveEvent.parent = oldParentDir
treeRemoveEvent.child = oldElement
manager.childRemoved(treeRemoveEvent)
for (oldElement in oldElements) {
val treeRemoveEvent = PsiTreeChangeEventImpl(manager)
treeRemoveEvent.parent = oldParentDir
treeRemoveEvent.child = oldElement
manager.childRemoved(treeRemoveEvent)
}
val treeAddEvent = PsiTreeChangeEventImpl(manager)
treeAddEvent.parent = newParentDir
treeAddEvent.child = newElement
@@ -41,6 +41,7 @@ internal class MultiversePsiEventTest {
private val sourceRoot = sharedSourceRootFixture(module1, module2)
private val sourceRoot2 = sharedSourceRootFixture(module1, module2)
private val m1sourceRoot = module1.sourceRootFixture()
@Suppress("unused")
private val registerFakeLang = testFixture {
@@ -282,6 +283,51 @@ internal class MultiversePsiEventTest {
expectedEventNumber = 2
)
@Test
fun `test we receive 2 child moved events on moving a file with 2 psi files`() = doChangeTest(
listenerFactory = { counter ->
object : PsiTreeChangeAdapter() {
override fun childMoved(event: PsiTreeChangeEvent) {
counter.incrementAndGet()
}
}
},
updateBlock = { file ->
file.move(this, sourceRoot2.get().virtualFile)
},
expectedEventNumber = 2
)
@Test
fun `test we receive 1 child moved event (and 1 child removed event) on moving a file with 2 psi files to a directory where only 1 context exists`() = doChangeTest(
listenerFactory = { counter ->
object : PsiTreeChangeAdapter() {
override fun childMoved(event: PsiTreeChangeEvent) {
counter.incrementAndGet()
}
}
},
updateBlock = { file ->
file.move(this, m1sourceRoot.get().virtualFile)
},
expectedEventNumber = 1
)
@Test
fun `test we receive (1 child moved event and) 1 child removed event on moving a file with 2 psi files to a directory where only 1 context exists`() = doChangeTest(
listenerFactory = { counter ->
object : PsiTreeChangeAdapter() {
override fun childRemoved(event: PsiTreeChangeEvent) {
counter.incrementAndGet()
}
}
},
updateBlock = { file ->
file.move(this, m1sourceRoot.get().virtualFile)
},
expectedEventNumber = 1
)
private fun doChangeTest(
listenerFactory: (AtomicInteger) -> PsiTreeChangeListener,
updateBlock: (file: VirtualFile) -> Unit,