diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 33d4d3d6d125..498e928d0f62 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -425,7 +425,7 @@ python.sdk.installation.balloon.error.message=Python installation interrupted python.sdk.installation.balloon.error.action=See the documentation python.sdk.python.executable.not.found=Python executable {0} is missing python.sdk.failed.to.create.interpreter.title=Failed to create interpreter -python.sdk.can.t.obtain.python.version=Cannot obtain python version +python.sdk.can.t.obtain.python.version=Cannot obtain Python version python.sdk.empty.version.string=Python interpreter returned the empty output as a version string python.sdk.non.zero.exit.code=Python interpreter process exited with a non-zero exit code {0} python.sdk.executable.not.found.header=Python executable is not found. Choose one of the following options: @@ -504,7 +504,7 @@ sdk.create.python.version=Python version: sdk.create.conda.executable.path=Path to conda: sdk.create.conda.missing.text=No conda executable found sdk.create.conda.install.fix=Install Miniconda -sdk.create.error.base.broken=Base Python {0} is broken, virtual environment can not be created on top of it +sdk.create.error.base.broken=Base Python {0} is broken, virtual environment cannot be created on top of it sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0} sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment sdk.create.simple.uv.hint=uv environment will be created in the project root: {0} @@ -661,7 +661,7 @@ configurable.PyActiveSdkModuleConfigurable.python.interpreter.display.name=Pytho configurable.PyActiveSdkModuleConfigurable.pycharm.interpreter.display.name=Interpreter configurable.PyUserTypeRenderersConfigurable.display.name=Python Type Renderers configurable.PyUserTypeRenderersConfigurable.pycharm.display.name=Type Renderers -configurable.PyUserTypeRenderersConfigurable.description=Define custom display formats for various data types across your application. +configurable.PyUserTypeRenderersConfigurable.description=Define custom display formats for various data types across your application configurable.plots.pycharm.display.name=Plots flask.name=Flask settings.default.remote.interpreter=Default Python Remote Interpreter @@ -880,6 +880,7 @@ debugger.debug.process.running=Process is running debugger.remote.starting.debug.server.at.port=Starting debug server at port {0}\n debugger.use.the.following.code.to.connect.to.the.debugger=Use the following code to connect to the debugger:\n debugger.progress.title.stackframe.processing=StackFrame processing +debugger.dialog.message.failed.to.create.debug=Failed to create debug session: run content descriptor is null python.local.attach.group.name=Python @@ -1775,4 +1776,5 @@ sdk.cannot.find.venv.for.module=Can't find venv for the module sdk.set.up.uv.environment=Set up uv environment sdk.cannot.find.uv.executable=Cannot find uv executable -sdk.cannot.find.python=No Pythons were found on the system +sdk.cannot.find.python=No Python installations were found on your system +debugger.step.into.my.code.switch.link=Switch to My code \ No newline at end of file diff --git a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java index ba5a89855f3d..862015c471a4 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java +++ b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java @@ -89,6 +89,7 @@ import com.jetbrains.python.remote.RemoteProcessControl; import com.jetbrains.python.tables.TableCommandParameters; import com.jetbrains.python.tables.TableCommandType; import com.jetbrains.python.testing.AbstractPythonTestRunConfiguration; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -110,7 +111,7 @@ import static com.jetbrains.python.debugger.variablesview.usertyperenderers.Conf import static com.jetbrains.python.debugger.variablesview.usertyperenderers.ConfigureTypeRenderersActionKt.loadTypeRendererChildren; import static com.jetbrains.python.statistics.PythonDebuggerIdsHolder.CONNECTION_FAILED; -public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, ProcessListener, PyDebugProcessWithConsole { +public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, ProcessListener, PyDebugProcessWithConsole, PyStepIntoSupport { private static final Logger LOG = Logger.getInstance(PyDebugProcess.class); private static final int CONNECTION_TIMEOUT = 60000; @@ -595,6 +596,24 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr passToCurrentThread(context, ResumeOrStepCommand.Mode.STEP_INTO_MY_CODE); } + @ApiStatus.Internal + @Override + public boolean isStepIntoMyCodeAvailable() { + return true; + } + + @ApiStatus.Internal + @Override + public @Nullable String getStepIntoMyCodeUnavailableReason() { + return null; + } + + @ApiStatus.Internal + @Override + public void performStepIntoMyCode() { + startStepIntoMyCode(getSession().getSuspendContext()); + } + public void startSetNextStatement(@Nullable XSuspendContext context, @NotNull XSourcePosition sourcePosition, @NotNull PyDebugCallback> callback) { diff --git a/python/src/com/jetbrains/python/debugger/PyStepIntoMyCodeAction.java b/python/src/com/jetbrains/python/debugger/PyStepIntoMyCodeAction.java index 271cfcc59a9a..d3eca56eca36 100644 --- a/python/src/com/jetbrains/python/debugger/PyStepIntoMyCodeAction.java +++ b/python/src/com/jetbrains/python/debugger/PyStepIntoMyCodeAction.java @@ -1,48 +1,89 @@ -// 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.debugger; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.ex.TooltipDescriptionProvider; +import com.intellij.openapi.actionSystem.ex.TooltipLinkProvider; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.ide.DataManager; import com.intellij.openapi.project.Project; -import com.intellij.xdebugger.XDebugProcess; import com.intellij.xdebugger.XDebugSession; +import com.intellij.xdebugger.XDebuggerManager; import com.intellij.xdebugger.impl.DebuggerSupport; import com.intellij.xdebugger.impl.actions.DebuggerActionHandler; import com.intellij.xdebugger.impl.actions.XDebuggerActionBase; import com.intellij.xdebugger.impl.actions.XDebuggerSuspendedActionHandler; +import com.jetbrains.python.PyBundle; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import javax.swing.JComponent; -public class PyStepIntoMyCodeAction extends XDebuggerActionBase { - private final XDebuggerSuspendedActionHandler myStepIntoMyCodeHandler; +public class PyStepIntoMyCodeAction extends XDebuggerActionBase + implements TooltipDescriptionProvider, TooltipLinkProvider { - public PyStepIntoMyCodeAction() { - super(); - myStepIntoMyCodeHandler = new XDebuggerSuspendedActionHandler() { - @Override - protected void perform(@NotNull XDebugSession session, @NotNull DataContext dataContext) { - XDebugProcess debugProcess = session.getDebugProcess(); - if (debugProcess instanceof PyDebugProcess pyDebugProcess) { - pyDebugProcess.startStepIntoMyCode(debugProcess.getSession().getSuspendContext()); - } + private final XDebuggerSuspendedActionHandler myHandler = new XDebuggerSuspendedActionHandler() { + @Override + protected void perform(@NotNull XDebugSession session, @NotNull DataContext dataContext) { + if (session.getDebugProcess() instanceof PyStepIntoSupport support) { + support.performStepIntoMyCode(); } + } - @Override - public boolean isEnabled(@NotNull Project project, @NotNull AnActionEvent event) { - return super.isEnabled(project, event) && PyDebugSupportUtils.isCurrentPythonDebugProcess(event); - } - }; - } + @Override + protected boolean isEnabled(@NotNull XDebugSession session, @NotNull DataContext dataContext) { + return super.isEnabled(session, dataContext) + && session.getDebugProcess() instanceof PyStepIntoSupport support + && support.isStepIntoMyCodeAvailable(); + } + }; @Override + @SuppressWarnings("deprecation") protected @NotNull DebuggerActionHandler getHandler(@NotNull DebuggerSupport debuggerSupport) { - return myStepIntoMyCodeHandler; + return myHandler; } @Override protected boolean isHidden(@NotNull AnActionEvent event) { - Project project = event.getData(CommonDataKeys.PROJECT); - return project == null || !PyDebugSupportUtils.isCurrentPythonDebugProcess(event); + return false; + } + + @ApiStatus.Internal + @Override + public void update(@NotNull AnActionEvent event) { + super.update(event); + XDebugSession session = event.getData(XDebugSession.DATA_KEY); + if (session == null) { + Project project = event.getProject(); + if (project == null) return; + session = XDebuggerManager.getInstance(project).getCurrentSession(); + } + if (session == null) return; + if (session.getDebugProcess() instanceof PyStepIntoSupport support) { + String reason = support.getStepIntoMyCodeUnavailableReason(); + if (reason != null) { + event.getPresentation().setDescription(reason); + } + } + } + + @ApiStatus.Internal + @Override + public @Nullable TooltipLink getTooltipLink(@Nullable JComponent owner) { + if (owner == null) return null; + DataContext dataContext = DataManager.getInstance().getDataContext(owner); + XDebugSession session = dataContext.getData(XDebugSession.DATA_KEY); + if (session == null) { + Project project = dataContext.getData(CommonDataKeys.PROJECT); + if (project == null) return null; + session = XDebuggerManager.getInstance(project).getCurrentSession(); + } + if (session == null) return null; + if (!(session.getDebugProcess() instanceof PyStepIntoSupport support)) return null; + if (support.isStepIntoMyCodeAvailable()) return null; + return new TooltipLink(PyBundle.message("debugger.step.into.my.code.switch.link"), () -> support.applyJustMyCodeChange(true)); } } diff --git a/python/src/com/jetbrains/python/debugger/PyStepIntoSupport.kt b/python/src/com/jetbrains/python/debugger/PyStepIntoSupport.kt new file mode 100644 index 000000000000..1cc4712cacd6 --- /dev/null +++ b/python/src/com/jetbrains/python/debugger/PyStepIntoSupport.kt @@ -0,0 +1,39 @@ +// 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 com.intellij.openapi.util.NlsContexts +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface PyStepIntoSupport { + /** True when stepping into user code only is currently possible. */ + val isStepIntoMyCodeAvailable: Boolean + + /** Localized tooltip explaining why stepIntoMyCode is unavailable (null if available). */ + val stepIntoMyCodeUnavailableReason: @NlsContexts.Tooltip String? + + /** Execute a "step into my code" for the current suspend context. */ + fun performStepIntoMyCode() + + /** + * True when stepping into all code (including libraries) is available. + * Defaults to true; override to false when the session was launched with justMyCode=true. + */ + val isStepIntoAllCodeAvailable: Boolean get() = true + + /** + * Localized tooltip explaining why stepIntoAllCode is unavailable (null if available or not applicable). + * Shown in the toolbar tooltip when [isStepIntoAllCodeAvailable] is false. + */ + val stepIntoAllCodeUnavailableReason: @NlsContexts.Tooltip String? get() = null + + /** Execute a "step into all code" (including libraries) for the current suspend context. */ + fun performStepIntoAllCode() {} + + /** + * Applies [enableJustMyCode] to the active run configuration and opens the run configuration editor + * so the user can verify and then restart the session manually. + * Default is a no-op (used by pydevd, which does not have a justMyCode session-level flag). + */ + fun applyJustMyCodeChange(enableJustMyCode: Boolean) {} +}