[eel, sdk] IJPL-175225: Provide a way to get views of ProjectJdkTable for a particular environment

GitOrigin-RevId: dfc793199c8b2b4e3e996bdea36efb0f4de3404c
This commit is contained in:
Konstantin.Nisht
2025-01-17 13:26:04 +01:00
committed by intellij-monorepo-bot
parent c989009f1f
commit 26f4c58a67
10 changed files with 321 additions and 42 deletions

View File

@@ -0,0 +1,85 @@
// 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.openapi.projectRoots.impl
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkTableProjectViewProvider
import com.intellij.openapi.projectRoots.SdkTypeId
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.eel.EelDescriptor
import com.intellij.platform.eel.provider.LocalEelDescriptor
import com.intellij.platform.eel.provider.getEelDescriptor
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Unmodifiable
import java.nio.file.Path
@ApiStatus.Internal
class SdkTableProjectViewProviderImpl(val project: Project) : SdkTableProjectViewProvider {
private val descriptor = project.basePath?.toNioPathOrNull()?.getEelDescriptor() ?: LocalEelDescriptor
override fun getSdkTableView(): ProjectJdkTable {
val generalTable = ProjectJdkTable.getInstance()
if (!Registry.`is`("ide.workspace.model.per.environment.model.separation")) {
return generalTable
}
return ProjectJdkTableProjectView(descriptor, generalTable)
}
}
private class ProjectJdkTableProjectView(val descriptor: EelDescriptor, val delegate: ProjectJdkTable) : ProjectJdkTable() {
override fun findJdk(name: String): Sdk? {
return delegate.allJdks.find {
it.name == name && validateDescriptor(it)
}
}
override fun findJdk(name: String, type: String): Sdk? {
// sometimes delegate.findJdk can do mutating operations, like in case of ProjectJdkTableImpl
return allJdks.find { it.name == name && it.sdkType.name == type } ?: delegate.findJdk(name, type)
}
override fun getAllJdks(): Array<out Sdk> {
return delegate.allJdks.filter(::validateDescriptor).toTypedArray()
}
private fun validateDescriptor(sdk: Sdk): Boolean {
val sdkDescriptor = sdk.homePath?.let(Path::of)?.getEelDescriptor()
return if (sdkDescriptor == null) {
true
}
else {
sdkDescriptor == this.descriptor
}
}
override fun getSdksOfType(type: SdkTypeId): @Unmodifiable List<Sdk?> {
return allJdks.filter { it.sdkType == type }
}
override fun addJdk(jdk: Sdk) {
delegate.addJdk(jdk)
}
override fun removeJdk(jdk: Sdk) {
delegate.removeJdk(jdk)
}
override fun updateJdk(originalJdk: Sdk, modifiedJdk: Sdk) {
delegate.updateJdk(originalJdk, modifiedJdk)
}
override fun getDefaultSdkType(): SdkTypeId {
return delegate.defaultSdkType
}
override fun getSdkTypeByName(name: String): SdkTypeId {
return allJdks.find { it.name == name }?.sdkType ?: delegate.getSdkTypeByName(name)
}
override fun createSdk(name: String, sdkType: SdkTypeId): Sdk {
return delegate.createSdk(name, sdkType)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.openapi.projectRoots.impl;
import com.intellij.openapi.application.WriteAction;
@@ -12,6 +12,7 @@ import com.intellij.openapi.projectRoots.SdkType;
import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory;
import com.intellij.openapi.roots.ui.configuration.UnknownSdk;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.workspace.jps.entities.SdkEntity;
import com.intellij.ui.EditorNotificationPanel;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -116,7 +117,7 @@ final class UnknownMissingSdkFix implements UnknownSdkFix {
WriteAction.run(() -> {
ProjectJdkTable table = ProjectJdkTable.getInstance();
if (sdkName != null) {
Sdk clash = table.findJdk(sdkName);
SdkEntity clash = SdkUtils.findClashingSdk(sdkName, sdk);
if (clash != null) {
LOG.warn("SDK with name " + sdkName + " already exists: clash=" + clash + ", new=" + sdk);
return;

View File

@@ -0,0 +1,19 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("SdkUtils")
@file:ApiStatus.Internal
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.platform.eel.provider.LocalEelDescriptor
import com.intellij.platform.eel.provider.getEelDescriptor
import com.intellij.platform.workspace.jps.entities.SdkEntity
import com.intellij.workspaceModel.ide.impl.GlobalWorkspaceModel
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
fun findClashingSdk(sdkName: String, sdk: Sdk): SdkEntity? {
val descriptor = sdk.homePath?.let { Path.of(it) }?.getEelDescriptor() ?: LocalEelDescriptor
val relevantSnapshot = GlobalWorkspaceModel.getInstance(descriptor).currentSnapshot
return relevantSnapshot.entities(SdkEntity::class.java).find { it.name == sdkName }
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.util.indexing.roots;
import com.intellij.openapi.application.ReadAction;
@@ -103,7 +103,7 @@ public final class IndexableFilesIndexImpl implements IndexableFilesIndex {
ModuleDependencyIndex moduleDependencyIndex = ModuleDependencyIndex.getInstance(project);
if (!Registry.is("ide.workspace.model.sdk.remove.custom.processing")) {
List<Sdk> sdks = new ArrayList<>();
for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
for (Sdk sdk : ProjectJdkTable.getInstance(project).getAllJdks()) {
if (moduleDependencyIndex.hasDependencyOn(sdk)) {
sdks.add(sdk);
}

View File

@@ -1,18 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.openapi.projectRoots
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.roots.ui.configuration.SdkTestCase.TestSdkGenerator
import com.intellij.openapi.roots.ui.configuration.SdkTestCase.TestSdkGenerator.SdkInfo
import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel
import com.intellij.openapi.roots.ui.configuration.testSdkFixture
import com.intellij.platform.eel.path.EelPath
import com.intellij.platform.eel.provider.utils.EelPathUtils
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.platform.testFramework.junit5.eel.fixture.eelFixture
import com.intellij.platform.testFramework.junit5.eel.fixture.tempDirFixture
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.RegistryKey
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.projectFixture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import java.nio.file.Files
@@ -26,45 +28,184 @@ class ProjectJdkEelTest {
val tempDirFixture = eel.tempDirFixture()
val eelProject = projectFixture(tempDirFixture, openAfterCreation = true)
val testSdk = testSdkFixture()
val testSdkGenerator = testSdkFixture()
@Test
fun `only relevant jdks are visible`() = timeoutRunBlocking {
val jdkTable = ProjectJdkTable.getInstance()
val localTempDirectory = Files.createTempDirectory("local-sdk")
val localSdk = TestSdkGenerator.createTestSdk(SdkInfo("local sdk", "1", localTempDirectory.toString()))
writeAction {
jdkTable.addJdk(localSdk)
}
withAddedSdk(jdkTable, "local sdk", EnvKind.Local) {
withAddedSdk(jdkTable, "eel sdk", EnvKind.Eel) {
val localModel = getLocalSdkModel()
val eelModel = getEelSdkModel()
val eelTempDirectory = EelPathUtils.createTemporaryDirectory(eelProject.get())
val eelSdk = testSdk.get().createTestSdk(SdkInfo("eel sdk", "1", eelTempDirectory.toString()))
writeAction {
jdkTable.addJdk(eelSdk)
}
val localModel = ProjectSdksModel().apply {
reset(localProject.get())
}
val eelModel = ProjectSdksModel().apply {
reset(eelProject.get())
}
try {
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
Assertions.assertTrue {
localModel.sdks.intersect(eelModel.sdks.asList()).isEmpty()
}
}
finally {
writeAction {
jdkTable.removeJdk(localSdk)
jdkTable.removeJdk(eelSdk)
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
Assertions.assertTrue {
localModel.sdks.intersect(eelModel.sdks.asList()).isEmpty()
}
}
}
}
@Test
fun `multiple SDKs with the same name can exist`() = timeoutRunBlocking {
val jdkTable = ProjectJdkTable.getInstance()
val sharedName = "shared sdk name"
withAddedSdk(jdkTable, sharedName, EnvKind.Local) {
withAddedSdk(jdkTable, sharedName, EnvKind.Eel) {
val localModel = getLocalSdkModel()
val eelModel = getEelSdkModel()
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
Assertions.assertTrue {
localModel.sdks.intersect(eelModel.sdks.asList()).isEmpty()
}
Assertions.assertTrue {
localModel.sdks[0].name == eelModel.sdks[0].name
}
}
}
}
@Test
fun `removal of one jdk does not affect another`() = timeoutRunBlocking {
val jdkTable = ProjectJdkTable.getInstance()
val sharedName = "shared sdk name"
withAddedSdk(jdkTable, sharedName, EnvKind.Local) {
withAddedSdk(jdkTable, sharedName, EnvKind.Eel) {
val localModel = getLocalSdkModel()
val eelModel = getEelSdkModel()
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
eelModel.removeSdk(eelModel.sdks[0])
eelModel.apply()
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 0 }
Assertions.assertTrue {
localModel.sdks[0].name == sharedName
}
}
}
}
@Test
fun `addition of one jdk does not affect another`() = timeoutRunBlocking {
val jdkTable = ProjectJdkTable.getInstance()
val sharedName = "shared sdk name"
withAddedSdk(jdkTable, sharedName, EnvKind.Eel) {
val localModel = getLocalSdkModel()
val eelModel = getEelSdkModel()
val newSdk = testSdkGenerator.get().createTestSdk(SdkInfo(sharedName, "1", Files.createTempDirectory(sharedName).toString()))
Assertions.assertTrue { localModel.sdks.size == 0 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
writeAction {
jdkTable.addJdk(newSdk)
}
try {
localModel.reset(localProject.get())
Assertions.assertTrue { localModel.sdks.size == 1 }
Assertions.assertTrue { eelModel.sdks.size == 1 }
Assertions.assertEquals(newSdk.homePath, localModel.sdks[0].homePath)
Assertions.assertNotEquals(newSdk.homePath, eelModel.sdks[0].homePath)
}
finally {
writeAction {
jdkTable.removeJdk(newSdk)
}
}
}
}
@Test
@RegistryKey("ide.workspace.model.per.environment.model.separation", "true")
fun `different views of ProjectJdkTable are independent`() = timeoutRunBlocking {
val globalTable = ProjectJdkTable.getInstance()
val localJdkTableView = ProjectJdkTable.getInstance(localProject.get())
val eelJdkTableView = ProjectJdkTable.getInstance(eelProject.get())
withAddedSdk(localJdkTableView, "local sdk", EnvKind.Local) {
withAddedSdk(eelJdkTableView, "eel sdk", EnvKind.Eel) {
val allJdks = globalTable.allJdks.toList()
val allLocalJdks = localJdkTableView.allJdks.toList()
val allEelJdks = eelJdkTableView.getAllJdks().toList()
Assertions.assertEquals(2, allJdks.size)
Assertions.assertEquals(1, allLocalJdks.size)
Assertions.assertEquals(1, allEelJdks.size)
Assertions.assertNotEquals(allLocalJdks[0], allEelJdks[0])
Assertions.assertTrue { allLocalJdks.all { it in allJdks } }
Assertions.assertTrue { allEelJdks.all { it in allJdks } }
}
}
}
@Test
@RegistryKey("ide.workspace.model.per.environment.model.separation", "true")
fun `sdks are identifiable by their name in different views`() = timeoutRunBlocking {
val globalTable = ProjectJdkTable.getInstance()
val localJdkTableView = ProjectJdkTable.getInstance(localProject.get())
val eelJdkTableView = ProjectJdkTable.getInstance(eelProject.get())
val commonName = "common sdk name"
withAddedSdk(globalTable, commonName, EnvKind.Local) {
withAddedSdk(globalTable, commonName, EnvKind.Eel) {
val localSdk = localJdkTableView.findJdk(commonName)!!
val eelSdk = eelJdkTableView.findJdk(commonName)!!
Assertions.assertEquals(localSdk.name, eelSdk.name)
Assertions.assertNotEquals(localSdk.homePath, eelSdk.homePath)
}
}
}
enum class EnvKind {
Eel,
Local
}
private suspend fun withAddedSdk(table: ProjectJdkTable, name: String, kind: EnvKind, block: suspend CoroutineScope.() -> Unit) {
coroutineScope {
val tempDir = when (kind) {
EnvKind.Eel -> EelPathUtils.createTemporaryDirectory(eelProject.get())
EnvKind.Local -> Files.createTempDirectory(name)
}
val sdk = testSdkGenerator.get().createTestSdk(SdkInfo(name, "1", tempDir.toString()))
writeAction {
table.addJdk(sdk)
}
try {
block()
}
finally {
writeAction {
table.removeJdk(sdk)
}
}
}
}
private fun getLocalSdkModel(): ProjectSdksModel {
return ProjectSdksModel().apply {
reset(localProject.get())
}
}
private fun getEelSdkModel(): ProjectSdksModel {
return ProjectSdksModel().apply {
reset(eelProject.get())
}
}
}

View File

@@ -84,6 +84,9 @@
<applicationService serviceInterface="com.intellij.openapi.projectRoots.ProjectJdkTable"
serviceImplementation="com.intellij.openapi.projectRoots.impl.ProjectJdkTableImpl"/>
<projectService serviceInterface="com.intellij.openapi.projectRoots.SdkTableProjectViewProvider"
serviceImplementation="com.intellij.openapi.projectRoots.impl.SdkTableProjectViewProviderImpl"/>
<applicationService serviceInterface="com.intellij.workspaceModel.ide.legacyBridge.GlobalSdkTableBridgeRegistry"
serviceImplementation="com.intellij.workspaceModel.ide.impl.legacyBridge.sdk.GlobalSdkTableBridgeRegistryImpl"/>
<workspace.bridgeInitializer implementation="com.intellij.workspaceModel.ide.impl.legacyBridge.sdk.GlobalSdkBridgeInitializer"/>

View File

@@ -38,6 +38,8 @@ Fa:com.intellij.openapi.module.ModuleManager
*:com.intellij.openapi.project.ProjectTypesProvider
- sf:EP_NAME:com.intellij.openapi.extensions.ExtensionPointName
- a:inferProjectTypes(com.intellij.openapi.project.Project):java.util.Collection
Fa:com.intellij.openapi.projectRoots.ProjectJdkTable
- *s:getInstance(com.intellij.openapi.project.Project):com.intellij.openapi.projectRoots.ProjectJdkTable
*:com.intellij.openapi.roots.AdditionalLibraryRootsListener
- sf:TOPIC:com.intellij.util.messages.Topic
- s:fireAdditionalLibraryChanged(com.intellij.openapi.project.Project,java.lang.String,java.util.Collection,java.util.Collection,java.lang.String):V

View File

@@ -1,9 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.openapi.projectRoots;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.concurrency.annotations.RequiresWriteLock;
import com.intellij.util.messages.Topic;
@@ -17,10 +18,21 @@ import java.util.List;
*/
@ApiStatus.NonExtendable
public abstract class ProjectJdkTable {
/**
* Retrieves an SDK table containing <i>all</i> available SDKs in the IDE. There can be several SDKs with the same name and type.
*/
public static ProjectJdkTable getInstance() {
return ApplicationManager.getApplication().getService(ProjectJdkTable.class);
}
/**
* Retrieves an SDK table relevant to the provided project. The SDKs in the provided table are unique by their name and type.
*/
@ApiStatus.Experimental
public static @NotNull ProjectJdkTable getInstance(@NotNull Project project) {
return project.getService(SdkTableProjectViewProvider.class).getSdkTableView();
}
public abstract @Nullable Sdk findJdk(@NotNull String name);
public abstract @Nullable Sdk findJdk(@NotNull String name, @NotNull String type);

View File

@@ -0,0 +1,16 @@
// 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.openapi.projectRoots;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Provides a possibility to obtain {@link ProjectJdkTable} relevant to a particular {@link Project}.
* It does not mean that the resulting {@link ProjectJdkTable}'s lifetime and storage is bound to a project;
* rather, we intend to provide a "view" of {@link ProjectJdkTable}.
*/
@ApiStatus.Internal
public interface SdkTableProjectViewProvider {
@NotNull ProjectJdkTable getSdkTableView();
}

View File

@@ -262,7 +262,7 @@ open class ProjectRootManagerImpl(
return null
}
val projectJdkTable = ProjectJdkTable.getInstance()
val projectJdkTable = ProjectJdkTable.getInstance(project)
if (projectSdkType == null) {
return projectJdkTable.findJdk(projectSdkName!!)
}