diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/SdkTableProjectViewProviderImpl.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/SdkTableProjectViewProviderImpl.kt index 178b096989a3..257c90f18276 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/SdkTableProjectViewProviderImpl.kt +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/SdkTableProjectViewProviderImpl.kt @@ -71,7 +71,7 @@ private class ProjectJdkTableProjectView(val descriptor: EelDescriptor, val dele } private fun validateDescriptor(sdk: Sdk): Boolean { - val sdkDescriptor = sdk.homePath?.let { getEelDescriptorOfHomePath(it) } + val sdkDescriptor = sdk.homePath?.let { getEffectiveWorkspaceEelDescriptorOfHomePath(it) } return if (sdkDescriptor == null) { true } diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/util.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/util.kt index 0d8605acd579..b3a83f430b93 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/util.kt +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/util.kt @@ -7,6 +7,7 @@ package com.intellij.openapi.projectRoots.impl import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.SdkTypeId +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.LocalEelMachine @@ -23,13 +24,46 @@ fun findClashingSdk(sdkName: String, sdk: Sdk): SdkEntity? { return relevantSnapshot.entities(SdkEntity::class.java).find { it.name == sdkName } } -fun getEelDescriptorOfHomePath(homePath: String): EelDescriptor = - try { +/** + * Returns the [EelDescriptor] that the Workspace Model should use for SDK entities + * associated with the given `homePath`. + * + * This helper abstracts the current "per-environment" separation of the global + * workspace model by EEL descriptors: + * + * - If the registry key `ide.workspace.model.per.environment.model.separation` is ON, + * the descriptor is inferred from `homePath` via `Path.getEelDescriptor()` and thus + * points to the machine that actually owns the path (e.g., local OS, a specific WSL + * distribution, a remote/container environment). + * - If the registry key is OFF, the local machine descriptor ([LocalEelDescriptor]) is + * always returned so that SDKs from different environments are kept together in a + * single, shared model. This escape hatch is required by IDEs that expect certain + * remote interpreters (e.g., WSL) to be visible from both local and remote projects, + * where strict separation would otherwise break discovery or reuse. + * - If `homePath` cannot be parsed into a valid [Path], the method falls back to + * [LocalEelDescriptor]. + * + * The returned descriptor determines which instance of [GlobalWorkspaceModel] will hold + * the corresponding [SdkEntity] records, preventing cross-environment linkage issues + * when separation is enabled, while preserving legacy behavior when it is disabled. + * + * @param homePath a string path used solely to infer the owning environment; it does not + * need to exist locally and is not validated beyond basic path parsing + * @return the effective environment ([EelDescriptor]) to use for workspace model operations + * @see com.intellij.workspaceModel.ide.impl.GlobalWorkspaceModel + * @see com.intellij.platform.eel.provider.getEelDescriptor + */ +fun getEffectiveWorkspaceEelDescriptorOfHomePath(homePath: String): EelDescriptor { + if (!Registry.`is`("ide.workspace.model.per.environment.model.separation")) { + return LocalEelDescriptor + } + return try { Path.of(homePath).getEelDescriptor() } catch (_: InvalidPathException) { LocalEelDescriptor } +} /** * Creates an SDK in the [GlobalWorkspaceModel] that matches the eek environment inferred @@ -61,7 +95,7 @@ fun ProjectJdkTable.createSdkForEnvironment( homePathForEnvironmentDetection: String, ): Sdk = if (this is EnvironmentScopedSdkTableOps) { - val eelDescriptor = getEelDescriptorOfHomePath(homePathForEnvironmentDetection) + val eelDescriptor = getEffectiveWorkspaceEelDescriptorOfHomePath(homePathForEnvironmentDetection) createSdk(name, sdkType, eelDescriptor) } else { diff --git a/platform/lang-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/sdk/GlobalSdkBridgesLoader.kt b/platform/lang-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/sdk/GlobalSdkBridgesLoader.kt index 5340faec5d74..41a8ebbdccc7 100644 --- a/platform/lang-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/sdk/GlobalSdkBridgesLoader.kt +++ b/platform/lang-impl/src/com/intellij/workspaceModel/ide/impl/legacyBridge/sdk/GlobalSdkBridgesLoader.kt @@ -26,13 +26,39 @@ import com.intellij.workspaceModel.ide.toPath import java.nio.file.InvalidPathException import java.util.concurrent.ConcurrentHashMap -private fun SdkEntity.getAssociatedEelDescriptor(): EelDescriptor = - try { +/** + * Resolves the effective [EelDescriptor] (environment) that the Global Workspace Model + * should associate with this [SdkEntity]. + * + * Behavior depends on the per-environment model separation flag + * (`ide.workspace.model.per.environment.model.separation`): + * + * - When separation is ON, the environment is inferred from `homePath`, so the SDK is attributed + * to the machine that actually owns the path (e.g., local OS, a particular WSL distribution, + * or a remote/container environment). + * - When separation is OFF, [LocalEelDescriptor] is always returned so SDKs from different + * environments are kept together in a single, shared global model (compatibility mode for IDEs + * that expect cross-environment SDK visibility). + * - If `homePath` is `null`, cannot be converted to a path, or throws [InvalidPathException], + * the method falls back to [LocalEelDescriptor]. + * + * The resolved descriptor is used to: + * - decide which [com.intellij.workspaceModel.ide.impl.GlobalWorkspaceModel] instance + * (per environment) should store the SDK entity; + * - filter which per-environment bridges should create/load SDK bridges, preventing unrelated + * environments from processing changes. + */ +private fun SdkEntity.getAssociatedEffectiveWorkspaceEelDescriptor(): EelDescriptor { + if (!Registry.`is`("ide.workspace.model.per.environment.model.separation")) { + return LocalEelDescriptor + } + return try { homePath?.toPath()?.getEelDescriptor() ?: LocalEelDescriptor } catch (_: InvalidPathException) { LocalEelDescriptor } +} private class GlobalSdkBridgeInitializer : BridgeInitializer { override fun isEnabled(): Boolean = true @@ -45,7 +71,7 @@ private class GlobalSdkBridgeInitializer : BridgeInitializer { val environmentName = projectEelDescriptor.machine.getInternalEnvironmentName() for (addChange in addChanges) { - if (addChange.newEntity.getAssociatedEelDescriptor() != projectEelDescriptor) continue + if (addChange.newEntity.getAssociatedEffectiveWorkspaceEelDescriptor() != projectEelDescriptor) continue // Will initialize the bridge if missing builder.mutableSdkMap.getOrPutDataByEntity(addChange.newEntity) { val sdkEntityCopy = SdkBridgeImpl.createEmptySdkEntity("", "", environmentName = environmentName) @@ -63,7 +89,7 @@ private class GlobalSdkBridgesLoader(private val eelMachine: EelMachine) : Globa val sdks = mutableStorage .entities(SdkEntity::class.java) .filter { mutableStorage.sdkMap.getDataByEntity(it) == null } - .filter { it.getAssociatedEelDescriptor().machine == eelMachine } + .filter { it.getAssociatedEffectiveWorkspaceEelDescriptor().machine == eelMachine } .map { sdkEntity -> val sdkEntityBuilder = sdkEntity.createEntityTreeCopy(false) as SdkEntityBuilder sdkEntity to ProjectJdkImpl(SdkBridgeImpl(sdkEntityBuilder, environmentName)) @@ -85,7 +111,7 @@ private class GlobalSdkBridgesLoader(private val eelMachine: EelMachine) : Globa val environmentName = eelMachine.getInternalEnvironmentName() for (addChange in addChanges) { - if (addChange.newEntity.getAssociatedEelDescriptor().machine != eelMachine) continue + if (addChange.newEntity.getAssociatedEffectiveWorkspaceEelDescriptor().machine != eelMachine) continue // Will initialize the bridge if missing builder.mutableSdkMap.getOrPutDataByEntity(addChange.newEntity) { val sdkEntityCopy = SdkBridgeImpl.createEmptySdkEntity("", "", environmentName = environmentName)