PY-90077 show newly added interpreter in "Show All" without reopening

The interpreter list was served from a cached ProjectSdksModel held by the PyConfigurableInterpreterList project service, refreshed only on explicit reset(). Adding an interpreter writes straight to ProjectJdkTable, so the cache went stale and the new interpreter appeared only on the second opening of the dialog.

Read the live SDK table instead: the combo and widget query ProjectJdkTable directly, and the "Show All" dialog now owns a short-lived ProjectSdksModel reset on open. The logic moves to Project.getAssignablePythonSdks / filterAssignablePythonSdks extensions in python-sdk; PythonInterpreterTargetEnvironmentFactory and its target helpers move down to python-sdk so the filtering can live there (packages unchanged). PyConfigurableInterpreterList is kept as a deprecated shim delegating to the new API, since external plugins still reference getInstance()/getAllPythonSdks().

Also tie the run-config common-options form SDK-table listener to a parentDisposable, fixing a pre-existing listener leak.


(cherry picked from commit d20a1a1b7c2af46f8d44a10a7824c86a32698c1b)

IJ-MR-210000

GitOrigin-RevId: 77dc194b005e8b295081e424e3a3234ccf4d5ff2
This commit is contained in:
Vitaly Legchilkin
2026-06-24 16:02:17 +00:00
committed by intellij-monorepo-bot
parent b7a251e7d8
commit f23b2dea40
16 changed files with 148 additions and 156 deletions
@@ -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();
@@ -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) {
@@ -96,7 +96,7 @@ interface PythonInterpreterTargetEnvironmentFactory : PluginAware {
companion object {
@JvmStatic
val EP_NAME = ExtensionPointName<PythonInterpreterTargetEnvironmentFactory>("Pythonid.interpreterTargetEnvironmentFactory")
val EP_NAME: ExtensionPointName<PythonInterpreterTargetEnvironmentFactory> = 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()
@@ -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
@@ -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<Unit> {
}
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<Sdk> = 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<Sdk>, module: Module?): List<Sdk> {
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
}
}
@@ -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 {
@@ -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<Object> 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 "<No interpreter>".
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<Sdk> 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);
}
@@ -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<Sdk> getAllPythonSdks(@Nullable Module module) {
var targetModuleSitsOn = (module != null)
? PythonInterpreterTargetEnvironmentFactory.Companion.getTargetModuleResidesOn(module)
: null;
List<Sdk> 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<Sdk> getAllPythonSdks() {
return getAllPythonSdks(null);
}
private static class PyInterpreterComparator implements Comparator<Sdk> {
@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);
}
}
@@ -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
}
}
@@ -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<Sdk>
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<AnAction> =
if (fromPopup) {
listOf(RemoveAction(), RenameAction(), ShowPathsAction())
@@ -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()
}
}
}
@@ -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 {
@@ -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<Boolean> listener);
}
@@ -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))
}
}