mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
intellij-monorepo-bot
parent
b7a251e7d8
commit
f23b2dea40
+19
-40
@@ -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();
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user