diff --git a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitchHandler.kt b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitchHandler.kt index b1ad77a1c026..af5bde6e51c8 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitchHandler.kt +++ b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitchHandler.kt @@ -25,6 +25,8 @@ interface PyDebuggerBackendSwitchHandler { /** * Controls visibility of the backend switcher UI. * Return false to hide the switcher (e.g., when the currently open file uses a remote kernel). + * This is one of two gates: returning true does not show the switcher by itself, since it is also + * hidden on Debug tool window tabs that do not run a Python or Jupyter session. */ fun shouldShowSwitcher(project: Project): Boolean = true diff --git a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt index b3df875495a7..cf4a663cc7b2 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt +++ b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.ex.ComboBoxAction import com.intellij.openapi.components.service @@ -47,9 +48,11 @@ import com.intellij.util.ui.JBUI import com.jetbrains.python.sdk.findPythonSdk import kotlinx.coroutines.launch import com.intellij.openapi.projectRoots.Sdk +import com.intellij.platform.debugger.impl.ui.XDebuggerEntityConverter import com.intellij.xdebugger.XDebugSession import com.intellij.xdebugger.XDebugSessionListener import com.intellij.xdebugger.XDebuggerManager +import com.intellij.xdebugger.impl.ui.DebuggerUIUtil import com.jetbrains.python.icons.PythonIcons import org.jetbrains.annotations.ApiStatus import java.lang.ref.WeakReference @@ -103,12 +106,12 @@ internal class PyDebuggerBackendSwitcherAction : ComboBoxAction(), DumbAware { return } - if (!PyDebuggerBackendSwitcherVisibilityPin.isPinned(project)) { - val handlers = PyDebuggerBackendSwitchHandler.EP_NAME.extensionList - if (isPythonDapPluginInstalledAndEnabled() && handlers.all { !it.shouldShowSwitcher(project) }) { - e.presentation.isEnabledAndVisible = false - return - } + if (!isPyDebuggerBackendSwitcherVisible(project, e)) { + // Only the toolbar presence is in question here, so the action stays enabled: invoking it by id + // keeps working, which is how the backend is chosen before the first session exists. + e.presentation.isEnabled = true + e.presentation.isVisible = false + return } e.presentation.isEnabledAndVisible = true @@ -254,6 +257,39 @@ internal class PyDebuggerBackendSwitcherAction : ComboBoxAction(), DumbAware { } +/** + * Whether the backend switcher belongs on the Debug tool window tab the [e] context points at. + * + * The switcher lives in the tool window header, which is shared by every tab of every product that + * bundles the Python plugin, so it has to opt in per tab rather than default to visible: a C++ or a + * JavaScript session must not be told which Python debugger backend it runs on (CPP-51572). + */ +@ApiStatus.Internal +fun isPyDebuggerBackendSwitcherVisible(project: Project, e: AnActionEvent): Boolean { + if (PyDebuggerBackendSwitcherVisibilityPin.isPinned(project)) return true + + // While the PythonDAP plugin is installed, the handlers report whether the current editor and run + // configuration are Python at all, and a unanimous no hides the switcher. That is what keeps it off a + // notebook backed by a remote kernel. + val handlers = PyDebuggerBackendSwitchHandler.EP_NAME.extensionList + if (isPythonDapPluginInstalledAndEnabled() && handlers.all { !it.shouldShowSwitcher(project) }) return false + + // Neither the pin nor the handlers look at the Debug tool window, which is why a C++ or a JavaScript tab + // kept the switcher. It belongs on Python and Jupyter tabs only, whether or not the plugin is installed: + // without it the switcher is the promotion surface for debugpy, and those tabs are where it belongs. + return isPythonDebugTab(e) +} + +private fun isPythonDebugTab(e: AnActionEvent): Boolean { + // The header toolbar takes its data context from the selected tab, but a tab only publishes a run + // profile when it was started from an execution environment, which a Jupyter or console session + // was not. Those are recognized through the session instead: they run a pydevd-backed process. + if (e.getData(LangDataKeys.RUN_PROFILE) is AbstractPythonRunConfiguration<*>) return true + val sessionProxy = DebuggerUIUtil.getSessionProxy(e) ?: return false + val session = XDebuggerEntityConverter.getSession(sessionProxy) ?: return false + return session.runProfile is AbstractPythonRunConfiguration<*> || session.debugProcess is PyDebugProcess +} + /** * Returns the SDK of the currently selected run configuration, falling back to the project SDK. */