PY-90402 [debugger]: add support for enabling disabled Python DAP Debugger plugin in plugin suggestions

Merge-request: IJ-MR-216357
Merged-by: Maxim Popov <maxim.popov@jetbrains.com>

(cherry picked from commit 9b8343d4342d019ff4c4dffa7c14ae01d6318fde)

IJ-MR-216357

GitOrigin-RevId: 051016e7b7d602705a508aced468ba21de2517bb
This commit is contained in:
Maxim Popov
2026-07-31 15:53:58 +00:00
committed by intellij-monorepo-bot
parent 9846eddcab
commit 909c50a404
4 changed files with 53 additions and 5 deletions
@@ -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
@@ -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<PyDapPluginLoadListener>().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
@@ -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"
@@ -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()