diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructure.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructure.kt index f194d1a79f4..c74ed471c86 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructure.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructure.kt @@ -13,7 +13,9 @@ import org.jetbrains.kotlin.idea.fir.low.level.api.file.builder.ModuleFileCache import org.jetbrains.kotlin.idea.fir.low.level.api.lazy.resolve.FirLazyDeclarationResolver import org.jetbrains.kotlin.idea.fir.low.level.api.providers.firIdeProvider import org.jetbrains.kotlin.idea.fir.low.level.api.util.findSourceNonLocalFirDeclaration +import org.jetbrains.kotlin.idea.search.getKotlinFqName import org.jetbrains.kotlin.idea.util.getElementTextInContext +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType @@ -57,7 +59,7 @@ internal class FileStructure( ktFile.forEachDescendantOfType( canGoInside = { psi -> psi !is KtFunction && psi !is KtValVarKeywordOwner } ) { declaration -> - if (declaration.isStructureElementContainer()) { + if (FileStructureUtil.isStructureElementContainer(declaration)) { add(declaration) } } @@ -104,12 +106,3 @@ internal class FileStructure( else -> error("Invalid container $container") } } - -private fun KtDeclaration.isStructureElementContainer(): Boolean { - if (this !is KtClassOrObject && this !is KtDeclarationWithBody && this !is KtProperty && this !is KtTypeAlias) return false - if (this is KtEnumEntry) return false - if (containingClassOrObject is KtEnumEntry) return false - return !KtPsiUtil.isLocal(this) -} - - diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructureUtil.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructureUtil.kt new file mode 100644 index 00000000000..d3f7b1bd765 --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/file/structure/FileStructureUtil.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.fir.low.level.api.file.structure + +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject + +internal object FileStructureUtil { + fun isStructureElementContainer(ktDeclaration: KtDeclaration): Boolean = when { + ktDeclaration !is KtClassOrObject && ktDeclaration !is KtDeclarationWithBody && ktDeclaration !is KtProperty && ktDeclaration !is KtTypeAlias -> false + ktDeclaration is KtEnumEntry -> false + ktDeclaration.containingClassOrObject is KtEnumEntry -> false + else -> !KtPsiUtil.isLocal(ktDeclaration) + } +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionProviderStorage.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionProviderStorage.kt index 6c84e5ceb3d..1c11d3e546c 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionProviderStorage.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/sessions/FirIdeSessionProviderStorage.kt @@ -104,7 +104,7 @@ private class FirSessionWithModificationTracker( val firSession: FirIdeSourcesSession, ) { private val modificationTracker = firSession.project.service() - .createModuleOutOfBlockModificationTracker(firSession.moduleInfo.module) + .createModuleWithoutDependenciesOutOfBlockModificationTracker(firSession.moduleInfo.module) private val timeStamp = modificationTracker.modificationCount diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt index 696fdfe14c4..3a39f7f7250 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt @@ -20,40 +20,42 @@ import com.intellij.pom.tree.TreeAspect import com.intellij.pom.tree.events.TreeChangeEvent import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingInBodyDeclarationWith -import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingOrThisDeclaration import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.FileElementFactory import org.jetbrains.kotlin.idea.util.module -import org.jetbrains.kotlin.psi.KtNamedFunction -import org.jetbrains.kotlin.psi.psiUtil.isAncestor -import java.util.* internal class KotlinFirModificationTrackerService(project: Project) : Disposable { init { - val model = PomManager.getModel(project) - model.addModelListener(Listener()) + PomManager.getModel(project).addModelListener(Listener()) - val connection = project.messageBus.connect(this) - connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { - override fun rootsChanged(event: ModuleRootEvent) { - projectGlobalOutOfBlockInKotlinFilesModificationCount++ - - // todo increase modificationCountForModule + project.messageBus.connect(this).subscribe( + ProjectTopics.PROJECT_ROOTS, + object : ModuleRootListener { + override fun rootsChanged(event: ModuleRootEvent) = increaseModificationCountForAllModules() } - }) + ) } - internal var projectGlobalOutOfBlockInKotlinFilesModificationCount = 0L + var projectGlobalOutOfBlockInKotlinFilesModificationCount = 0L private set - internal fun getOutOfBlockModificationCountForModules(module: Module): Long = - modificationCountForModule[module] ?: 0L + private val moduleModificationsState = ModuleModificationsState() + + fun getOutOfBlockModificationCountForModules(module: Module): Long = + moduleModificationsState.getModificationsCountForModule(module) - private val modificationCountForModule = WeakHashMap() private val treeAspect = TreeAspect.getInstance(project) override fun dispose() {} + private fun increaseModificationCountForAllModules() { + projectGlobalOutOfBlockInKotlinFilesModificationCount++ + moduleModificationsState.increaseModificationCountForAllModules() + } + private inner class Listener : PomModelListener { + override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean = + treeAspect == aspect + override fun modelChanged(event: PomModelEvent) { val changeSet = event.getChangeSet(treeAspect) as TreeChangeEvent? ?: return if (changeSet.rootElement.psi.language != KotlinLanguage.INSTANCE) return @@ -66,7 +68,7 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl isOutOfBlockChangeInAnyModule = isOutOfBlockChangeInAnyModule || isOutOfBlock if (isOutOfBlock) { element.psi.module?.let { module -> - modificationCountForModule.compute(module) { _, value -> (value ?: 0) + 1 } + moduleModificationsState.increaseModificationCountForModule(module) } } } @@ -84,8 +86,33 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl !FileElementFactory.isReanalyzableContainer(container) } } - - override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean = - treeAspect == aspect } +} + +private class ModuleModificationsState { + private val modificationCountForModule = hashMapOf() + private var state: Long = 0L + + fun getModificationsCountForModule(module: Module) = modificationCountForModule.compute(module) { _, modifications -> + when { + modifications == null -> ModuleModifications(0, state) + modifications.state == state -> modifications + else -> ModuleModifications(modificationsCount = modifications.modificationsCount + 1, state = state) + } + }!!.modificationsCount + + fun increaseModificationCountForAllModules() { + state++ + } + + fun increaseModificationCountForModule(module: Module) { + modificationCountForModule.compute(module) { _, modifications -> + when (modifications) { + null -> ModuleModifications(0, state) + else -> ModuleModifications(ModuleModifications(0, state).modificationsCount + 1, state) + } + } + } + + private data class ModuleModifications(val modificationsCount: Long, val state: Long) } \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt index c3e676801d8..0444cf44ff9 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt @@ -14,11 +14,16 @@ class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project fun createProjectWideOutOfBlockModificationTracker(): ModificationTracker = KotlinFirOutOfBlockModificationTracker(project) - fun createModuleOutOfBlockModificationTracker(module: Module): ModificationTracker = + fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker = KotlinFirOutOfBlockModuleModificationTracker(module) - } +fun Project.createProjectWideOutOfBlockModificationTracker() = + service().createProjectWideOutOfBlockModificationTracker() + +fun Module.createModuleWithoutDependenciesOutOfBlockModificationTracker() = + project.service().createModuleWithoutDependenciesOutOfBlockModificationTracker(this) + private class KotlinFirOutOfBlockModificationTracker(project: Project) : ModificationTracker { private val trackerService = project.service() diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinModuleOutOfBlockTrackerTest.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinModuleOutOfBlockTrackerTest.kt new file mode 100644 index 00000000000..af6a808c70c --- /dev/null +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/tests/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinModuleOutOfBlockTrackerTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.fir.low.level.api.trackers + +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager +import com.intellij.testFramework.PsiTestUtil +import junit.framework.Assert +import org.jetbrains.kotlin.idea.stubs.AbstractMultiModuleTest +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.rootManager +import org.jetbrains.kotlin.idea.util.sourceRoots +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import java.nio.file.Files +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.writeText + +class KotlinModuleOutOfBlockTrackerTest : AbstractMultiModuleTest() { + override fun getTestDataPath(): String = error("Should not be called") + + fun testThatModuleOutOfBlockChangeInfluenceOnlySingleModule() { + val moduleA = createModuleWithModificationTracker("a") { + listOf( + FileWithText("main.kt", "fun main() = 10") + ) + } + val moduleB = createModuleWithModificationTracker("b") + val moduleC = createModuleWithModificationTracker("c") + + + val moduleAWithTracker = ModuleWithModificationTracker(moduleA) + val moduleBWithTracker = ModuleWithModificationTracker(moduleB) + val moduleCWithTracker = ModuleWithModificationTracker(moduleC) + + moduleA.typeInFunctionBody("main.kt", textAfterTyping = "fun main() = hello10") + + Assert.assertTrue( + "Out of block modification count for module A with out of block should change after typing, modification count is ${moduleAWithTracker.modificationCount}", + moduleAWithTracker.changed() + ) + Assert.assertFalse( + "Out of block modification count for module B without out of block should not change after typing, modification count is ${moduleBWithTracker.modificationCount}", + moduleBWithTracker.changed() + ) + Assert.assertFalse( + "Out of block modification count for module C without out of block should not change after typing, modification count is ${moduleCWithTracker.modificationCount}", + moduleCWithTracker.changed() + ) + } + + fun testThatInEveryModuleOutOfBlockWillHappenAfterContentRootChange() { + val moduleA = createModuleWithModificationTracker("a") + val moduleB = createModuleWithModificationTracker("b") + val moduleC = createModuleWithModificationTracker("c") + + val moduleAWithTracker = ModuleWithModificationTracker(moduleA) + val moduleBWithTracker = ModuleWithModificationTracker(moduleB) + val moduleCWithTracker = ModuleWithModificationTracker(moduleC) + + runWriteAction { + moduleA.sourceRoots.first().createChildData(/* requestor = */ null, "file.kt") + } + + Assert.assertTrue( + "Out of block modification count for module A should change after content root change, modification count is ${moduleAWithTracker.modificationCount}", + moduleAWithTracker.changed() + ) + Assert.assertTrue( + "Out of block modification count for module B should change after content root change, modification count is ${moduleBWithTracker.modificationCount}", + moduleBWithTracker.changed() + ) + Assert.assertTrue( + "Out of block modification count for module C should change after content root change modification count is ${moduleCWithTracker.modificationCount}", + moduleCWithTracker.changed() + ) + } + + private fun Module.typeInFunctionBody(fileName: String, textAfterTyping: String) { + val file = "${sourceRoots.first().url}/$fileName" + val virtualFile = VirtualFileManager.getInstance().findFileByUrl(file)!! + val ktFile = PsiManager.getInstance(project).findFile(virtualFile) as KtFile + configureByExistingFile(virtualFile) + + val singleFunction = ktFile.declarations.single() as KtNamedFunction + + editor.caretModel.moveToOffset(singleFunction.bodyExpression!!.textOffset) + type("hello") + PsiDocumentManager.getInstance(project).commitAllDocuments() + Assert.assertEquals(textAfterTyping, ktFile.text) + } + + @OptIn(ExperimentalPathApi::class) + private fun createModuleWithModificationTracker( + name: String, + createFiles: () -> List = { emptyList() }, + ): Module { + val tmpDir = createTempDirectory().toPath() + createFiles().forEach { file -> + Files.createFile(tmpDir.resolve(file.name)).writeText(file.text) + } + val module: Module = createModule("$tmpDir/$name", moduleType) + val root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tmpDir.toFile())!! + WriteCommandAction.writeCommandAction(module.project).run { + root.refresh(false, true) + } + + PsiTestUtil.addSourceContentToRoots(module, root) + return module + } + + private data class FileWithText(val name: String, val text: String) + + private class ModuleWithModificationTracker(module: Module) { + private val modificationTracker = module.createModuleWithoutDependenciesOutOfBlockModificationTracker() + private val initialModificationCount = modificationTracker.modificationCount + + val modificationCount: Long + get() = modificationTracker.modificationCount + + fun changed(): Boolean = + modificationTracker.modificationCount != initialModificationCount + } +} \ No newline at end of file