diff --git a/compiler/frontend/src/org/jetbrains/kotlin/analyzer/AnalyzerFacade.kt b/compiler/frontend/src/org/jetbrains/kotlin/analyzer/AnalyzerFacade.kt index a287b01a820..8f81bb1707c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/analyzer/AnalyzerFacade.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/analyzer/AnalyzerFacade.kt @@ -18,7 +18,9 @@ package org.jetbrains.kotlin.analyzer import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.project.Project +import com.intellij.openapi.util.ModificationTracker import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiModificationTracker import org.jetbrains.kotlin.builtins.DefaultBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.config.LanguageVersionSettings @@ -85,9 +87,17 @@ class ResolverForProjectImpl( private val packagePartProviderFactory: (M, ModuleContent) -> PackagePartProvider = { _, _ -> PackagePartProvider.Empty }, private val firstDependency: M? = null, private val modulePlatforms: (M) -> MultiTargetPlatform?, - private val packageOracleFactory: PackageOracleFactory = PackageOracleFactory.OptimisticFactory + private val packageOracleFactory: PackageOracleFactory = PackageOracleFactory.OptimisticFactory, + private val invalidateOnOOCB: Boolean = true ) : ResolverForProject() { - private val descriptorByModule = mutableMapOf() + + private class ModuleData( + val moduleDescriptor: ModuleDescriptorImpl, + val modificationTracker: ModificationTracker?, + val modificationCount: Long? + ) + + private val descriptorByModule = mutableMapOf() private val moduleInfoByDescriptor = mutableMapOf() val modules = modules.toSet() @@ -156,18 +166,42 @@ class ResolverForProjectImpl( private fun doGetDescriptorForModule(module: M): ModuleDescriptorImpl { if (module in modules) { return projectContext.storageManager.compute { - descriptorByModule.getOrPut(module) { - ModuleDescriptorImpl(module.name, - projectContext.storageManager, builtIns, modulePlatforms(module), module.capabilities).apply { - moduleInfoByDescriptor[this] = module - setupModuleDescriptor(module, this) - } + var moduleData = descriptorByModule.getOrPut(module) { + createModuleDescriptor(module) } + val currentModCount = moduleData.modificationTracker?.modificationCount + if (currentModCount != null && currentModCount > moduleData.modificationCount!!) { + moduleData = recreateModuleDescriptor(module) + } + moduleData.moduleDescriptor } } return delegateResolver.descriptorForModule(module) as ModuleDescriptorImpl } + + private fun recreateModuleDescriptor(module: M): ModuleData { + val oldDescriptor = descriptorByModule[module]?.moduleDescriptor + if (oldDescriptor != null) { + oldDescriptor.isValid = false + moduleInfoByDescriptor.remove(oldDescriptor) + resolverByModuleDescriptor.remove(oldDescriptor) + } + + val moduleData = createModuleDescriptor(module) + descriptorByModule[module] = moduleData + return moduleData + } + + private fun createModuleDescriptor(module: M): ModuleData { + val moduleDescriptor = ModuleDescriptorImpl(module.name, + projectContext.storageManager, builtIns, modulePlatforms(module), module.capabilities) + moduleInfoByDescriptor[moduleDescriptor] = module + setupModuleDescriptor(module, moduleDescriptor) + val modificationTracker = (module as? TrackableModuleInfo)?.createModificationTracker() ?: + (PsiModificationTracker.SERVICE.getInstance(projectContext.project).outOfCodeBlockModificationTracker.takeIf { invalidateOnOOCB }) + return ModuleData(moduleDescriptor, modificationTracker, modificationTracker?.modificationCount) + } } data class ModuleContent( @@ -207,6 +241,10 @@ interface ModuleInfo { } } +interface TrackableModuleInfo : ModuleInfo { + fun createModificationTracker(): ModificationTracker +} + abstract class AnalyzerFacade { abstract fun createResolverForModule( moduleInfo: M, diff --git a/core/descriptors/src/org/jetbrains/kotlin/descriptors/ModuleDescriptor.kt b/core/descriptors/src/org/jetbrains/kotlin/descriptors/ModuleDescriptor.kt index 88e301b4863..b69ae2b1173 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/descriptors/ModuleDescriptor.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/descriptors/ModuleDescriptor.kt @@ -48,4 +48,6 @@ interface ModuleDescriptor : DeclarationDescriptor { class Capability(val name: String) { override fun toString() = name } + + val isValid: Boolean } diff --git a/core/descriptors/src/org/jetbrains/kotlin/descriptors/impl/ModuleDescriptorImpl.kt b/core/descriptors/src/org/jetbrains/kotlin/descriptors/impl/ModuleDescriptorImpl.kt index f61eaaa28f6..96dbf20cb66 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/descriptors/impl/ModuleDescriptorImpl.kt +++ b/core/descriptors/src/org/jetbrains/kotlin/descriptors/impl/ModuleDescriptorImpl.kt @@ -47,6 +47,14 @@ class ModuleDescriptorImpl @JvmOverloads constructor( private var dependencies: ModuleDependencies? = null private var packageFragmentProviderForModuleContent: PackageFragmentProvider? = null + override var isValid: Boolean = true + + private fun assertValid() { + if (!isValid) { + throw IllegalStateException("Accessing invalid module descriptor $this") + } + } + private val packages = storageManager.createMemoizedFunction { fqName: FqName -> LazyPackageViewDescriptorImpl(this, fqName, storageManager) } @@ -60,9 +68,13 @@ class ModuleDescriptorImpl @JvmOverloads constructor( override val allImplementingModules: Set get() = this.dependencies.sure { "Dependencies of module $id were not set" }.allImplementingModules - override fun getPackage(fqName: FqName): PackageViewDescriptor = packages(fqName) + override fun getPackage(fqName: FqName): PackageViewDescriptor { + assertValid() + return packages(fqName) + } override fun getSubPackagesOf(fqName: FqName, nameFilter: (Name) -> Boolean): Collection { + assertValid() return packageFragmentProvider.getSubPackagesOf(fqName, nameFilter) } @@ -117,7 +129,10 @@ class ModuleDescriptorImpl @JvmOverloads constructor( } val packageFragmentProvider: PackageFragmentProvider - get() = packageFragmentProviderForWholeModuleWithDependencies + get() { + assertValid() + return packageFragmentProviderForWholeModuleWithDependencies + } @Suppress("UNCHECKED_CAST") override fun getCapability(capability: ModuleDescriptor.Capability) = capabilities[capability] as? T diff --git a/core/descriptors/src/org/jetbrains/kotlin/types/ErrorUtils.java b/core/descriptors/src/org/jetbrains/kotlin/types/ErrorUtils.java index 5a9a92b5ddc..381751cf9e9 100644 --- a/core/descriptors/src/org/jetbrains/kotlin/types/ErrorUtils.java +++ b/core/descriptors/src/org/jetbrains/kotlin/types/ErrorUtils.java @@ -122,6 +122,11 @@ public class ErrorUtils { public KotlinBuiltIns getBuiltIns() { return DefaultBuiltIns.getInstance(); } + + @Override + public boolean isValid() { + return false; + } }; } diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/IdeaModuleInfos.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/IdeaModuleInfos.kt index cf2834c89be..2b2f34e4313 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/IdeaModuleInfos.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/IdeaModuleInfos.kt @@ -23,6 +23,7 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.* import com.intellij.openapi.roots.impl.libraries.LibraryEx import com.intellij.openapi.roots.libraries.Library +import com.intellij.openapi.util.ModificationTracker import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.CachedValueProvider @@ -30,9 +31,11 @@ import com.intellij.psi.util.CachedValuesManager import com.intellij.util.PathUtil import com.intellij.util.SmartList import org.jetbrains.kotlin.analyzer.ModuleInfo +import org.jetbrains.kotlin.analyzer.TrackableModuleInfo import org.jetbrains.kotlin.caches.resolve.LibraryModuleInfo import org.jetbrains.kotlin.descriptors.ModuleDescriptor import org.jetbrains.kotlin.idea.framework.getLibraryPlatform +import org.jetbrains.kotlin.idea.project.KotlinModuleModificationTracker import org.jetbrains.kotlin.idea.project.TargetPlatformDetector import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope import org.jetbrains.kotlin.idea.util.isInSourceContentWithoutInjected @@ -97,7 +100,7 @@ private fun ideaModelDependencies(module: Module, productionOnly: Boolean): List return result.toList() } -interface ModuleSourceInfo : IdeaModuleInfo { +interface ModuleSourceInfo : IdeaModuleInfo, TrackableModuleInfo { val module: Module override val displayedName get() = module.name @@ -107,6 +110,9 @@ interface ModuleSourceInfo : IdeaModuleInfo { override val platform: TargetPlatform get() = TargetPlatformDetector.getPlatform(module) + + override fun createModificationTracker(): ModificationTracker = + KotlinModuleModificationTracker(module) } data class ModuleProductionSourceInfo(override val module: Module) : ModuleSourceInfo { diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinCacheServiceImpl.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinCacheServiceImpl.kt index 4abf109c3f3..4f6bc881abb 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinCacheServiceImpl.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/KotlinCacheServiceImpl.kt @@ -125,6 +125,7 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService { LibraryModificationTracker.getInstance(project), ProjectRootModificationTracker.getInstance(project) ), + invalidateOnOOCB = false, reuseDataFrom = null ) @@ -134,6 +135,7 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService { project, librariesContext, settings, reuseDataFrom = facadeForSdk, moduleFilter = { it is LibraryInfo }, + invalidateOnOOCB = false, dependencies = listOf( LibraryModificationTracker.getInstance(project), ProjectRootModificationTracker.getInstance(project) @@ -146,7 +148,10 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService { project, modulesContext, settings, reuseDataFrom = facadeForLibraries, moduleFilter = { !it.isLibraryClasses() }, - dependencies = listOf(PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT) + dependencies = listOf( + LibraryModificationTracker.getInstance(project), + ProjectRootModificationTracker.getInstance(project) + ) ) } diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ModuleDependencyMapper.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ModuleDependencyMapper.kt index 96142c2965c..8c4c62e946e 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ModuleDependencyMapper.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ModuleDependencyMapper.kt @@ -52,7 +52,8 @@ fun createModuleResolverProvider( moduleFilter: (IdeaModuleInfo) -> Boolean, allModules: Collection?, providedBuiltIns: KotlinBuiltIns?, // null means create new builtins based on SDK - dependencies: Collection + dependencies: Collection, + invalidateOnOOCB: Boolean = true ): ModuleResolverProvider { val builtIns = providedBuiltIns ?: createBuiltIns(analysisSettings, globalContext) @@ -82,7 +83,8 @@ fun createModuleResolverProvider( delegateResolver, { _, c -> IDEPackagePartProvider(c.moduleContentScope) }, analysisSettings.sdk?.let { SdkInfo(project, it) }, modulePlatforms = { module -> module.platform?.multiTargetPlatform }, - packageOracleFactory = project.service() + packageOracleFactory = project.service(), + invalidateOnOOCB = invalidateOnOOCB ) if (providedBuiltIns == null && builtIns is JvmBuiltIns) { diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ProjectResolutionFacade.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ProjectResolutionFacade.kt index 90f9ab00a76..194ef99b6e3 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ProjectResolutionFacade.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/ProjectResolutionFacade.kt @@ -30,14 +30,15 @@ import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.CompositeBindingContext internal class ProjectResolutionFacade( - val debugString: String, - val resolverDebugName: String, + private val debugString: String, + private val resolverDebugName: String, val project: Project, val globalContext: GlobalContextImpl, val settings: PlatformAnalysisSettings, val reuseDataFrom: ProjectResolutionFacade?, val moduleFilter: (IdeaModuleInfo) -> Boolean, val dependencies: List, + private val invalidateOnOOCB: Boolean = true, val syntheticFiles: Collection = listOf(), val allModules: Collection? = null // null means create resolvers for modules from idea model ) { @@ -61,11 +62,12 @@ internal class ProjectResolutionFacade( delegateResolver = delegateResolverForProject, moduleFilter = moduleFilter, allModules = allModules, providedBuiltIns = delegateResolverProvider?.builtIns, - dependencies = dependencies + dependencies = dependencies, + invalidateOnOOCB = invalidateOnOOCB ) } - val moduleResolverProvider: ModuleResolverProvider + private val moduleResolverProvider: ModuleResolverProvider get() = globalContext.storageManager.compute { cachedValue.value } fun resolverForModuleInfo(moduleInfo: IdeaModuleInfo) = moduleResolverProvider.resolverForProject.resolverForModule(moduleInfo) diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt index 475b62539c8..153f4ebdbaf 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt @@ -16,8 +16,12 @@ package org.jetbrains.kotlin.idea.project +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.Key +import com.intellij.openapi.util.ModificationTracker import com.intellij.pom.PomManager import com.intellij.pom.PomModelAspect import com.intellij.pom.event.PomModelEvent @@ -28,6 +32,7 @@ import com.intellij.psi.PsiCodeFragment import com.intellij.psi.PsiElement import com.intellij.psi.impl.PsiModificationTrackerImpl import com.intellij.psi.util.PsiModificationTracker +import com.intellij.util.CommonProcessors import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType import org.jetbrains.kotlin.psi.psiUtil.isAncestor @@ -41,10 +46,27 @@ class KotlinCodeBlockModificationListener( project: Project, private val treeAspect: TreeAspect ) { + private val perModuleModCount = mutableMapOf() + private val modificationTrackerImpl = modificationTracker as PsiModificationTrackerImpl + + private var lastAffectedModule: Module? = null + private var lastAffectedModuleModCount = -1L + + // All modifications since that count are known to be single-module modifications reflected in + // perModuleModCount map + private var perModuleChangesHighwatermark: Long? = null + + fun getModificationCount(module: Module): Long { + return perModuleModCount[module] ?: + perModuleChangesHighwatermark ?: + modificationTrackerImpl.outOfCodeBlockModificationCount + } + + fun hasPerModuleModificationCounts() = perModuleChangesHighwatermark != null + init { val model = PomManager.getModel(project) - @Suppress("NAME_SHADOWING") - val modificationTracker = modificationTracker as PsiModificationTrackerImpl + val messageBusConnection = project.messageBus.connect() model.addModelListener(object: PomModelListener { override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean { return aspect == treeAspect @@ -58,13 +80,31 @@ class KotlinCodeBlockModificationListener( // contents to be replaced, which is represented in a POM event as an empty list of changed elements if (changedElements.any { getInsideCodeBlockModificationScope(it.psi) == null } || (file is PsiCodeFragment && changedElements.isEmpty())) { + messageBusConnection.deliverImmediately() if (file.isPhysical) { - modificationTracker.incCounter() + lastAffectedModule = ModuleUtil.findModuleForPsiElement(file) + lastAffectedModuleModCount = modificationTrackerImpl.outOfCodeBlockModificationCount + modificationTrackerImpl.incCounter() } incOutOfBlockModificationCount(file) } } }) + + messageBusConnection.subscribe(PsiModificationTracker.TOPIC, PsiModificationTracker.Listener { + val newModCount = modificationTrackerImpl.outOfCodeBlockModificationCount + val affectedModule = lastAffectedModule + if (affectedModule != null && newModCount == lastAffectedModuleModCount + 1) { + if (perModuleChangesHighwatermark== null) { + perModuleChangesHighwatermark = lastAffectedModuleModCount + } + perModuleModCount[affectedModule] = newModCount + } + else { + perModuleChangesHighwatermark = null + perModuleModCount.clear() + } + }) } companion object { @@ -117,6 +157,8 @@ class KotlinCodeBlockModificationListener( KtProperty::class.java, KtNamedFunction::class.java ) + + fun getInstance(project: Project) = project.getComponent(KotlinCodeBlockModificationListener::class.java) } } @@ -124,3 +166,31 @@ private val FILE_OUT_OF_BLOCK_MODIFICATION_COUNT = Key("FILE_OUT_OF_BLOCK_ val KtFile.outOfBlockModificationCount: Long get() = getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0 + +class KotlinModuleModificationTracker(val module: Module): ModificationTracker { + private val kotlinModCountListener = KotlinCodeBlockModificationListener.getInstance(module.project) + private val psiModificationTracker = PsiModificationTracker.SERVICE.getInstance(module.project) + private val dependencies by lazy { + HashSet().apply { + ModuleRootManager.getInstance(module).orderEntries().recursively().forEachModule( + CommonProcessors.CollectProcessor(this)) + } + } + + override fun getModificationCount(): Long { + val currentGlobalCount = psiModificationTracker.outOfCodeBlockModificationCount + + if (kotlinModCountListener.hasPerModuleModificationCounts()) { + val selfCount = kotlinModCountListener.getModificationCount(module) + if (selfCount == currentGlobalCount) return selfCount + var maxCount = selfCount + for (dependency in dependencies) { + val depCount = kotlinModCountListener.getModificationCount(dependency) + if (depCount == currentGlobalCount) return currentGlobalCount + if (depCount > maxCount) maxCount = depCount + } + return maxCount + } + return currentGlobalCount + } +} diff --git a/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m1/m1.kt b/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m1/m1.kt new file mode 100644 index 00000000000..22412f84c9e --- /dev/null +++ b/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m1/m1.kt @@ -0,0 +1,4 @@ +package foo.bar.used + +public fun m1() { +} \ No newline at end of file diff --git a/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m2/m2.kt b/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m2/m2.kt new file mode 100644 index 00000000000..65a5fe6f69c --- /dev/null +++ b/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m2/m2.kt @@ -0,0 +1,5 @@ +import foo.bar.used.* + +public fun m2() { + m1() +} \ No newline at end of file diff --git a/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m3/m3.kt b/idea/testData/multiModuleHighlighting/recomputeResolversOnChange/m3/m3.kt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleHighlightingTest.kt b/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleHighlightingTest.kt index e86b61dc5ec..42e15ea8c57 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleHighlightingTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/caches/resolve/MultiModuleHighlightingTest.kt @@ -17,8 +17,16 @@ package org.jetbrains.kotlin.idea.caches.resolve import com.intellij.facet.FacetManager +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.DependencyScope +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager +import com.intellij.psi.impl.PsiModificationTrackerImpl +import com.intellij.psi.util.PsiModificationTracker import org.jetbrains.kotlin.analyzer.ModuleInfo import org.jetbrains.kotlin.analyzer.ResolverForModuleComputationTracker import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments @@ -30,8 +38,12 @@ import org.jetbrains.kotlin.idea.completion.test.withServiceRegistered import org.jetbrains.kotlin.idea.facet.KotlinFacetConfiguration import org.jetbrains.kotlin.idea.facet.KotlinFacetType import org.jetbrains.kotlin.idea.framework.JSLibraryKind +import org.jetbrains.kotlin.idea.project.KotlinCodeBlockModificationListener +import org.jetbrains.kotlin.idea.project.KotlinModuleModificationTracker import org.jetbrains.kotlin.idea.test.PluginTestCaseBase +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.projectStructure.sdk import org.jetbrains.kotlin.test.TestJdkKind.FULL_JDK open class MultiModuleHighlightingTest : AbstractMultiModuleHighlightingTest() { @@ -66,20 +78,9 @@ open class MultiModuleHighlightingTest : AbstractMultiModuleHighlightingTest() { } fun testLazyResolvers() { - val resolversComputed = mutableSetOf() + val tracker = ResolverTracker() - val resolversTracker = object : ResolverForModuleComputationTracker { - override fun onResolverComputed(moduleInfo: ModuleInfo) { - (moduleInfo as IdeaModuleInfo).let { - if (it is ModuleSourceInfo) { - val module = it.module - resolversComputed.add(module) - } - } - } - } - - project.withServiceRegistered(resolversTracker) { + project.withServiceRegistered(tracker) { val module1 = module("m1") val module2 = module("m2") val module3 = module("m3") @@ -87,15 +88,82 @@ open class MultiModuleHighlightingTest : AbstractMultiModuleHighlightingTest() { module3.addDependency(module2) module3.addDependency(module1) - assertTrue(module1 !in resolversComputed) - assertTrue(module2 !in resolversComputed) - assertTrue(module3 !in resolversComputed) + assertTrue(module1 !in tracker.moduleResolversComputed) + assertTrue(module2 !in tracker.moduleResolversComputed) + assertTrue(module3 !in tracker.moduleResolversComputed) checkHighlightingInAllFiles { "m3" in file.name } - assertTrue(module1 in resolversComputed) - assertTrue(module2 !in resolversComputed) - assertTrue(module3 in resolversComputed) + assertTrue(module1 in tracker.moduleResolversComputed) + assertTrue(module2 !in tracker.moduleResolversComputed) + assertTrue(module3 in tracker.moduleResolversComputed) + } + } + + class ResolverTracker : ResolverForModuleComputationTracker { + val moduleResolversComputed = mutableListOf() + val sdkResolversComputed = mutableListOf() + + override fun onResolverComputed(moduleInfo: ModuleInfo) { + when (moduleInfo) { + is ModuleSourceInfo -> moduleResolversComputed.add(moduleInfo.module) + is SdkInfo -> sdkResolversComputed.add(moduleInfo.sdk) + } + } + } + + fun testRecomputeResolversOnChange() { + val tracker = ResolverTracker() + + project.withServiceRegistered(tracker) { + val module1 = module("m1") + val module2 = module("m2") + val module3 = module("m3") + + module2.addDependency(module1) + module3.addDependency(module2) + // Ensure modules have the same SDK instance, and not two distinct SDKs with the same path + ModuleRootModificationUtil.setModuleSdk(module2, module1.sdk) + + assertEquals(0, tracker.sdkResolversComputed.size) + + checkHighlightingInAllFiles { "m2" in file.name } + + assertEquals(2, tracker.moduleResolversComputed.size) + + tracker.sdkResolversComputed.clear() + tracker.moduleResolversComputed.clear() + + val module1ModCount = KotlinCodeBlockModificationListener.getInstance(myProject).getModificationCount(module1) + + val module1ModTracker = KotlinModuleModificationTracker(module1) + val module2ModTracker = KotlinModuleModificationTracker(module2) + val module3ModTracker = KotlinModuleModificationTracker(module3) + + val contentRoot = ModuleRootManager.getInstance(module2).contentRoots.single() + val m2 = contentRoot.findChild("m2.kt")!! + val m2doc = FileDocumentManager.getInstance().getDocument(m2)!! + project.executeWriteCommand("a") { + m2doc.insertString(m2doc.textLength , "fun foo() = 1") + PsiDocumentManager.getInstance(myProject).commitAllDocuments() + } + val currentModCount = PsiManager.getInstance(project).modificationTracker.outOfCodeBlockModificationCount + + assertEquals(module1ModCount, KotlinCodeBlockModificationListener.getInstance(myProject).getModificationCount(module1)) + assertEquals(module1ModCount, module1ModTracker.modificationCount) + assertEquals(currentModCount, module2ModTracker.modificationCount) + assertEquals(currentModCount, module3ModTracker.modificationCount) + + checkHighlightingInAllFiles { "m2" in file.name } + + assertEquals(0, tracker.sdkResolversComputed.size) + assertEquals(1, tracker.moduleResolversComputed.size) + + tracker.moduleResolversComputed.clear() + (PsiModificationTracker.SERVICE.getInstance(myProject) as PsiModificationTrackerImpl).incOutOfCodeBlockModificationCounter() + checkHighlightingInAllFiles { "m2" in file.name } + assertEquals(0, tracker.sdkResolversComputed.size) + assertEquals(2, tracker.moduleResolversComputed.size) } }