diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java index 35d52949544b..79a2cbdb4fe6 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java @@ -2,21 +2,20 @@ package com.intellij.pycharm.community.ide.impl; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.Disposable; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.ProjectJdkTable; import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.projectRoots.SdkModel; import com.intellij.openapi.roots.ModuleRootManager; -import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.CollectionComboBoxModel; import com.intellij.util.PathMappingSettings; +import com.intellij.util.messages.MessageBusConnection; import com.jetbrains.python.PyBundle; -import com.jetbrains.python.configuration.PyConfigurableInterpreterList; -import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.run.AbstractPyCommonOptionsForm; import com.jetbrains.python.run.PyCommonOptionsFormData; import com.jetbrains.python.sdk.PySdkListCellRenderer; @@ -95,11 +94,21 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm { } @Override - public void subscribe() { - PyConfigurableInterpreterList myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); - ProjectSdksModel myProjectSdksModel = myInterpreterList.getModel(); - myProjectSdksModel.addListener(new MyListener(this, myInterpreterList)); - updateSdkList(true, myInterpreterList); + public void subscribe(@NotNull Disposable parentDisposable) { + // Refresh the interpreter combo from the live SDK table whenever it changes. The connection is tied to + // `parentDisposable`, so the listener does not outlive the owning UI. + MessageBusConnection connection = myProject.getMessageBus().connect(parentDisposable); + connection.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, new ProjectJdkTable.Listener() { + @Override + public void jdkAdded(@NotNull Sdk jdk) { updateSdkList(true); } + + @Override + public void jdkRemoved(@NotNull Sdk jdk) { updateSdkList(true); } + + @Override + public void jdkNameChanged(@NotNull Sdk jdk, @NotNull String previousName) { updateSdkList(true); } + }); + updateSdkList(true); } @Override @@ -193,7 +202,7 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm { ); } - public void updateSdkList(boolean preserveSelection, PyConfigurableInterpreterList myInterpreterList) { + public void updateSdkList(boolean preserveSelection) { myPythonSdks = new ArrayList<>(PythonSdkUtil.getAllSdks()); Sdk selection = preserveSelection ? (Sdk)content.interpreterComboBox.getSelectedItem() : null; if (!myPythonSdks.contains(selection)) { @@ -319,36 +328,6 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm { myEnvPaths = strings; } - private static class MyListener implements SdkModel.Listener { - private final PyIdeCommonOptionsForm myForm; - private final PyConfigurableInterpreterList myInterpreterList; - - MyListener(PyIdeCommonOptionsForm form, PyConfigurableInterpreterList interpreterList) { - myForm = form; - myInterpreterList = interpreterList; - } - - - private void update() { - myForm.updateSdkList(true, myInterpreterList); - } - - @Override - public void sdkAdded(@NotNull Sdk sdk) { - update(); - } - - @Override - public void beforeSdkRemove(@NotNull Sdk sdk) { - update(); - } - - @Override - public void sdkChanged(@NotNull Sdk sdk, String previousName) { - update(); - } - } - @Override public String getModuleName() { Module module = getModule(); diff --git a/python/pluginCore/impl/src/com/intellij/python/community/plugin/impl/run/PyPluginCommonOptionsForm.java b/python/pluginCore/impl/src/com/intellij/python/community/plugin/impl/run/PyPluginCommonOptionsForm.java index 7f7c97feeb80..a7482c054589 100644 --- a/python/pluginCore/impl/src/com/intellij/python/community/plugin/impl/run/PyPluginCommonOptionsForm.java +++ b/python/pluginCore/impl/src/com/intellij/python/community/plugin/impl/run/PyPluginCommonOptionsForm.java @@ -1,6 +1,7 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.python.community.plugin.impl.run; +import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; import com.intellij.openapi.module.Module; @@ -89,7 +90,7 @@ public class PyPluginCommonOptionsForm implements AbstractPyCommonOptionsForm { } @Override - public void subscribe() { + public void subscribe(@NotNull Disposable parentDisposable) { } private void addInterpreterComboBoxActionListener(ActionListener listener) { diff --git a/python/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt b/python/python-sdk/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt similarity index 96% rename from python/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt rename to python/python-sdk/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt index 8aa5e4f842e4..4295469606d3 100644 --- a/python/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt +++ b/python/python-sdk/src/com/jetbrains/python/run/PythonInterpreterTargetEnvironmentFactory.kt @@ -96,7 +96,7 @@ interface PythonInterpreterTargetEnvironmentFactory : PluginAware { companion object { @JvmStatic - val EP_NAME = ExtensionPointName("Pythonid.interpreterTargetEnvironmentFactory") + val EP_NAME: ExtensionPointName = ExtensionPointName("Pythonid.interpreterTargetEnvironmentFactory") @JvmStatic fun findPythonTargetInterpreter(sdk: Sdk, project: Project): HelpersAwareTargetEnvironmentRequest = @@ -137,7 +137,7 @@ interface PythonInterpreterTargetEnvironmentFactory : PluginAware { * Target provides access to its filesystem using VFS (like WSL) */ @JvmStatic - fun getTargetWithMappedLocalVfs(targetEnvironmentConfiguration: TargetEnvironmentConfiguration) = EP_NAME.extensionList.asSequence().mapNotNull { + fun getTargetWithMappedLocalVfs(targetEnvironmentConfiguration: TargetEnvironmentConfiguration): TargetWithMappedLocalVfs? = EP_NAME.extensionList.asSequence().mapNotNull { it.asTargetWithMappedLocalVfs(targetEnvironmentConfiguration) }.firstOrNull() diff --git a/python/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt b/python/python-sdk/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt similarity index 86% rename from python/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt rename to python/python-sdk/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt index 3e842aaedae6..60bc41204cf9 100644 --- a/python/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt +++ b/python/python-sdk/src/com/jetbrains/python/run/TargetConfigurationWithLocalFsAccessEx.kt @@ -14,7 +14,7 @@ import org.jetbrains.annotations.ApiStatus * ``\\wsl$`` projects allow WSL and Docker */ @ApiStatus.Internal -internal fun TargetConfigurationWithLocalFsAccess.allowCreationTargetOfThisType(confType: TargetEnvironmentType<*>): Boolean { +fun TargetConfigurationWithLocalFsAccess.allowCreationTargetOfThisType(confType: TargetEnvironmentType<*>): Boolean { val javaClass = asTargetConfig.getTargetType().javaClass return javaClass == confType.javaClass || javaClass in confType.canProbablyRunCodeForeignTypes } @@ -25,7 +25,7 @@ internal fun TargetConfigurationWithLocalFsAccess.allowCreationTargetOfThisType( * ``\\wsl$`` projects allow Docker AND only WSL with right distro */ @ApiStatus.Internal -internal fun TargetConfigurationWithLocalFsAccess.codeCouldProbablyBeRunWithConfig(config: TargetEnvironmentConfiguration?): Boolean { +fun TargetConfigurationWithLocalFsAccess.codeCouldProbablyBeRunWithConfig(config: TargetEnvironmentConfiguration?): Boolean { if (config == null) return false // For now no local target could run remote if (asTargetConfig == config) return true // Same config (like same wsl distro) return asTargetConfig.getTargetType().javaClass in config.getTargetType().canProbablyRunCodeForeignTypes diff --git a/python/src/com/jetbrains/python/run/target/HelpersAwareLocalTargetEnvironmentRequest.kt b/python/python-sdk/src/com/jetbrains/python/run/target/HelpersAwareLocalTargetEnvironmentRequest.kt similarity index 100% rename from python/src/com/jetbrains/python/run/target/HelpersAwareLocalTargetEnvironmentRequest.kt rename to python/python-sdk/src/com/jetbrains/python/run/target/HelpersAwareLocalTargetEnvironmentRequest.kt diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/ProjectExt.kt b/python/python-sdk/src/com/jetbrains/python/sdk/ProjectExt.kt index 61a104be70c8..02b138cbc6f4 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/ProjectExt.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/ProjectExt.kt @@ -1,17 +1,29 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk +import com.intellij.execution.target.TargetBasedSdkAdditionalData +import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ModuleRootModificationUtil import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.platform.eel.EelMachine +import com.intellij.platform.eel.provider.LocalEelMachine +import com.intellij.platform.eel.provider.getEelMachine +import com.intellij.platform.eel.provider.ownsPath import com.intellij.util.concurrency.annotations.RequiresWriteLock import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory +import com.jetbrains.python.run.codeCouldProbablyBeRunWithConfig import com.jetbrains.python.sdk.impl.PySdkBundle.message +import com.jetbrains.python.sdk.legacy.PythonSdkUtil import org.jetbrains.annotations.ApiStatus.Internal +import java.nio.file.InvalidPathException +import java.nio.file.Path /** * Renames the SDK currently registered as [oldName] to [newName] and keeps this project's references pointing at it. @@ -72,3 +84,42 @@ fun Project.renameSdk(oldName: String, newName: String): PyResult { } return Result.success(Unit) } + +/** + * Returns all Python SDKs registered in the IDE that are usable from this project, optionally restricted to the target + * the given [module] resides on. Remote interpreters are sorted last, then by name. + */ +@Internal +fun Project.getAssignablePythonSdks(module: Module?): List = filterAssignablePythonSdks(PythonSdkUtil.getAllSdks(), module) + +/** + * Filters and sorts [sdks] the same way [getAssignablePythonSdks] does. The "Python Interpreters" dialog passes the editable + * copies from its own `ProjectSdksModel` here, so the displayed list matches the live one. + */ +@Internal +fun Project.filterAssignablePythonSdks(sdks: Collection, module: Module?): List { + val eelMachine = getEelMachine() + val targetModuleSitsOn = module?.let { PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(it) } + return sdks + .filter { sdk -> + PythonSdkUtil.isPythonSdk(sdk) && + sdkMatchesEel(eelMachine, sdk) && + (targetModuleSitsOn == null || targetModuleSitsOn.codeCouldProbablyBeRunWithConfig(sdk.targetEnvConfiguration)) + } + .sortedWith(compareBy({ PythonSdkUtil.isRemote(it) }, { it.name })) +} + +/** + * Mirrors `ProjectSdksModel.sdkMatchesEel` (which lives in lang-impl and is unavailable here): target-based SDKs are + * always eligible; other SDKs must have a home path owned by the project's [eelMachine]. + */ +private fun sdkMatchesEel(eelMachine: EelMachine, sdk: Sdk): Boolean { + if (sdk.sdkAdditionalData is TargetBasedSdkAdditionalData) return true + val home = sdk.homePath ?: return false + return try { + eelMachine.ownsPath(Path.of(home)) + } + catch (_: InvalidPathException) { + eelMachine == LocalEelMachine + } +} diff --git a/python/src/com/jetbrains/python/target/targetWithVfs/TargetWithMappedLocalVfs.kt b/python/python-sdk/src/com/jetbrains/python/target/targetWithVfs/TargetWithMappedLocalVfs.kt similarity index 100% rename from python/src/com/jetbrains/python/target/targetWithVfs/TargetWithMappedLocalVfs.kt rename to python/python-sdk/src/com/jetbrains/python/target/targetWithVfs/TargetWithMappedLocalVfs.kt diff --git a/python/src/com/jetbrains/python/conda/sdk/configuration/PyEnvironmentYmlSdkConfiguration.kt b/python/src/com/jetbrains/python/conda/sdk/configuration/PyEnvironmentYmlSdkConfiguration.kt index f70f0ff75cbe..fb11c9766ab1 100644 --- a/python/src/com/jetbrains/python/conda/sdk/configuration/PyEnvironmentYmlSdkConfiguration.kt +++ b/python/src/com/jetbrains/python/conda/sdk/configuration/PyEnvironmentYmlSdkConfiguration.kt @@ -18,7 +18,6 @@ import com.intellij.util.FileName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonBinary -import com.jetbrains.python.configuration.PyConfigurableInterpreterList import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.getOrNull import com.jetbrains.python.onSuccess @@ -48,6 +47,7 @@ import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity +import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.setAssociationToModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -145,7 +145,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten return PyCondaCommand(condaExecutable.path.pathString, null).createCondaSdkFromExistingEnvironment( getCondaEnvIdentity(module, condaExecutable) ?: return PyResult.localizedError(PyBundle.message("sdk.cannot.use.existing.conda.environment")), - PyConfigurableInterpreterList.getInstance(module.project).model.sdks.toList(), + PythonSdkUtil.getAllSdks(), ) } @@ -173,7 +173,7 @@ internal class PyEnvironmentYmlSdkConfiguration : PyProjectSdkConfigurationExten val binaryToExec = BinOnEel(condaExecutable.path) val existingEnvs = PyCondaEnv.getEnvs(binaryToExec, forceRefresh = true).getOrNull() ?: emptyList() - val existingSdks = PyConfigurableInterpreterList.getInstance(project).model.sdks + val existingSdks = PythonSdkUtil.getAllSdks() val newCondaEnvInfo = NewCondaEnvRequest.LocalEnvByLocalEnvironmentFile(environmentYml.toNioPath(), existingEnvs) val sdk = PyCondaCommand(condaExecutable.path.pathString, null) .createCondaSdkAlongWithNewEnv(newCondaEnvInfo, existingSdks.toList()).getOr { diff --git a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java index 1b277fef14e6..3fbcbdb40b33 100644 --- a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java @@ -12,7 +12,6 @@ import com.intellij.openapi.options.UnnamedConfigurable; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ProjectRootManager; -import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.ListPopup; @@ -40,6 +39,7 @@ import com.jetbrains.python.sdk.PyTransferredSdkRootsKt; import com.jetbrains.python.sdk.PythonSdkType; import com.jetbrains.python.sdk.SdkExtKt; import com.jetbrains.python.sdk.legacy.PythonSdkUtil; +import com.jetbrains.python.sdk.ProjectExtKt; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -68,10 +68,6 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { private final @Nullable Module myModule; - private final @NotNull PyConfigurableInterpreterList myInterpreterList; - - private final @NotNull ProjectSdksModel myProjectSdksModel; - private final @NotNull JPanel myMainPanel; private final @NotNull ComboBox mySdkCombo; @@ -114,9 +110,6 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { packagesNotificationPanel, customizer); - myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); - myProjectSdksModel = myInterpreterList.getModel(); - // Reflect the SDK configuration mutex reactively: disable the controls only while the lock is // held and refresh the combo once it is released, since a background configuration may have // created or changed the interpreter in the meantime. @@ -196,7 +189,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { // do not use `getOriginalSelectedSdk()` here since `model` won't find original sdk for selected item due to applying final Sdk currentSelectedSdk = getEditableSelectedSdk(); - if (currentSelectedSdk != null && myProjectSdksModel.findSdk(currentSelectedSdk.getName()) != null) { + if (currentSelectedSdk != null && getEditableSdkUsingOriginal(currentSelectedSdk) != null) { // nothing has been selected but previously selected sdk still exists, stay with it updateSdkListAndSelect(currentSelectedSdk); } @@ -219,8 +212,8 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { @Nullable private Sdk getOriginalSelectedSdk() { - final Sdk editableSdk = getEditableSelectedSdk(); - return editableSdk == null ? null : myProjectSdksModel.findSdk(editableSdk); + // The combo holds the real SDKs from the live table, so the selected item is already the original. + return getEditableSelectedSdk(); } @Nullable @@ -271,17 +264,13 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { @Override public final void reset() { - // The SDK model is cached per project (see PyConfigurableInterpreterList) and may have been - // populated before a new interpreter was created — e.g. by the interpreter widget popup, which - // builds its list from the same model. Refresh it from the live SDK table so a just-created - // interpreter is present and selectable instead of the combo showing "". - myProjectSdksModel.reset(myProject); + // The combo reads the live SDK table, so a just-created interpreter is present without any extra refresh. updateSdkListAndSelect(getSdk()); } @NotNull private List getAvailableSdks() { - return myInterpreterList.getAllPythonSdks(myModule); + return ProjectExtKt.getAssignablePythonSdks(myProject, myModule); } private void updateSdkListAndSelect(@Nullable Sdk selectedSdk) { @@ -317,12 +306,16 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { @Nullable private Sdk getEditableSdkUsingOriginal(@Nullable Sdk sdk) { - return sdk == null ? null : myProjectSdksModel.findSdk(sdk.getName()); + if (sdk == null) return null; + // The SDK may come from the "Python Interpreters" dialog as an editable copy; match the live table SDK by name. + for (Sdk available : getAvailableSdks()) { + if (available.getName().equals(sdk.getName())) return available; + } + return null; } @Override public final void disposeUIResources() { - myInterpreterList.disposeModel(); if (myDisposable != null) { Disposer.dispose(myDisposable); } diff --git a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java index 22fbde840233..e1734d2b9603 100644 --- a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java +++ b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java @@ -1,101 +1,42 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.configuration; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel; -import com.intellij.openapi.util.Comparing; -import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory; -import com.jetbrains.python.run.TargetConfigurationWithLocalFsAccessExKt; -import com.jetbrains.python.sdk.SdkExtKt; -import com.jetbrains.python.sdk.legacy.PythonSdkUtil; -import org.jetbrains.annotations.ApiStatus; +import com.jetbrains.python.sdk.ProjectExtKt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import static com.jetbrains.python.sdk.legacy.PythonSdkUtil.isRemote; - /** - * Manages the SDK model shared between PythonSdkConfigurable and PyActiveSdkConfigurable. + * @deprecated The cached SDK model was removed; query the live SDK table via + * {@link ProjectExtKt#getAssignablePythonSdks(Project, com.intellij.openapi.module.Module)} instead. Retained for + * external plugins that still reference this service. */ -@Service(Service.Level.PROJECT) +@Deprecated public final class PyConfigurableInterpreterList { - private ProjectSdksModel myModel; + private final @NotNull Project myProject; + private PyConfigurableInterpreterList(@NotNull Project project) { + myProject = project; + } + + /** + * @deprecated Obtain interpreters via {@link ProjectExtKt#getAssignablePythonSdks(Project, com.intellij.openapi.module.Module)}. + */ + @Deprecated public static PyConfigurableInterpreterList getInstance(@Nullable Project project) { final Project effectiveProject = project != null ? project : ProjectManager.getInstance().getDefaultProject(); - final PyConfigurableInterpreterList instance = effectiveProject.getService(PyConfigurableInterpreterList.class); - if (effectiveProject != project) { - instance.disposeModel(); - } - return instance; - } - - public ProjectSdksModel getModel() { - if (myModel == null) { - myModel = new ProjectSdksModel(); - myModel.reset(null); - } - return myModel; - } - - public void disposeModel() { - if (myModel != null) { - myModel.disposeUIResources(); - myModel = null; - } + return new PyConfigurableInterpreterList(effectiveProject); } /** - * @param module if not null and module resides on certain target, returns only SDKs for this target - */ - @ApiStatus.Internal - public @NotNull List getAllPythonSdks(@Nullable Module module) { - var targetModuleSitsOn = (module != null) - ? PythonInterpreterTargetEnvironmentFactory.Companion.getTargetModuleResidesOn(module) - : null; - - List result = new ArrayList<>(); - for (Sdk sdk : getModel().getSdks()) { - if (!PythonSdkUtil.isPythonSdk(sdk)) continue; - if (targetModuleSitsOn != null) { - var sdkConfig = SdkExtKt.getTargetEnvConfiguration(sdk); - if (!TargetConfigurationWithLocalFsAccessExKt.codeCouldProbablyBeRunWithConfig(targetModuleSitsOn, sdkConfig)) { - continue; - } - } - result.add(sdk); - } - result.sort(new PyInterpreterComparator()); - return result; - } - - /** - * Returns all Python SDKs visible across the IDE. - * - * @deprecated The global SDK table is being replaced with per-project SDK visibility. + * @deprecated Use {@link ProjectExtKt#getAssignablePythonSdks(Project, com.intellij.openapi.module.Module)} with a {@code null} module. */ @Deprecated public @NotNull List getAllPythonSdks() { - return getAllPythonSdks(null); - } - - private static class PyInterpreterComparator implements Comparator { - @Override - public int compare(@NotNull Sdk o1, Sdk o2) { - // Remote SDKs last - final boolean isRemote1 = isRemote(o1); - final boolean isRemote2 = isRemote(o2); - if (isRemote1 != isRemote2) return isRemote1 ? 1 : -1; - - return Comparing.compare(o1.getName(), o2.getName()); - } + return ProjectExtKt.getAssignablePythonSdks(myProject, null); } } diff --git a/python/src/com/jetbrains/python/configuration/PythonInterpreterConfigurable.kt b/python/src/com/jetbrains/python/configuration/PythonInterpreterConfigurable.kt index 0cca53442e1e..b15edaf2da6d 100644 --- a/python/src/com/jetbrains/python/configuration/PythonInterpreterConfigurable.kt +++ b/python/src/com/jetbrains/python/configuration/PythonInterpreterConfigurable.kt @@ -56,15 +56,17 @@ internal class PythonInterpreterConfigurable(moduleOrProject: ModuleOrProject) : val configurable = PythonInterpreterConfigurable(if (module != null) ModuleAndProject(module) else ProjectOnly(project)) // `ShowSettingsUtil.editConfigurable()` with `advancedInitialization` parameter could be possibly also used here val dialogWrapper = SettingsDialogFactory.getInstance().create(project, DIMENSION_KEY, configurable, true, false) - // select project Python interpreter - configurable.masterDetailsComponent.selectNodeInTree(initiallySelectedSdk) + // Select the project Python interpreter. The dialog owns its model, which `create()` has just reloaded from the + // live SDK table; re-resolve the caller's `initiallySelectedSdk` against it by name because the model holds + // editable copies, not the caller's instance. + val sdkToSelect = initiallySelectedSdk?.let { configurable.masterDetailsComponent.projectSdksModel.findSdk(it.name) ?: it } + configurable.masterDetailsComponent.selectNodeInTree(sdkToSelect) val isOKClicked = dialogWrapper.showAndGet() // note that clicking "Apply" button and then "Cancel" results in `false` value of `isOKClicked` return if (isOKClicked) { configurable.masterDetailsComponent.storedSelectedSdk } else { - configurable.masterDetailsComponent.projectSdksModel.reset(project) null } } diff --git a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt index abf3af674ecd..2291546815f1 100644 --- a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt +++ b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt @@ -18,6 +18,7 @@ import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel import com.intellij.openapi.ui.InputValidatorEx import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.ui.Messages @@ -32,6 +33,7 @@ import com.jetbrains.python.sdk.ModuleOrProject.ProjectOnly import com.jetbrains.python.sdk.PythonSdkUpdater import com.jetbrains.python.sdk.collectAddInterpreterActions import com.jetbrains.python.sdk.customizeWithSdkValue +import com.jetbrains.python.sdk.filterAssignablePythonSdks import com.jetbrains.python.sdk.isAssociatedWithAnotherModule import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.noInterpreterMarker @@ -48,8 +50,6 @@ import javax.swing.tree.TreePath * the list. */ internal class PythonInterpreterMasterDetails(private val moduleOrProject: ModuleOrProject, private val parentConfigurable: Configurable) : MasterDetailsComponent() { - private val pythonConfigurableInterpreterList: PyConfigurableInterpreterList = PyConfigurableInterpreterList.getInstance(moduleOrProject.project) - private val project = moduleOrProject.project // Temporary hack as lots of legacy code accept nullable module. Remove after legacy code migrated to ModuleOrProject. @@ -58,7 +58,9 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul is ProjectOnly -> null } - internal val projectSdksModel = pythonConfigurableInterpreterList.model + // The dialog owns a short-lived editable SDK model instead of sharing a cached project-level one. It is reloaded + // from the live SDK table in `reset()` and disposed in `disposeUIResources()` (PY-90077). + internal val projectSdksModel = ProjectSdksModel() /** * Indicates whether Python paths of one or more interpreters have been changed by user via Python Paths dialog. @@ -116,11 +118,15 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul } private val allPythonSdksInEdit: List - get() = pythonConfigurableInterpreterList.getAllPythonSdks(module) + get() = project.filterAssignablePythonSdks(projectSdksModel.sdks.toList(), module) override fun reset() { pythonPathsModified = false + // Reload the editable model from the live SDK table so interpreters added elsewhere (e.g. the "Add Interpreter" + // link or the status-bar widget) appear immediately; the tree is then built from these editable copies. + projectSdksModel.reset(project) + myRoot.removeAllChildren() val visiblePythonSdks = when { @@ -152,6 +158,11 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul projectSdksModel.apply(this) } + override fun disposeUIResources() { + super.disposeUIResources() + projectSdksModel.disposeUIResources() + } + override fun createActions(fromPopup: Boolean): List = if (fromPopup) { listOf(RemoveAction(), RenameAction(), ShowPathsAction()) diff --git a/python/src/com/jetbrains/python/console/PyConsoleOptionsConfigurable.kt b/python/src/com/jetbrains/python/console/PyConsoleOptionsConfigurable.kt index e41ec6a08479..e433072b6750 100644 --- a/python/src/com/jetbrains/python/console/PyConsoleOptionsConfigurable.kt +++ b/python/src/com/jetbrains/python/console/PyConsoleOptionsConfigurable.kt @@ -122,6 +122,7 @@ class PyConsoleOptionsConfigurable(private val myProject: Project) : SearchableC override fun isModified(): Boolean = panel.isModified override fun apply() = panel.apply() override fun reset() = panel.reset() + override fun disposeUIResources() = panel.disposeUIResources() } } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/console/PyConsoleSpecificOptionsPanel.kt b/python/src/com/jetbrains/python/console/PyConsoleSpecificOptionsPanel.kt index 70d05207bc12..6f9c417825e0 100644 --- a/python/src/com/jetbrains/python/console/PyConsoleSpecificOptionsPanel.kt +++ b/python/src/com/jetbrains/python/console/PyConsoleSpecificOptionsPanel.kt @@ -5,6 +5,8 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer import com.intellij.openapi.ui.DialogPanel import com.intellij.psi.PsiDocumentManager import com.intellij.ui.EditorTextField @@ -27,10 +29,13 @@ internal class PyConsoleSpecificOptionsPanel(private val project: Project) { private lateinit var editorTextField: EditorTextField private lateinit var commonOptionsForm: AbstractPyCommonOptionsForm + // Owns the SDK-table subscription set up by `commonOptionsForm.subscribe`; disposed in `disposeUIResources`. + private val uiDisposable: Disposable = Disposer.newDisposable() + fun createPanel(optionsProvider: PyConsoleOptions.PyConsoleSettings): JComponent { consoleSettings = optionsProvider commonOptionsForm = PyCommonOptionsFormFactory.getInstance().createForm(createCommonOptionsFormData(project)).also { - it.subscribe() + it.subscribe(uiDisposable) } editorTextField = createEditorTextField(optionsProvider.myCustomStartScript) @@ -50,6 +55,10 @@ internal class PyConsoleSpecificOptionsPanel(private val project: Project) { consoleSettings.reset(project, commonOptionsForm) } + fun disposeUIResources() { + Disposer.dispose(uiDisposable) + } + private fun createPanel(): DialogPanel { return panel { row { diff --git a/python/src/com/jetbrains/python/run/AbstractPyCommonOptionsForm.java b/python/src/com/jetbrains/python/run/AbstractPyCommonOptionsForm.java index 1ba4e827b720..a284e36a3448 100644 --- a/python/src/com/jetbrains/python/run/AbstractPyCommonOptionsForm.java +++ b/python/src/com/jetbrains/python/run/AbstractPyCommonOptionsForm.java @@ -1,8 +1,10 @@ // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.python.run; +import com.intellij.openapi.Disposable; import com.intellij.ui.PanelWithAnchor; import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; import javax.swing.JComponent; import java.util.function.Consumer; @@ -13,7 +15,11 @@ public interface AbstractPyCommonOptionsForm extends AbstractPythonRunConfigurat JComponent getMainPanel(); - void subscribe(); + /** + * Subscribes to live SDK-table changes to keep the interpreter list up to date. The subscription is released when + * {@code parentDisposable} is disposed. + */ + void subscribe(@NotNull Disposable parentDisposable); void addInterpreterModeListener(Consumer listener); } diff --git a/python/src/com/jetbrains/python/sdk/PySdkPopupFactory.kt b/python/src/com/jetbrains/python/sdk/PySdkPopupFactory.kt index ce747a031764..c8686cb97006 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkPopupFactory.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkPopupFactory.kt @@ -18,7 +18,6 @@ import com.intellij.openapi.wm.ToolWindowManager import com.intellij.util.SlowOperations import com.intellij.util.ui.SwingHelper import com.jetbrains.python.PyBundle -import com.jetbrains.python.configuration.PyConfigurableInterpreterList import com.jetbrains.python.inspections.interpreter.InterpreterSettingsQuickFix import com.jetbrains.python.psi.LanguageLevel import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory @@ -53,9 +52,8 @@ class PySdkPopupFactory(val module: Module) { fun createPopup(context: DataContext): ListPopup { val group = DefaultActionGroup() - val interpreterList = PyConfigurableInterpreterList.getInstance(module.project) val moduleSdksByTypes = SlowOperations.knownIssue("PY-76167").use { - groupModuleSdksByTypes(interpreterList.getAllPythonSdks(module), module) { + groupModuleSdksByTypes(module.project.getAssignablePythonSdks(module), module) { !it.isSdkSeemsValid || !LanguageLevel.SUPPORTED_LEVELS.contains(PythonSdkType.getLanguageLevelForSdk(it)) } }