mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-87975 UI for switching between debug backends
Add a segmented button to Settings | Python | Debugger Add ComboBoxAction in the Debug tool window toolbar (Python.DebuggerBackendSwitcher) Show a one-time GotIt tooltip on the first debug session Show a confirmation dialog when switching backend during an active session Merge-request: IJ-MR-194172 Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com> (cherry picked from commit 965677f0668b3493290f9f3bb59ed6e7da2f2f38) GitOrigin-RevId: 7f0429d17e817a9e0c043b25a69d244a064320c6
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4a12a549cc
commit
4abe6f0b4d
@@ -882,6 +882,14 @@ debugger.use.the.following.code.to.connect.to.the.debugger=Use the following cod
|
||||
debugger.progress.title.stackframe.processing=StackFrame processing
|
||||
debugger.dialog.message.failed.to.create.debug=Failed to create debug session: run content descriptor is null
|
||||
|
||||
debugger.backend.switcher.label=Debugger mode:
|
||||
debugger.backend.pydevd.description=The core debugger engine that runs inside Python. Handles breakpoints, stepping, and execution control behind the scenes.
|
||||
debugger.backend.debugpy.description=Connects your IDE to a running Python program for debugging. Enables breakpoints, stepping, variable inspection, and remote debugging from your editor.
|
||||
debugger.backend.debugpy.disabled.tooltip=debugpy requires Python 3.9 or later
|
||||
debugger.backend.switch.during.session.title=Switch Debugger Backend
|
||||
debugger.backend.switch.during.session.message=Debugger backend cannot be switched during a running debug session. To apply the new setting, your current sessions will have to be restarted.
|
||||
debugger.backend.switch.and.restart=Switch and Restart
|
||||
|
||||
python.local.attach.group.name=Python
|
||||
|
||||
pydev.console.runner.settings=Settings
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.debugger
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Internal
|
||||
enum class PyDebuggerBackend { PYDEVD, DEBUGPY }
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.python.debugger.PyDebuggerConfigurable">
|
||||
<grid id="27dc6" binding="myMainPanel" layout-manager="GridLayoutManager" row-count="4" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<grid id="27dc6" binding="myMainPanel" layout-manager="GridLayoutManager" row-count="6" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
|
||||
<margin top="0" left="0" bottom="0" right="0"/>
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="735" height="333"/>
|
||||
<xy x="20" y="20" width="735" height="358"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
@@ -133,9 +133,15 @@
|
||||
</grid>
|
||||
</children>
|
||||
</grid>
|
||||
<component id="b3d2e" class="javax.swing.JLabel" binding="myBackendDescription" custom-create="true">
|
||||
<constraints>
|
||||
<grid row="4" column="0" row-span="1" col-span="4" vsize-policy="0" hsize-policy="3" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
<vspacer id="5915a">
|
||||
<constraints>
|
||||
<grid row="3" column="1" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
<grid row="5" column="1" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
</vspacer>
|
||||
<component id="ffb0f" class="com.intellij.ui.components.JBLabel" binding="myAttachFilterLabel">
|
||||
@@ -187,6 +193,12 @@
|
||||
<min value="0"/>
|
||||
</properties>
|
||||
</component>
|
||||
<component id="a1b2c" class="javax.swing.JPanel" binding="myBackendPanel" custom-create="true">
|
||||
<constraints>
|
||||
<grid row="3" column="0" row-span="1" col-span="4" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
|
||||
</constraints>
|
||||
<properties/>
|
||||
</component>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
||||
|
||||
@@ -5,7 +5,9 @@ import com.intellij.icons.AllIcons;
|
||||
import com.intellij.ide.IdeTooltipManager;
|
||||
import com.intellij.openapi.options.Configurable;
|
||||
import com.intellij.openapi.options.SearchableConfigurable;
|
||||
import com.intellij.openapi.ui.panel.ComponentPanelBuilder;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.roots.ProjectRootManager;
|
||||
import com.intellij.openapi.ui.ComboBox;
|
||||
import com.intellij.openapi.ui.Messages;
|
||||
import com.intellij.openapi.util.NlsSafe;
|
||||
@@ -17,10 +19,15 @@ import com.intellij.ui.components.ActionLink;
|
||||
import com.intellij.ui.components.JBCheckBox;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.ui.components.JBTextField;
|
||||
import com.intellij.ui.dsl.builder.IntelliJSpacingConfiguration;
|
||||
import com.intellij.ui.dsl.builder.SegmentedButton;
|
||||
import com.intellij.ui.dsl.builder.components.SegmentedButtonComponent;
|
||||
import com.intellij.uiDesigner.core.GridConstraints;
|
||||
import com.intellij.uiDesigner.core.GridLayoutManager;
|
||||
import com.intellij.uiDesigner.core.Spacer;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PythonRuntimeService;
|
||||
import com.jetbrains.python.psi.LanguageLevel;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -30,6 +37,7 @@ import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
@@ -56,6 +64,9 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
private final JPanel myDebuggerPortPanel;
|
||||
private final JBLabel myDebuggerEvaluationResponseTimeoutLabel;
|
||||
private final JBIntSpinner myDebuggerEvaluationResponseTimeout;
|
||||
private SegmentedButtonComponent<PyDebuggerBackend> myBackendSelector;
|
||||
private JPanel myBackendPanel;
|
||||
private JLabel myBackendDescription;
|
||||
|
||||
private enum PyQtBackend {
|
||||
AUTO(PyBundle.messagePointer("python.debugger.qt.backend.auto")),
|
||||
@@ -91,6 +102,7 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
|
||||
public PyDebuggerConfigurable(Project project) {
|
||||
myProject = project;
|
||||
initBackendPanel(project);
|
||||
{
|
||||
warningIcon = new JBLabel(AllIcons.General.BalloonWarning);
|
||||
IdeTooltipManager.getInstance().setCustomTooltip(
|
||||
@@ -130,7 +142,7 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
// >>> IMPORTANT!! <<<
|
||||
// DO NOT EDIT OR ADD ANY CODE HERE!
|
||||
myMainPanel = new JPanel();
|
||||
myMainPanel.setLayout(new GridLayoutManager(4, 4, new Insets(0, 0, 0, 0), -1, -1));
|
||||
myMainPanel.setLayout(new GridLayoutManager(6, 4, new Insets(0, 0, 0, 0), -1, -1));
|
||||
final JPanel panel1 = new JPanel();
|
||||
panel1.setLayout(new GridLayoutManager(7, 6, new Insets(0, 0, 0, 0), -1, -1));
|
||||
myMainPanel.add(panel1, new GridConstraints(0, 0, 1, 4, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
|
||||
@@ -215,7 +227,10 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED,
|
||||
null, null, null, 0, false));
|
||||
final Spacer spacer2 = new Spacer();
|
||||
myMainPanel.add(spacer2, new GridConstraints(3, 1, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, 1,
|
||||
myMainPanel.add(myBackendDescription, new GridConstraints(4, 0, 1, 4, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
|
||||
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
|
||||
GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||
myMainPanel.add(spacer2, new GridConstraints(5, 1, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, 1,
|
||||
GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
|
||||
myAttachFilterLabel = new JBLabel();
|
||||
this.$$$loadLabelText$$$(myAttachFilterLabel, this.$$$getMessageFromBundle$$$("messages/PyBundle",
|
||||
@@ -247,6 +262,9 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
|
||||
GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0,
|
||||
false));
|
||||
myMainPanel.add(myBackendPanel, new GridConstraints(3, 0, 1, 4, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
|
||||
GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
|
||||
GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
|
||||
myAttachFilterLabel.setLabelFor(myAttachProcessFilter);
|
||||
myDebuggerEvaluationResponseTimeoutLabel.setLabelFor(myAttachProcessFilter);
|
||||
}
|
||||
@@ -267,9 +285,50 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
}
|
||||
});
|
||||
|
||||
myBackendSelector.addChangeListener(e -> {
|
||||
PyDebuggerBackend selected = myBackendSelector.getSelectedItem();
|
||||
if (selected != null) {
|
||||
myBackendDescription.setText(getBackendDescription(selected));
|
||||
}
|
||||
});
|
||||
|
||||
myAttachFilterLabel.setText(PyBundle.message("debugger.attach.to.process.filter.names"));
|
||||
}
|
||||
|
||||
private void applyBackend() {
|
||||
PyDebuggerBackend newBackend = myBackendSelector.getSelectedItem() != null
|
||||
? myBackendSelector.getSelectedItem() : PyDebuggerBackend.PYDEVD;
|
||||
PyDebuggerOptionsProvider.switchBackendWithRestart(myProject, newBackend);
|
||||
}
|
||||
|
||||
private void initBackendPanel(@NotNull Project project) {
|
||||
var projectSdk = ProjectRootManager.getInstance(project).getProjectSdk();
|
||||
boolean debugpyDisabled = projectSdk != null &&
|
||||
PythonRuntimeService.getInstance().getLanguageLevelForSdk(projectSdk)
|
||||
.isOlderThan(LanguageLevel.PYTHON39);
|
||||
myBackendSelector = new SegmentedButtonComponent<>(backend -> {
|
||||
@NlsSafe String name = backend == PyDebuggerBackend.DEBUGPY ? "debugpy" : "pydevd";
|
||||
boolean enabled = backend != PyDebuggerBackend.DEBUGPY || !debugpyDisabled;
|
||||
String tooltip = backend == PyDebuggerBackend.DEBUGPY && debugpyDisabled
|
||||
? PyBundle.message("debugger.backend.debugpy.disabled.tooltip") : null;
|
||||
return SegmentedButton.Companion.createPresentation(name, tooltip, null, enabled);
|
||||
});
|
||||
myBackendSelector.setItems(PyDebuggerBackend.getEntries());
|
||||
myBackendSelector.setSpacing(new IntelliJSpacingConfiguration());
|
||||
myBackendPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
myBackendPanel.add(new JBLabel(PyBundle.message("debugger.backend.switcher.label") + " "));
|
||||
myBackendPanel.add(myBackendSelector);
|
||||
//noinspection deprecation - no Java-friendly replacement (deprecated in favor of Kotlin UI DSL)
|
||||
myBackendDescription = ComponentPanelBuilder.createCommentComponent(
|
||||
getBackendDescription(PyDebuggerOptionsProvider.getInstance(project).getSelectedBackend()), true);
|
||||
}
|
||||
|
||||
private static @Nls String getBackendDescription(@NotNull PyDebuggerBackend backend) {
|
||||
return backend == PyDebuggerBackend.PYDEVD
|
||||
? PyBundle.message("debugger.backend.pydevd.description")
|
||||
: PyBundle.message("debugger.backend.debugpy.description");
|
||||
}
|
||||
|
||||
private static Method $$$cachedGetBundleMethod$$$ = null;
|
||||
|
||||
/** @noinspection ALL */
|
||||
@@ -365,7 +424,8 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
@Override
|
||||
public boolean isModified() {
|
||||
PyDebuggerOptionsProvider settings = PyDebuggerOptionsProvider.getInstance(myProject);
|
||||
return myAttachToSubprocess.isSelected() != settings.isAttachToSubprocess() ||
|
||||
return myBackendSelector.getSelectedItem() != settings.getSelectedBackend() ||
|
||||
myAttachToSubprocess.isSelected() != settings.isAttachToSubprocess() ||
|
||||
mySaveSignatures.isSelected() != settings.isSaveCallSignatures() ||
|
||||
mySupportGevent.isSelected() != settings.isSupportGeventDebugging() ||
|
||||
myDropIntoDebuggerOnFailedTests.isSelected() != settings.isDropIntoDebuggerOnFailedTest() ||
|
||||
@@ -394,9 +454,10 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
|
||||
settings.setRunDebuggerInServerMode(myRunDebuggerInServerMode.isSelected());
|
||||
settings.setDebuggerPort(myDebuggerPort.getNumber());
|
||||
|
||||
settings.setAttachProcessFilter(myAttachProcessFilter.getText());
|
||||
settings.setEvaluationResponseTimeout(myDebuggerEvaluationResponseTimeout.getNumber());
|
||||
|
||||
applyBackend();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -412,5 +473,6 @@ public final class PyDebuggerConfigurable implements SearchableConfigurable, Con
|
||||
myRunDebuggerInServerMode.setSelected(settings.isRunDebuggerInServerMode());
|
||||
myAttachProcessFilter.setText(settings.getAttachProcessFilter());
|
||||
myDebuggerEvaluationResponseTimeout.setNumber(settings.getEvaluationResponseTimeout());
|
||||
myBackendSelector.setSelectedItem(PyDebuggerOptionsProvider.getInstance(myProject).getSelectedBackend());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
// Copyright 2000-2020 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.debugger;
|
||||
|
||||
import com.intellij.execution.ProgramRunnerUtil;
|
||||
import com.intellij.execution.RunnerAndConfigurationSettings;
|
||||
import com.intellij.execution.executors.DefaultDebugExecutor;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.components.PersistentStateComponent;
|
||||
import com.intellij.openapi.components.Service;
|
||||
import com.intellij.openapi.components.State;
|
||||
import com.intellij.openapi.components.Storage;
|
||||
import com.intellij.openapi.components.StoragePathMacros;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.Messages;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.xdebugger.XDebugSession;
|
||||
import com.intellij.xdebugger.XDebugSessionListener;
|
||||
import com.intellij.xdebugger.XDebuggerManager;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
@State(
|
||||
name = "PyDebuggerOptionsProvider",
|
||||
storages = {
|
||||
@Storage(StoragePathMacros.WORKSPACE_FILE)
|
||||
}
|
||||
storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
|
||||
)
|
||||
@ApiStatus.Internal
|
||||
public final class PyDebuggerOptionsProvider implements PersistentStateComponent<PyDebuggerOptionsProvider.State> {
|
||||
@@ -49,6 +60,7 @@ public final class PyDebuggerOptionsProvider implements PersistentStateComponent
|
||||
public int myDebuggerPort = 29781;
|
||||
public @NonNls String myAttachProcessFilter = "python";
|
||||
public int myEvaluationResponseTimeout = 60_000;
|
||||
public @NonNls String myDebuggerBackend = PyDebuggerBackend.PYDEVD.name();
|
||||
}
|
||||
|
||||
|
||||
@@ -134,5 +146,76 @@ public final class PyDebuggerOptionsProvider implements PersistentStateComponent
|
||||
public void setEvaluationResponseTimeout(int timeout) {
|
||||
myState.myEvaluationResponseTimeout = timeout;
|
||||
}
|
||||
|
||||
public @NotNull PyDebuggerBackend getSelectedBackend() {
|
||||
try {
|
||||
return PyDebuggerBackend.valueOf(myState.myDebuggerBackend);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return PyDebuggerBackend.PYDEVD;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedBackend(@NotNull PyDebuggerBackend backend) {
|
||||
myState.myDebuggerBackend = backend.name();
|
||||
}
|
||||
|
||||
public static boolean hasActivePythonSessions(@NotNull Project project) {
|
||||
return ContainerUtil.exists(
|
||||
XDebuggerManager.getInstance(project).getDebugSessions(),
|
||||
session -> session.getRunProfile() instanceof AbstractPythonRunConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to {@code newBackend}, prompting the user to confirm a session restart when sessions are active.
|
||||
*
|
||||
* @return {@code true} if the backend was changed (possibly with a restart), {@code false} if the user cancelled.
|
||||
*/
|
||||
public static boolean switchBackendWithRestart(@NotNull Project project, @NotNull PyDebuggerBackend newBackend) {
|
||||
PyDebuggerOptionsProvider options = getInstance(project);
|
||||
if (options.getSelectedBackend() == newBackend) return true;
|
||||
boolean hasSessions = hasActivePythonSessions(project);
|
||||
if (hasSessions) {
|
||||
int result = Messages.showDialog(
|
||||
project,
|
||||
PyBundle.message("debugger.backend.switch.during.session.message"),
|
||||
PyBundle.message("debugger.backend.switch.during.session.title"),
|
||||
new String[]{PyBundle.message("debugger.backend.switch.and.restart"), Messages.getCancelButton()},
|
||||
0,
|
||||
Messages.getWarningIcon()
|
||||
);
|
||||
if (result != 0) return false;
|
||||
}
|
||||
options.setSelectedBackend(newBackend);
|
||||
if (hasSessions) {
|
||||
restartAllPythonSessions(project);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void restartAllPythonSessions(@NotNull Project project) {
|
||||
List<XDebugSession> sessions = ContainerUtil.filter(
|
||||
XDebuggerManager.getInstance(project).getDebugSessions(),
|
||||
session -> session.getRunProfile() instanceof AbstractPythonRunConfiguration
|
||||
);
|
||||
|
||||
for (XDebugSession session : sessions) {
|
||||
RunnerAndConfigurationSettings settings = session.getExecutionEnvironment() != null
|
||||
? session.getExecutionEnvironment().getRunnerAndConfigurationSettings() : null;
|
||||
if (settings != null) {
|
||||
session.addSessionListener(new XDebugSessionListener() {
|
||||
@Override
|
||||
public void sessionStopped() {
|
||||
// sessionStopped fires on a non-EDT thread (stopAsync callback); dispatch to EDT for executeConfiguration
|
||||
ApplicationManager.getApplication().invokeLater(
|
||||
() -> ProgramRunnerUtil.executeConfiguration(settings, DefaultDebugExecutor.getDebugExecutorInstance())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
session.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user