From 7e35a5675ffc523c3d548c754cd441dd7d5369b1 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Fri, 14 Nov 2025 14:57:12 +0100 Subject: [PATCH] IJPL-218636 [psi] send all corresponding events on file move GitOrigin-RevId: 745ad1d400aa37f763db6fbcf3fb41ece68bed4f --- .../psi/impl/file/impl/FileManagerImpl.java | 8 +-- .../psi/impl/file/impl/PsiVFSListener.kt | 63 ++++++++++++------- .../impl/file/impl/MultiversePsiEventTest.kt | 46 ++++++++++++++ 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/platform/core-impl/src/com/intellij/psi/impl/file/impl/FileManagerImpl.java b/platform/core-impl/src/com/intellij/psi/impl/file/impl/FileManagerImpl.java index 91ab65bfa6f6..72eafc2c1ed5 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/file/impl/FileManagerImpl.java +++ b/platform/core-impl/src/com/intellij/psi/impl/file/impl/FileManagerImpl.java @@ -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 filesHavingRelevantViewProviders = new HashSet<>(); Map irrelevantViewProviders = new LinkedHashMap<>(); 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 e893245288e8..45dec22eb7f5 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 @@ -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) { - val allOldElements = ArrayList(events.size) + val allOldElements = ArrayList>(events.size) val allOldParentDirs = ArrayList(events.size) val allNewParentDirs = ArrayList(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 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 2fb168bfb88c..cc0bc8ec19ba 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 @@ -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,