[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
This commit is contained in:
Nikolay Chashnikov
2023-05-19 12:35:04 +02:00
committed by intellij-monorepo-bot
parent c7ff71c8bf
commit 6146dfc7c3
6 changed files with 102 additions and 47 deletions

View File

@@ -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);

View File

@@ -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<Module>
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)
}

View File

@@ -16,6 +16,7 @@ public class ModuleScopeProviderImpl implements ModuleScopeProvider {
private final IntObjectMap<GlobalSearchScope> 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;
}
}

View File

@@ -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<CachedValue<ConcurrentMap<Integer, VirtualFileEnumeration>>> CACHED_FILE_ID_ENUMERATIONS_KEY =
@@ -68,25 +67,14 @@ public final class ModuleWithDependenciesScope extends GlobalSearchScope impleme
private Object2IntMap<VirtualFile> calcRoots(@Nullable ModelBranch branch) {
Set<VirtualFile> roots = new LinkedHashSet<>();
if (hasOption(CONTENT)) {
Set<Module> 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<Module> calcModules() {
// In the case that hasOption(CONTENT), the order of the modules set matters for
// ordering the content roots, so use a LinkedHashSet
Set<Module> modules = new LinkedHashSet<>();
Set<Module> 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<VirtualFile> 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<VirtualFile> 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<VirtualFile> getRoots() {
List<VirtualFile> result = new ArrayList<>(myRoots.keySet());

View File

@@ -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);

View File

@@ -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<IndexingTestEntity> {
private class CustomContentFileSetContributor(private val module: Module) : WorkspaceFileIndexContributor<IndexingTestEntity> {
override val entityClass: Class<IndexingTestEntity>
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
}