From 6146dfc7c3ee0bfe09bbb86eb3a23514a3a88784 Mon Sep 17 00:00:00 2001 From: Nikolay Chashnikov Date: Fri, 19 May 2023 12:35:04 +0200 Subject: [PATCH] [platform] support custom content entries in Module::getModuleContent*Scope methods (IDEA-307392) If some IDE (e.g. Rider) contributes custom files to the module content which aren't located under the module content roots, they still need to be included in Module::getModuleContentScope. In order to support this, separate classes for content of a module and for content of a module and its dependencies were extracted from ModuleWithDependenciesScope. These new classes use ProjectFileIndex::getModuleForFile instead of getContentRootForFile, so they work for custom content files, and this also works faster than the generic implementation from ModuleWithDependenciesScope. VirtualFileEnumeration isn't supported for ModuleContentScope because it seems that this optimization is relevant for ModuleWithDependenciesScope only, and currently there is no efficient way to compute all content roots of a module including custom ones. GitOrigin-RevId: df8ec70a81d1a2954f64abc13a25fafd97a31eda --- .../com/intellij/roots/ModuleScopesTest.java | 3 + .../module/impl/scopes/ModuleContentScopes.kt | 58 +++++++++++++++++++ .../impl/scopes/ModuleScopeProviderImpl.java | 10 +++- .../scopes/ModuleWithDependenciesScope.java | 52 +++++------------ .../psi/search/GlobalSearchScopeTest.java | 2 +- .../fileIndex/CustomContentFileSetTest.kt | 24 ++++++-- 6 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleContentScopes.kt diff --git a/java/java-tests/testSrc/com/intellij/roots/ModuleScopesTest.java b/java/java-tests/testSrc/com/intellij/roots/ModuleScopesTest.java index 23d904168ea3..31faefe41525 100644 --- a/java/java-tests/testSrc/com/intellij/roots/ModuleScopesTest.java +++ b/java/java-tests/testSrc/com/intellij/roots/ModuleScopesTest.java @@ -78,7 +78,9 @@ public class ModuleScopesTest extends JavaModuleTestCase { VirtualFile file = myFixture.createFile("a/data/A.java", "class A {}"); PsiTestUtil.addContentRoot(module, file.getParent()); assertFalse(module.getModuleScope().contains(file)); + assertTrue(module.getModuleContentScope().contains(file)); assertFalse(module.getModuleWithDependenciesScope().contains(file)); + assertTrue(module.getModuleContentWithDependenciesScope().contains(file)); assertFalse(module.getModuleWithDependenciesAndLibrariesScope(true).contains(file)); } @@ -127,6 +129,7 @@ public class ModuleScopesTest extends JavaModuleTestCase { VirtualFile classB = myFixture.createFile("b/Test.java", "public class Test { }"); assertTrue(moduleA.getModuleWithDependenciesAndLibrariesScope(true).contains(classB)); assertFalse(moduleA.getModuleWithDependenciesAndLibrariesScope(false).contains(classB)); + assertFalse(moduleA.getModuleContentWithDependenciesScope().contains(classB)); assertFalse(moduleA.getModuleWithDependenciesAndLibrariesScope(false).isSearchInModuleContent(moduleB)); VirtualFile[] compilationClasspath = getCompilationClasspath(moduleA); diff --git a/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleContentScopes.kt b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleContentScopes.kt new file mode 100644 index 000000000000..b382231e5936 --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleContentScopes.kt @@ -0,0 +1,58 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("EqualsOrHashCode") +package com.intellij.openapi.module.impl.scopes + +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.indexing.IndexingBundle +import it.unimi.dsi.fastutil.objects.Object2IntMap +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap + +internal class ModuleContentScope(private val module: Module) : GlobalSearchScope(module.project) { + private val projectFileIndex = ProjectFileIndex.getInstance(module.project) + + override fun contains(file: VirtualFile): Boolean = projectFileIndex.getModuleForFile(file) == module + override fun isSearchInModuleContent(aModule: Module): Boolean = aModule == module + override fun isSearchInLibraries(): Boolean = false + override fun equals(other: Any?): Boolean = (other as? ModuleContentScope)?.module == module + override fun calcHashCode(): Int = module.hashCode() + override fun toString(): String = "ModuleContentScope{module=${module.name}}" + override fun getDisplayName(): String = IndexingBundle.message("search.scope.module", module.name) +} + +internal class ModuleWithDependenciesContentScope(private val rootModule: Module) : GlobalSearchScope(rootModule.project) { + private val moduleToOrder : Object2IntMap + private val projectFileIndex = ProjectFileIndex.getInstance(rootModule.project) + + init { + moduleToOrder = Object2IntOpenHashMap() + var i = 1 + ModuleRootManager.getInstance(rootModule).orderEntries().recursively().withoutLibraries().withoutSdk().productionOnly().forEachModule { + moduleToOrder.put(it, i++) + true + } + } + + override fun contains(file: VirtualFile): Boolean { + val module = projectFileIndex.getModuleForFile(file) + return module != null && module in moduleToOrder + } + + override fun isSearchInModuleContent(aModule: Module): Boolean = aModule in moduleToOrder + override fun isSearchInLibraries(): Boolean = false + + override fun compare(file1: VirtualFile, file2: VirtualFile): Int { + val order1 = projectFileIndex.getModuleForFile(file1)?.let { moduleToOrder.getInt(it) } ?: 0 + val order2 = projectFileIndex.getModuleForFile(file2)?.let { moduleToOrder.getInt(it) } ?: 0 + //see javadoc of GlobalSearchScope::compareTo: a positive result indicates that file1 is located in the classpath before file2 + return order2.compareTo(order1) + } + + override fun equals(other: Any?): Boolean = (other as? ModuleWithDependenciesContentScope)?.rootModule == rootModule + override fun calcHashCode(): Int = rootModule.hashCode() + override fun toString(): String = "ModuleWithDependenciesContentScope{rootModule=${rootModule.name}}" + override fun getDisplayName(): String = IndexingBundle.message("search.scope.module", rootModule.name) +} diff --git a/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleScopeProviderImpl.java b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleScopeProviderImpl.java index e39575b58e2f..a9391f951cc5 100644 --- a/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleScopeProviderImpl.java +++ b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleScopeProviderImpl.java @@ -16,6 +16,7 @@ public class ModuleScopeProviderImpl implements ModuleScopeProvider { private final IntObjectMap myScopeCache = ConcurrentCollectionFactory.createConcurrentIntObjectMap(); private ModuleWithDependentsTestScope myModuleTestsWithDependentsScope; + private volatile ModuleWithDependenciesContentScope myModuleWithDependenciesContentScope; public ModuleScopeProviderImpl(@NotNull Module module) { myModule = module; @@ -58,13 +59,17 @@ public class ModuleScopeProviderImpl implements ModuleScopeProvider { @NotNull @Override public GlobalSearchScope getModuleContentScope() { - return getCachedScope(ModuleWithDependenciesScope.CONTENT); + return new ModuleContentScope(myModule); } @NotNull @Override public GlobalSearchScope getModuleContentWithDependenciesScope() { - return getCachedScope(ModuleWithDependenciesScope.CONTENT | ModuleWithDependenciesScope.MODULES); + ModuleWithDependenciesContentScope scope = myModuleWithDependenciesContentScope; + if (scope == null) { + myModuleWithDependenciesContentScope = scope = new ModuleWithDependenciesContentScope(myModule); + } + return scope; } @Override @@ -112,5 +117,6 @@ public class ModuleScopeProviderImpl implements ModuleScopeProvider { public void clearCache() { myScopeCache.clear(); myModuleTestsWithDependentsScope = null; + myModuleWithDependenciesContentScope = null; } } diff --git a/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleWithDependenciesScope.java b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleWithDependenciesScope.java index 6143b17844aa..f22b496dfaef 100644 --- a/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleWithDependenciesScope.java +++ b/platform/analysis-impl/src/com/intellij/openapi/module/impl/scopes/ModuleWithDependenciesScope.java @@ -41,9 +41,8 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme public static final int LIBRARIES = 0x02; public static final int MODULES = 0x04; public static final int TESTS = 0x08; - public static final int CONTENT = 0x20; - @MagicConstant(flags = {COMPILE_ONLY, LIBRARIES, MODULES, TESTS, CONTENT}) + @MagicConstant(flags = {COMPILE_ONLY, LIBRARIES, MODULES, TESTS}) @interface ScopeConstant {} private static final Key>> CACHED_FILE_ID_ENUMERATIONS_KEY = @@ -68,25 +67,14 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme private Object2IntMap calcRoots(@Nullable ModelBranch branch) { Set roots = new LinkedHashSet<>(); - if (hasOption(CONTENT)) { - Set modules = calcModules(); - myModules = new HashSet<>(modules); - for (Module m : modules) { - for (ContentEntry entry : ModuleRootManager.getInstance(m).getContentEntries()) { - ContainerUtil.addIfNotNull(roots, branch == null ? entry.getFile() : branch.findFileByUrl(entry.getUrl())); - } - } - } - else { - OrderRootsEnumerator en = getOrderEnumeratorForOptions().roots(entry -> { - if (entry instanceof ModuleOrderEntry || entry instanceof ModuleSourceOrderEntry) return OrderRootType.SOURCES; - return OrderRootType.CLASSES; - }); - if (branch == null) { - Collections.addAll(roots, en.getRoots()); - } else { - roots.addAll(ContainerUtil.mapNotNull(en.getUrls(), branch::findFileByUrl)); - } + OrderRootsEnumerator en = getOrderEnumeratorForOptions().roots(entry -> { + if (entry instanceof ModuleOrderEntry || entry instanceof ModuleSourceOrderEntry) return OrderRootType.SOURCES; + return OrderRootType.CLASSES; + }); + if (branch == null) { + Collections.addAll(roots, en.getRoots()); + } else { + roots.addAll(ContainerUtil.mapNotNull(en.getUrls(), branch::findFileByUrl)); } int i = 1; @@ -109,9 +97,7 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme @NotNull private Set calcModules() { - // In the case that hasOption(CONTENT), the order of the modules set matters for - // ordering the content roots, so use a LinkedHashSet - Set modules = new LinkedHashSet<>(); + Set modules = new HashSet<>(); OrderEnumerator en = getOrderEnumeratorForOptions(); en.forEach(each -> { if (each instanceof ModuleOrderEntry) { @@ -162,12 +148,8 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme @Override public boolean contains(@NotNull VirtualFile file) { - Object2IntMap roots = getRoots(file); - if (hasOption(CONTENT)) { - return roots.containsKey(myProjectFileIndex.getContentRootForFile(file)); - } VirtualFile root = myProjectFileIndex.getModuleSourceOrLibraryClassesRoot(file); - return root != null && roots.containsKey(root); + return root != null && getRoots(file).containsKey(root); } private Object2IntMap getRoots(@NotNull VirtualFile file) { @@ -188,8 +170,8 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme @Override public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) { - VirtualFile r1 = getFileRoot(file1); - VirtualFile r2 = getFileRoot(file2); + VirtualFile r1 = myProjectFileIndex.getModuleSourceOrLibraryClassesRoot(file1); + VirtualFile r2 = myProjectFileIndex.getModuleSourceOrLibraryClassesRoot(file2); if (Comparing.equal(r1, r2)) return 0; if (r1 == null) return -1; @@ -203,14 +185,6 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme return i1 > 0 ? 1 : -1; } - @Nullable - private VirtualFile getFileRoot(@NotNull VirtualFile file) { - if (hasOption(CONTENT)) { - return myProjectFileIndex.getContentRootForFile(file); - } - return myProjectFileIndex.getModuleSourceOrLibraryClassesRoot(file); - } - @TestOnly public Collection getRoots() { List result = new ArrayList<>(myRoots.keySet()); diff --git a/platform/platform-tests/testSrc/com/intellij/psi/search/GlobalSearchScopeTest.java b/platform/platform-tests/testSrc/com/intellij/psi/search/GlobalSearchScopeTest.java index 742639abc9ab..453ded41862c 100644 --- a/platform/platform-tests/testSrc/com/intellij/psi/search/GlobalSearchScopeTest.java +++ b/platform/platform-tests/testSrc/com/intellij/psi/search/GlobalSearchScopeTest.java @@ -137,7 +137,7 @@ public class GlobalSearchScopeTest extends HeavyPlatformTestCase { assertNotNull(moduleRoot2); PsiTestUtil.addSourceRoot(getModule(), moduleRoot2); - GlobalSearchScope modScope = getModule().getModuleContentScope(); + GlobalSearchScope modScope = getModule().getModuleScope(); int compare = modScope.compare(moduleRoot, moduleRoot2); assertTrue(compare != 0); GlobalSearchScope union = modScope.uniteWith(GlobalSearchScope.EMPTY_SCOPE); diff --git a/platform/platform-tests/testSrc/com/intellij/workspaceModel/core/fileIndex/CustomContentFileSetTest.kt b/platform/platform-tests/testSrc/com/intellij/workspaceModel/core/fileIndex/CustomContentFileSetTest.kt index f76520cf3a2a..1cb9f9f8f4b8 100644 --- a/platform/platform-tests/testSrc/com/intellij/workspaceModel/core/fileIndex/CustomContentFileSetTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/workspaceModel/core/fileIndex/CustomContentFileSetTest.kt @@ -3,6 +3,8 @@ package com.intellij.workspaceModel.core.fileIndex import com.intellij.openapi.Disposable import com.intellij.openapi.application.readAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.roots.impl.assertIteratedContent import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.junit5.TestApplication @@ -10,6 +12,7 @@ import com.intellij.testFramework.junit5.TestDisposable import com.intellij.testFramework.rules.ProjectModelExtension import com.intellij.testFramework.workspaceModel.updateProjectModelAsync import com.intellij.util.indexing.testEntities.IndexingTestEntity +import com.intellij.workspaceModel.core.fileIndex.impl.ModuleRelatedRootData import com.intellij.workspaceModel.core.fileIndex.impl.WorkspaceFileIndexImpl import com.intellij.workspaceModel.ide.NonPersistentEntitySource import com.intellij.workspaceModel.ide.WorkspaceModel @@ -18,11 +21,10 @@ import com.intellij.workspaceModel.ide.toVirtualFileUrl import com.intellij.workspaceModel.storage.EntityStorage import com.intellij.workspaceModel.storage.url.VirtualFileUrlManager import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -import kotlin.test.assertFalse -import kotlin.test.assertTrue @TestApplication class CustomContentFileSetTest { @@ -33,14 +35,19 @@ class CustomContentFileSetTest { private val fileIndex get() = WorkspaceFileIndex.getInstance(projectModel.project) + private val projectFileIndex + get() = ProjectFileIndex.getInstance(projectModel.project) + @TestDisposable private lateinit var disposable: Disposable private lateinit var customContentFileSetRoot: VirtualFile + private lateinit var module: Module @BeforeEach fun setUp() { customContentFileSetRoot = projectModel.baseProjectDir.newVirtualDirectory("root") - WorkspaceFileIndexImpl.EP_NAME.point.registerExtension(CustomContentFileSetContributor(), disposable) + module = projectModel.createModule() + WorkspaceFileIndexImpl.EP_NAME.point.registerExtension(CustomContentFileSetContributor(module), disposable) } @Test @@ -59,6 +66,9 @@ class CustomContentFileSetTest { readAction { assertTrue(fileIndex.isInContent(file)) + assertEquals(module, projectFileIndex.getModuleForFile(file)) + assertTrue(module.moduleContentScope.contains(file)) + assertFalse(module.moduleScope.contains(file)) assertIteratedContent(projectModel.project, mustContain = listOf(root, file)) } @@ -68,18 +78,22 @@ class CustomContentFileSetTest { readAction { assertFalse(fileIndex.isInContent(root)) + assertNull(projectFileIndex.getModuleForFile(file)) + assertFalse(module.moduleContentScope.contains(file)) assertIteratedContent(projectModel.project, mustNotContain = listOf(root, file)) } } - private class CustomContentFileSetContributor : WorkspaceFileIndexContributor { + private class CustomContentFileSetContributor(private val module: Module) : WorkspaceFileIndexContributor { override val entityClass: Class get() = IndexingTestEntity::class.java override fun registerFileSets(entity: IndexingTestEntity, registrar: WorkspaceFileSetRegistrar, storage: EntityStorage) { for (root in entity.roots) { - registrar.registerFileSet(root, WorkspaceFileKind.CONTENT, entity, null) + registrar.registerFileSet(root, WorkspaceFileKind.CONTENT, entity, CustomModuleRelatedRootData(module)) } } } + + private class CustomModuleRelatedRootData(override val module: Module): ModuleRelatedRootData } \ No newline at end of file