diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index a6eebca1a7b3..3b5e11fedc00 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -935,6 +935,7 @@ debugger.backend.debugpy.disabled.tooltip=debugpy requires Python 3.9 or later debugger.backend.debugpy.disabled.remote.tooltip=debugpy is not supported for remote Python interpreters debugger.backend.debugpy.disabled.not.installed.tooltip=Install the Python DAP Debugger plugin to use the debugpy backend debugger.backend.install.debugpy.plugin=Install Python DAP Debugger Plugin +debugger.backend.enable.debugpy.plugin=Enable Python DAP Debugger Plugin debugger.backend.report.issue.debugpy=Report an Issue with debugpy debugger.backend.switch.after.install.title=Switch to debugpy debugger.backend.switch.after.install.message=The Python DAP Debugger plugin has been installed. Would you like to switch to the debugpy backend and restart your current debug sessions? @@ -1620,6 +1621,7 @@ advertiser.package.supported.by.pro=Maximize your efficiency with {0} advertiser.code.cells.supported.by.pro=Code cells are supported by PyCharm advertiser.python.dap.plugin=The Python DAP Debugger plugin is available for faster and more reliable Python debugging with the Debug Adapter Protocol advertiser.python.dap.plugin.install.text=Install Plugin +advertiser.python.dap.plugin.enable.text=Enable Plugin debugger.dap.plugin.install.got.it.header=Try the Python DAP Debugger plugin debugger.dap.plugin.install.got.it.text=Install the Python DAP Debugger plugin for faster and more reliable Python debugging with debugpy diff --git a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt index 46a500539ab5..a974244339e6 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt +++ b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendSwitcherAction.kt @@ -5,6 +5,7 @@ import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil import com.intellij.ide.actions.ReportFeedbackService import com.intellij.ide.actions.SendFeedbackAction +import com.intellij.ide.plugins.PluginEnabler import com.intellij.ide.plugins.PluginManager import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -198,6 +199,15 @@ internal class PyDebuggerBackendSwitcherAction : ComboBoxAction(), DumbAware { override fun update(e: AnActionEvent) { e.presentation.isEnabledAndVisible = !isPythonDapPluginInstalledAndEnabled() + val enableOnly = isPythonDapPluginInstalledButDisabled() + e.presentation.text = if (enableOnly) { + PyBundle.message("debugger.backend.enable.debugpy.plugin") + } + else { + PyBundle.message("debugger.backend.install.debugpy.plugin") + } + // The plugin is already on disk in the enable case, so nothing is downloaded and the badge would mislead. + e.presentation.icon = if (enableOnly) null else AllIcons.Actions.Download } override fun actionPerformed(e: AnActionEvent) { @@ -253,7 +263,15 @@ private fun getEffectiveSdk(project: Project): Sdk? = @ApiStatus.Internal fun installPythonDapPlugin(project: Project, onLoaded: Runnable? = null) { project.service().listen(onLoaded) - installAndEnable(project, setOf(PluginId.getId(PYTHON_DAP_PLUGIN_ID)), showDialog = true) {} + + val pluginId = PluginId.getId(PYTHON_DAP_PLUGIN_ID) + val descriptor = PluginManagerCore.getPlugin(pluginId) + if (descriptor != null && PluginManagerCore.isDisabled(pluginId)) { + PluginEnabler.getInstance().enable(listOf(descriptor)) + return + } + + installAndEnable(project, setOf(pluginId), showDialog = true) {} } // This method exists to provide backward compatibility with older versions of the DAP plugin @@ -323,6 +341,11 @@ internal fun isPythonDapPluginInstalledAndEnabled(): Boolean { return PluginManager.isPluginInstalled(pluginId) && !PluginManagerCore.isDisabled(pluginId) } +internal fun isPythonDapPluginInstalledButDisabled(): Boolean { + val pluginId = PluginId.getId(PYTHON_DAP_PLUGIN_ID) + return PluginManager.isPluginInstalled(pluginId) && PluginManagerCore.isDisabled(pluginId) +} + private fun switchBackend(project: Project, backend: PyDebuggerBackend) { val fromBackend = PyDebuggerOptionsProvider.getInstance(project).selectedBackend if (fromBackend == backend) return diff --git a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendToolWindowSetup.kt b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendToolWindowSetup.kt index e3d37a6a8d81..7162c875b78b 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebuggerBackendToolWindowSetup.kt +++ b/python/src/com/jetbrains/python/debugger/PyDebuggerBackendToolWindowSetup.kt @@ -2,6 +2,8 @@ package com.jetbrains.python.debugger import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionStubBase +import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.Separator import com.intellij.openapi.application.EDT @@ -17,6 +19,7 @@ import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.intellij.lang.annotations.Language internal class PyDebuggerBackendToolWindowSetup : ProjectActivity { override suspend fun execute(project: Project) { @@ -43,12 +46,25 @@ internal class PyDebuggerBackendToolWindowSetup : ProjectActivity { @RequiresEdt private fun setupTitleAction(toolWindow: ToolWindow) { - val action = ActionManager.getInstance().getAction("Python.DebuggerBackendSwitcher") ?: return + val action = ActionManager.getInstance().getAction(SWITCHER_ACTION_ID) ?: return val existing = ((toolWindow as? ToolWindowEx)?.decorator as? InternalDecoratorImpl)?.headerToolbarActions ?.let { group -> (group as? DefaultActionGroup)?.getChildActionsOrStubs()?.filter { it !is Separator } } ?: emptyList() - if (action !in existing) { - toolWindow.setTitleActions(existing + action) + // The header can hold the switcher either as the action itself or as an unresolved stub, and a + // dynamic plugin reload replaces the cached instance. Matching by identity therefore misses the + // entry that is already there and appends a second switcher, so drop every copy by id first. + val desired = existing.filterNot { it.isBackendSwitcher() } + action + if (desired != existing) { + toolWindow.setTitleActions(desired) } } + + // Compared by class name rather than with `is`: a switcher left behind by a previous plugin + // generation comes from a classloader that is already dead, so its class is not identical to ours. + private fun AnAction.isBackendSwitcher(): Boolean = + javaClass.name == PyDebuggerBackendSwitcherAction::class.java.name || + (this as? ActionStubBase)?.id == SWITCHER_ACTION_ID } + +@Language("devkit-action-id") +private const val SWITCHER_ACTION_ID = "Python.DebuggerBackendSwitcher" diff --git a/python/src/com/jetbrains/python/suggestions/PythonDapPluginSuggestion.kt b/python/src/com/jetbrains/python/suggestions/PythonDapPluginSuggestion.kt index 9f68654130c0..a09ce4332ba1 100644 --- a/python/src/com/jetbrains/python/suggestions/PythonDapPluginSuggestion.kt +++ b/python/src/com/jetbrains/python/suggestions/PythonDapPluginSuggestion.kt @@ -16,6 +16,7 @@ import com.jetbrains.python.PyBundle import com.jetbrains.python.PythonFileType import com.jetbrains.python.debugger.installPythonDapPlugin import com.jetbrains.python.debugger.isPythonDapPluginInstalledAndEnabled +import com.jetbrains.python.debugger.isPythonDapPluginInstalledButDisabled private const val PYTHON_DAP_PLUGIN_ID = "intellij.python.dap.plugin" private const val PYTHON_DAP_SUGGESTION_DISMISSED_KEY = "python.dap.suggestion.dismissed" @@ -50,7 +51,13 @@ internal class PythonDapPluginSuggestionProvider : PluginSuggestionProvider { val panel = EditorNotificationPanel(fileEditor, EditorNotificationPanel.Status.Info) panel.text = PyBundle.message("advertiser.python.dap.plugin") - panel.createActionLabel(PyBundle.message("advertiser.python.dap.plugin.install.text")) { + val actionText = if (isPythonDapPluginInstalledButDisabled()) { + PyBundle.message("advertiser.python.dap.plugin.enable.text") + } + else { + PyBundle.message("advertiser.python.dap.plugin.install.text") + } + panel.createActionLabel(actionText) { FUSEventSource.EDITOR.logInstallPlugins(pluginIds, project) installPythonDapPlugin(project) { EditorNotifications.getInstance(project).updateAllNotifications()