diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java index 56e1ec0c962c..122c1f5b3500 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValue.java @@ -2,6 +2,7 @@ package com.jetbrains.python.debugger; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import com.intellij.icons.AllIcons; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; @@ -9,6 +10,7 @@ import com.intellij.xdebugger.frame.*; import com.intellij.xdebugger.frame.presentation.XRegularValuePresentation; import com.jetbrains.python.debugger.pydev.PyDebugCallback; import com.jetbrains.python.debugger.pydev.PyVariableLocator; +import com.jetbrains.python.debugger.render.PyNodeRenderer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,8 @@ public class PyDebugValue extends XNamedValue { private int myOffset; private int myCollectionLength = -1; + private @NotNull PyDebugValueDescriptor myDescriptor = new PyDebugValueDescriptor(); + public enum ValuesPolicy { SYNC, ASYNC, ON_DEMAND } @@ -336,6 +340,7 @@ public class PyDebugValue extends XNamedValue { if (value.length() >= MAX_VALUE) { value = value.substring(0, MAX_VALUE); } + value = applyRendererIfApplicable(value); setElementPresentation(node, value); } @@ -456,6 +461,9 @@ public class PyDebugValue extends XNamedValue { ApplicationManager.getApplication().executeOnPooledThread(() -> { try { XValueChildrenList values = myFrameAccessor.loadVariable(this); + + restoreChildrenRenderers(values); + if (!node.isObsolete()) { updateLengthIfIsCollection(values); @@ -655,4 +663,49 @@ public class PyDebugValue extends XNamedValue { } } + @NotNull + public PyDebugValueDescriptor getDescriptor() { + return myDescriptor; + } + + public void setDescriptor(@NotNull PyDebugValueDescriptor descriptor) { + myDescriptor = descriptor; + } + + private void restoreChildrenRenderers(XValueChildrenList values) { + PyDebugValueDescriptor descriptor = getDescriptor(); + Map childrenDescriptors = descriptor.getChildrenDescriptors(); + + if (childrenDescriptors == null) { + childrenDescriptors = Maps.newHashMap(); + descriptor.setChildrenDescriptors(childrenDescriptors); + } + + if (values == null) return; + + for (int i = 0; i < values.size(); i++) { + if (values.getValue(i) instanceof PyDebugValue) { + PyDebugValue value = (PyDebugValue) values.getValue(i); + descriptor = childrenDescriptors.getOrDefault(value.getName(), null); + if (descriptor == null) { + descriptor = new PyDebugValueDescriptor(); + childrenDescriptors.put(value.getName(), descriptor); + } + value.setDescriptor(descriptor); + } + } + } + + private String applyRendererIfApplicable(String value) { + final PyNodeRenderer renderer = getDescriptor().getRenderer(); + final String type = getType(); + if (renderer == null || type == null) return value; + if (renderer.isApplicable(type)) { + return renderer.render(value); + } + else { + getDescriptor().setRenderer(null); + } + return value; + } } diff --git a/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValueDescriptor.java b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValueDescriptor.java new file mode 100644 index 000000000000..48522fab1de9 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/PyDebugValueDescriptor.java @@ -0,0 +1,40 @@ +// Copyright 2000-2019 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.jetbrains.python.debugger.render.PyNodeRenderer; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class PyDebugValueDescriptor { + + @Nullable private PyNodeRenderer myRenderer; + + /** + * Because of the variables and recreated every time we make a step in the debugger, + * we have to save the variable children's descriptors to restore them when updating + * the debugger variable view. Note that even if there is no renderer associated + * with a {@link PyDebugValue} instance, it still maintains a value descriptor + * to preserve access to its children renderers. Otherwise, it will be impossible to + * reach them during the renderers' restoration process. + */ + @Nullable private Map myChildrenDescriptors; + + @Nullable + public PyNodeRenderer getRenderer() { + return myRenderer; + } + + public void setRenderer(@Nullable PyNodeRenderer renderer) { + myRenderer = renderer; + } + + @Nullable + public Map getChildrenDescriptors() { + return myChildrenDescriptors; + } + + public void setChildrenDescriptors(@Nullable Map childrenDescriptors) { + myChildrenDescriptors = childrenDescriptors; + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/AbstractIntegerRenderer.java b/python/pydevSrc/com/jetbrains/python/debugger/render/AbstractIntegerRenderer.java new file mode 100644 index 000000000000..a776593967d6 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/AbstractIntegerRenderer.java @@ -0,0 +1,14 @@ +// Copyright 2000-2019 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.render; + +import com.google.common.collect.ImmutableSet; + +public abstract class AbstractIntegerRenderer implements PyNodeRenderer { + + private static final ImmutableSet mySupportedTypes = ImmutableSet.of("int"); + + @Override + public boolean isApplicable(String type) { + return type != null && mySupportedTypes.contains(type); + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/BinaryRenderer.java b/python/pydevSrc/com/jetbrains/python/debugger/render/BinaryRenderer.java new file mode 100644 index 000000000000..31005c5f9cce --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/BinaryRenderer.java @@ -0,0 +1,23 @@ +// Copyright 2000-2019 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.render; + +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; + +public class BinaryRenderer extends AbstractIntegerRenderer { + + private static final String myName = "Binary"; + + @Override + public @NonNls String getName() { + return myName; + } + + @Override + public @NotNull String render(@NotNull String value) { + final String prefix = value.startsWith("-") ? "-0b" : "0b"; + return prefix + new BigInteger(value).abs().toString(2); + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/DecimalRenderer.java b/python/pydevSrc/com/jetbrains/python/debugger/render/DecimalRenderer.java new file mode 100644 index 000000000000..3eac2731e78b --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/DecimalRenderer.java @@ -0,0 +1,20 @@ +// Copyright 2000-2019 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.render; + +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public class DecimalRenderer extends AbstractIntegerRenderer { + + private static final String myName = "Decimal"; + + @Override + public @NonNls String getName() { + return myName; + } + + @Override + public @NotNull String render(@NotNull String value) { + return value; + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/HexRenderer.java b/python/pydevSrc/com/jetbrains/python/debugger/render/HexRenderer.java new file mode 100644 index 000000000000..a47d726d8caf --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/HexRenderer.java @@ -0,0 +1,23 @@ +// Copyright 2000-2019 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.render; + +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import java.math.BigInteger; + +public class HexRenderer extends AbstractIntegerRenderer { + + private static final String myName = "Hex"; + + @Override + public @NonNls String getName() { + return myName; + } + + @Override + public @NotNull @NonNls String render(@NotNull String value) { + final String prefix = value.startsWith("-") ? "-0x" : "0x"; + return prefix + new BigInteger(value).abs().toString(16).toUpperCase(); + } +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRenderer.java b/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRenderer.java new file mode 100644 index 000000000000..dfb52b4e83e3 --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRenderer.java @@ -0,0 +1,12 @@ +// Copyright 2000-2019 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.render; + +public interface PyNodeRenderer { + + String getName(); + + String render(String value); + + boolean isApplicable(String type); + +} diff --git a/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRendererManager.java b/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRendererManager.java new file mode 100644 index 000000000000..51b8c15cb10a --- /dev/null +++ b/python/pydevSrc/com/jetbrains/python/debugger/render/PyNodeRendererManager.java @@ -0,0 +1,29 @@ +// Copyright 2000-2019 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.render; + +import org.jetbrains.annotations.Contract; + +import java.util.ArrayList; +import java.util.List; + +public class PyNodeRendererManager { + + private static final PyNodeRendererManager INSTANCE = new PyNodeRendererManager(); + + private final List myAvailableRenderers = new ArrayList<>(); + + private PyNodeRendererManager() { + myAvailableRenderers.add(new BinaryRenderer()); + myAvailableRenderers.add(new DecimalRenderer()); + myAvailableRenderers.add(new HexRenderer()); + } + + @Contract(pure = true) + public static PyNodeRendererManager getInstance() { + return INSTANCE; + } + + public List getAvailableRenderers() { + return new ArrayList<>(myAvailableRenderers); + } +} diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index fe820b625742..3f27bc977f3c 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -827,6 +827,10 @@ + + + + diff --git a/python/src/com/jetbrains/python/console/PythonConsoleView.java b/python/src/com/jetbrains/python/console/PythonConsoleView.java index c9dc02f9de31..c38e4c2ea2ab 100644 --- a/python/src/com/jetbrains/python/console/PythonConsoleView.java +++ b/python/src/com/jetbrains/python/console/PythonConsoleView.java @@ -1,6 +1,7 @@ // Copyright 2000-2019 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.console; +import com.google.common.collect.Maps; import com.intellij.execution.console.LanguageConsoleImpl; import com.intellij.execution.filters.OpenFileHyperlinkInfo; import com.intellij.execution.impl.ConsoleViewUtil; @@ -40,6 +41,7 @@ import com.jetbrains.python.PythonLanguage; import com.jetbrains.python.console.completion.PythonConsoleAutopopupBlockingHandler; import com.jetbrains.python.console.pydev.ConsoleCommunication; import com.jetbrains.python.console.pydev.ConsoleCommunicationListener; +import com.jetbrains.python.debugger.PyDebugValueDescriptor; import com.jetbrains.python.debugger.PyDebuggerEditorsProvider; import com.jetbrains.python.debugger.PyStackFrame; import com.jetbrains.python.debugger.PyStackFrameInfo; @@ -54,6 +56,7 @@ import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.awt.*; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -77,6 +80,8 @@ public class PythonConsoleView extends LanguageConsoleImpl implements Observable private boolean isShowVars; @Nullable private String mySdkHomePath; + private final Map> myDescriptorsCache = Maps.newConcurrentMap(); + /** * @param testMode this console will be used to display test output and should support TC messages */ @@ -364,6 +369,7 @@ public class PythonConsoleView extends LanguageConsoleImpl implements Observable public void showVariables(PydevConsoleCommunication consoleCommunication) { PyStackFrame stackFrame = new PyStackFrame(getProject(), consoleCommunication, new PyStackFrameInfo("", "", "", null), null); + stackFrame.restoreChildrenDescriptors(myDescriptorsCache); final XStandaloneVariablesView view = new XStandaloneVariablesView(getProject(), new PyDebuggerEditorsProvider(), stackFrame); consoleCommunication.addCommunicationListener(new ConsoleCommunicationListener() { @Override diff --git a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java index a29052e8703f..4c9d4c67aab9 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java +++ b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java @@ -111,6 +111,8 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr private boolean isCythonWarningShown = false; @Nullable private XCompositeNode myCurrentRootNode; + private final Map> myDescriptorsCache = Maps.newConcurrentMap(); + public PyDebugProcess(@NotNull XDebugSession session, @NotNull ServerSocket serverSocket, @NotNull ExecutionConsole executionConsole, @@ -1066,8 +1068,10 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr } public PyStackFrame createStackFrame(PyStackFrameInfo frameInfo) { - return new PyStackFrame(getSession().getProject(), this, frameInfo, - getPositionConverter().convertFromPython(frameInfo.getPosition(), frameInfo.getName())); + final PyStackFrame frame = new PyStackFrame(getSession().getProject(), this, frameInfo, + getPositionConverter().convertFromPython(frameInfo.getPosition(), frameInfo.getName())); + frame.restoreChildrenDescriptors(myDescriptorsCache); + return frame; } @Override diff --git a/python/src/com/jetbrains/python/debugger/PyStackFrame.java b/python/src/com/jetbrains/python/debugger/PyStackFrame.java index 67a42ef87572..0dd2560fd1e4 100644 --- a/python/src/com/jetbrains/python/debugger/PyStackFrame.java +++ b/python/src/com/jetbrains/python/debugger/PyStackFrame.java @@ -1,6 +1,7 @@ // Copyright 2000-2018 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.google.common.collect.Maps; import com.intellij.icons.AllIcons; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; @@ -48,6 +49,8 @@ public class PyStackFrame extends XStackFrame { private final PyStackFrameInfo myFrameInfo; private final XSourcePosition myPosition; + private @Nullable Map myChildrenDescriptors; + public PyStackFrame(@NotNull Project project, @NotNull final PyFrameAccessor debugProcess, @NotNull final PyStackFrameInfo frameInfo, XSourcePosition position) { @@ -146,6 +149,9 @@ public class PyStackFrame extends XStackFrame { String name = children.getName(i); if (value instanceof PyDebugValue) { PyDebugValue pyValue = (PyDebugValue)value; + + restoreValueDescriptor(pyValue); + if (pyValue.isReturnedVal() && debuggerSettings.isWatchReturnValues()) { returnedValues.put(name, value); } @@ -209,4 +215,26 @@ public class PyStackFrame extends XStackFrame { protected XSourcePosition getPosition() { return myPosition; } + + public void setChildrenDescriptors(@Nullable Map childrenDescriptors) { + myChildrenDescriptors = childrenDescriptors; + } + + public void restoreChildrenDescriptors(@NotNull Map> descriptorsCache) { + final String threadFrameId = getThreadFrameId(); + final Map childrenDescriptors = descriptorsCache.getOrDefault(threadFrameId, Maps.newHashMap()); + setChildrenDescriptors(childrenDescriptors); + descriptorsCache.put(threadFrameId, childrenDescriptors); + } + + private void restoreValueDescriptor(PyDebugValue value) { + if (myChildrenDescriptors != null) { + PyDebugValueDescriptor descriptor = myChildrenDescriptors.getOrDefault(value.getName(), null); + if (descriptor == null) { + descriptor = new PyDebugValueDescriptor(); + myChildrenDescriptors.put(value.getName(), descriptor); + } + value.setDescriptor(descriptor); + } + } } diff --git a/python/src/com/jetbrains/python/debugger/actions/ViewAsGroup.java b/python/src/com/jetbrains/python/debugger/actions/ViewAsGroup.java new file mode 100644 index 000000000000..4fe4f50b6982 --- /dev/null +++ b/python/src/com/jetbrains/python/debugger/actions/ViewAsGroup.java @@ -0,0 +1,118 @@ +// Copyright 2000-2019 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.actions; + +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.project.DumbAware; +import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase; +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl; +import com.jetbrains.python.debugger.PyDebugValue; +import com.jetbrains.python.debugger.render.DecimalRenderer; +import com.jetbrains.python.debugger.render.PyNodeRenderer; +import com.jetbrains.python.debugger.render.PyNodeRendererManager; +import one.util.streamex.StreamEx; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ViewAsGroup extends ActionGroup implements DumbAware { + + private static final AnAction[] myChildren; + + static { + final List children = new ArrayList<>(); + for (PyNodeRenderer renderer : PyNodeRendererManager.getInstance().getAvailableRenderers()) { + children.add(new RendererAction(renderer)); + } + myChildren = children.toArray(EMPTY_ARRAY); + } + + private static class RendererAction extends ToggleAction { + + private final PyNodeRenderer myPyNodeRenderer; + + private RendererAction(PyNodeRenderer pyNodeRenderer) { + super(pyNodeRenderer.getName()); + myPyNodeRenderer = pyNodeRenderer; + } + + @Override + public boolean isSelected(@NotNull AnActionEvent e) { + final List values = getSelectedValues(e); + + if (values.isEmpty()) return false; + + if (values.size() == 1) { + PyNodeRenderer renderer = values.get(0).getDescriptor().getRenderer(); + return renderer == null ? myPyNodeRenderer instanceof DecimalRenderer : renderer == myPyNodeRenderer; + } + + boolean allRenderersAreTheSame = values.stream().map(value -> value.getDescriptor().getRenderer()).distinct().count() == 1; + + if (allRenderersAreTheSame) { + if (values.get(0).getDescriptor().getRenderer() == myPyNodeRenderer + || (values.get(0).getDescriptor().getRenderer() == null && myPyNodeRenderer instanceof DecimalRenderer)) return true; + } + + return false; + } + + @Override + public void setSelected(@NotNull AnActionEvent e, boolean state) { + if (!state) return; + + final List selectedNodes = XDebuggerTreeActionBase.getSelectedNodes(e.getDataContext()); + + for (XValueNodeImpl node : selectedNodes) { + PyDebugValue value = (PyDebugValue) node.getValueContainer(); + value.getDescriptor().setRenderer(myPyNodeRenderer); + if (value.getValue() == null) continue; + String lbl = value.getDescriptor().getRenderer() != null ? value.getDescriptor(). + getRenderer().render(value.getValue()) : value.getValue(); + if (lbl != null) { + node.setPresentation(node.getIcon(), value.getType(), lbl, node.getChildCount() != 0); + } + } + } + + public PyNodeRenderer getRenderer() { + return myPyNodeRenderer; + } + } + + public ViewAsGroup() { + super(null, true); + } + + @NotNull + @Override + public AnAction[] getChildren(@Nullable AnActionEvent e) { + if (e == null) return EMPTY_ARRAY; + + final List values = getSelectedValues(e); + + if (values.isEmpty()) return EMPTY_ARRAY; + + for (PyDebugValue value : values) { + for (AnAction child : myChildren) { + if (!((RendererAction)child).getRenderer().isApplicable(value.getType())) return EMPTY_ARRAY; + } + } + + return myChildren; + } + + @Override + public boolean hideIfNoVisibleChildren() { + return true; + } + + @NotNull + public static List getSelectedValues(AnActionEvent e) { + return StreamEx.of(XDebuggerTreeActionBase.getSelectedNodes(e.getDataContext())) + .map(XValueNodeImpl::getValueContainer) + .select(PyDebugValue.class) + .toList(); + } +}