package com.jetbrains.lang.dart.ide.runner.server.vmService.frame; import com.intellij.icons.AllIcons; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.LayeredIcon; import com.intellij.xdebugger.XSourcePosition; import com.intellij.xdebugger.frame.*; import com.intellij.xdebugger.frame.presentation.XKeywordValuePresentation; import com.intellij.xdebugger.frame.presentation.XNumericValuePresentation; import com.intellij.xdebugger.frame.presentation.XStringValuePresentation; import com.jetbrains.lang.dart.ide.runner.server.vmService.DartVmServiceDebugProcess; import com.jetbrains.lang.dart.ide.runner.server.vmService.VmServiceConsumers; import org.dartlang.vm.service.consumer.GetObjectConsumer; import org.dartlang.vm.service.element.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; // TODO: implement some combination of XValue.getEvaluationExpression() / // XValue.calculateEvaluationExpression() in order to support evaluate expression in variable values. // See https://youtrack.jetbrains.com/issue/WEB-17629. public class DartVmServiceValue extends XNamedValue { private static final LayeredIcon FINAL_FIELD_ICON = new LayeredIcon(AllIcons.Nodes.Field, AllIcons.Nodes.FinalMark); private static final LayeredIcon STATIC_FIELD_ICON = new LayeredIcon(AllIcons.Nodes.Field, AllIcons.Nodes.StaticMark); private static final LayeredIcon STATIC_FINAL_FIELD_ICON = new LayeredIcon(AllIcons.Nodes.Field, AllIcons.Nodes.StaticMark, AllIcons.Nodes.FinalMark); @NotNull private final DartVmServiceDebugProcess myDebugProcess; @NotNull private String myIsolateId; @NotNull private final InstanceRef myInstanceRef; @Nullable private final LocalVarSourceLocation myLocalVarSourceLocation; @Nullable private final FieldRef myFieldRef; private final boolean myIsException; private Ref<Integer> myCollectionChildrenAlreadyShown = new Ref<>(0); public DartVmServiceValue(@NotNull final DartVmServiceDebugProcess debugProcess, @NotNull final String isolateId, @NotNull final String name, @NotNull final InstanceRef instanceRef, @Nullable final LocalVarSourceLocation localVarSourceLocation, @Nullable final FieldRef fieldRef, boolean isException) { super(name); myDebugProcess = debugProcess; myIsolateId = isolateId; myInstanceRef = instanceRef; myLocalVarSourceLocation = localVarSourceLocation; myFieldRef = fieldRef; myIsException = isException; } @Override public boolean canNavigateToSource() { return myLocalVarSourceLocation != null || myFieldRef != null; } @Override public void computeSourcePosition(@NotNull final XNavigatable navigatable) { if (myLocalVarSourceLocation != null) { reportSourcePosition(myDebugProcess, navigatable, myIsolateId, myLocalVarSourceLocation.myScriptRef, myLocalVarSourceLocation.myTokenPos); } else if (myFieldRef != null) { doComputeSourcePosition(myDebugProcess, navigatable, myIsolateId, myFieldRef); } else { navigatable.setSourcePosition(null); } } static void doComputeSourcePosition(@NotNull final DartVmServiceDebugProcess debugProcess, @NotNull final XNavigatable navigatable, @NotNull final String isolateId, @NotNull final FieldRef fieldRef) { debugProcess.getVmServiceWrapper().getObject(isolateId, fieldRef.getId(), new GetObjectConsumer() { @Override public void received(final Obj field) { final SourceLocation location = ((Field)field).getLocation(); reportSourcePosition(debugProcess, navigatable, isolateId, location == null ? null : location.getScript(), location == null ? -1 : location.getTokenPos()); } @Override public void received(final Sentinel sentinel) { navigatable.setSourcePosition(null); } @Override public void onError(final RPCError error) { navigatable.setSourcePosition(null); } }); } @Override public boolean canNavigateToTypeSource() { return true; } @Override public void computeTypeSourcePosition(@NotNull final XNavigatable navigatable) { myDebugProcess.getVmServiceWrapper().getObject(myIsolateId, myInstanceRef.getClassRef().getId(), new GetObjectConsumer() { @Override public void received(final Obj classObj) { final SourceLocation location = ((ClassObj)classObj).getLocation(); reportSourcePosition(myDebugProcess, navigatable, myIsolateId, location == null ? null : location.getScript(), location == null ? -1 : location.getTokenPos()); } @Override public void received(final Sentinel response) { navigatable.setSourcePosition(null); } @Override public void onError(final RPCError error) { navigatable.setSourcePosition(null); } }); } private static void reportSourcePosition(@NotNull final DartVmServiceDebugProcess debugProcess, @NotNull final XNavigatable navigatable, @NotNull final String isolateId, @Nullable final ScriptRef script, final int tokenPos) { if (script == null || tokenPos <= 0) { navigatable.setSourcePosition(null); return; } ApplicationManager.getApplication().executeOnPooledThread(() -> { final XSourcePosition sourcePosition = debugProcess.getSourcePosition(isolateId, script, tokenPos); ApplicationManager.getApplication().runReadAction(() -> navigatable.setSourcePosition(sourcePosition)); }); } @Override public void computePresentation(@NotNull final XValueNode node, @NotNull final XValuePlace place) { if (computeVarHavingStringValuePresentation(node)) return; if (computeRegExpPresentation(node)) return; if (computeMapPresentation(node)) return; if (computeListPresentation(node)) return; computeDefaultPresentation(node); // todo handle other special kinds: Type, TypeParameter, Pattern, may be some others as well } private Icon getIcon() { if (myIsException) return AllIcons.Debugger.Db_exception_breakpoint; if (myFieldRef != null) { if (myFieldRef.isStatic() && (myFieldRef.isFinal() || myFieldRef.isConst())) { return STATIC_FINAL_FIELD_ICON; } if (myFieldRef.isStatic()) { return STATIC_FIELD_ICON; } if (myFieldRef.isFinal() || myFieldRef.isConst()) { return FINAL_FIELD_ICON; } return AllIcons.Nodes.Field; } final InstanceKind kind = myInstanceRef.getKind(); if (kind == InstanceKind.Map || isListKind(kind)) return AllIcons.Debugger.Db_array; if (kind == InstanceKind.Null || kind == InstanceKind.Bool || kind == InstanceKind.Double || kind == InstanceKind.Int || kind == InstanceKind.String) { return AllIcons.Debugger.Db_primitive; } return AllIcons.Debugger.Value; } private boolean computeVarHavingStringValuePresentation(@NotNull final XValueNode node) { // getValueAsString() is provided for the instance kinds: Null, Bool, Double, Int, String (value may be truncated), Float32x4, Float64x2, Int32x4, StackTrace switch (myInstanceRef.getKind()) { case Null: case Bool: node.setPresentation(getIcon(), new XKeywordValuePresentation(myInstanceRef.getValueAsString()), false); break; case Double: case Int: node.setPresentation(getIcon(), new XNumericValuePresentation(myInstanceRef.getValueAsString()), false); break; case String: final String presentableValue = StringUtil.replace(myInstanceRef.getValueAsString(), "\"", "\\\""); node.setPresentation(getIcon(), new XStringValuePresentation(presentableValue), false); if (myInstanceRef.getValueAsStringIsTruncated()) { addFullStringValueEvaluator(node, myInstanceRef); } break; case Float32x4: case Float64x2: case Int32x4: case StackTrace: node.setFullValueEvaluator(new ImmediateFullValueEvaluator("Click to see stack trace...", myInstanceRef.getValueAsString())); node.setPresentation(getIcon(), myInstanceRef.getClassRef().getName(), "", true); break; default: return false; } return true; } private void addFullStringValueEvaluator(@NotNull final XValueNode node, @NotNull final InstanceRef stringInstanceRef) { assert stringInstanceRef.getKind() == InstanceKind.String : stringInstanceRef; node.setFullValueEvaluator(new XFullValueEvaluator() { @Override public void startEvaluation(@NotNull final XFullValueEvaluationCallback callback) { myDebugProcess.getVmServiceWrapper().getObject(myIsolateId, stringInstanceRef.getId(), new GetObjectConsumer() { @Override public void received(Obj instance) { assert instance instanceof Instance && ((Instance)instance).getKind() == InstanceKind.String : instance; callback.evaluated(((Instance)instance).getValueAsString()); } @Override public void received(Sentinel response) { callback.errorOccurred(response.getValueAsString()); } @Override public void onError(RPCError error) { callback.errorOccurred(error.getMessage()); } }); } }); } private boolean computeRegExpPresentation(@NotNull final XValueNode node) { if (myInstanceRef.getKind() == InstanceKind.RegExp) { // The pattern is always an instance of kind String. final InstanceRef pattern = myInstanceRef.getPattern(); assert pattern.getKind() == InstanceKind.String : pattern; final String patternString = StringUtil.replace(pattern.getValueAsString(), "\"", "\\\""); node.setPresentation(getIcon(), new XStringValuePresentation(patternString) { @Nullable @Override public String getType() { return myInstanceRef.getClassRef().getName(); } }, true); if (pattern.getValueAsStringIsTruncated()) { addFullStringValueEvaluator(node, pattern); } return true; } return false; } private boolean computeMapPresentation(@NotNull final XValueNode node) { if (myInstanceRef.getKind() == InstanceKind.Map) { final String value = "size = " + myInstanceRef.getLength(); node.setPresentation(getIcon(), myInstanceRef.getClassRef().getName(), value, myInstanceRef.getLength() > 0); return true; } return false; } private boolean computeListPresentation(@NotNull final XValueNode node) { if (isListKind(myInstanceRef.getKind())) { final String value = "size = " + myInstanceRef.getLength(); node.setPresentation(getIcon(), myInstanceRef.getClassRef().getName(), value, myInstanceRef.getLength() > 0); return true; } return false; } private void computeDefaultPresentation(@NotNull final XValueNode node) { myDebugProcess.getVmServiceWrapper() .evaluateInTargetContext(myIsolateId, myInstanceRef.getId(), "toString()", new VmServiceConsumers.EvaluateConsumerWrapper() { @Override public void received(final InstanceRef toStringInstanceRef) { if (toStringInstanceRef.getKind() == InstanceKind.String) { final String string = toStringInstanceRef.getValueAsString(); // default toString() implementation returns "Instance of 'ClassName'" - no interest to show if (string.equals("Instance of '" + myInstanceRef.getClassRef().getName() + "'")) { noGoodResult(); } else { node.setPresentation(getIcon(), myInstanceRef.getClassRef().getName(), string, true); } } else { noGoodResult(); // unlikely possible } } @Override public void noGoodResult() { node.setPresentation(getIcon(), myInstanceRef.getClassRef().getName(), "", true); } }); } @Override public void computeChildren(@NotNull final XCompositeNode node) { if (myInstanceRef.getKind() == InstanceKind.Null) { node.addChildren(XValueChildrenList.EMPTY, true); return; } if ((isListKind(myInstanceRef.getKind()) || myInstanceRef.getKind() == InstanceKind.Map)) { computeCollectionChildren(node); } else { myDebugProcess.getVmServiceWrapper().getObject(myIsolateId, myInstanceRef.getId(), new GetObjectConsumer() { @Override public void received(Obj instance) { addFields(node, ((Instance)instance).getFields()); } @Override public void received(Sentinel sentinel) { node.setErrorMessage(sentinel.getValueAsString()); } @Override public void onError(RPCError error) { node.setErrorMessage(error.getMessage()); } }); } } private void computeCollectionChildren(@NotNull final XCompositeNode node) { final int offset = myCollectionChildrenAlreadyShown.get(); final int count = Math.min(myInstanceRef.getLength() - offset, XCompositeNode.MAX_CHILDREN_TO_SHOW); myDebugProcess.getVmServiceWrapper().getCollectionObject(myIsolateId, myInstanceRef.getId(), offset, count, new GetObjectConsumer() { @Override public void received(Obj instance) { if (isListKind(myInstanceRef.getKind())) { addListChildren(node, ((Instance)instance).getElements()); } else if (myInstanceRef.getKind() == InstanceKind.Map) { addMapChildren(node, ((Instance)instance).getAssociations()); } else { assert false : myInstanceRef.getKind(); } myCollectionChildrenAlreadyShown.set(myCollectionChildrenAlreadyShown.get() + count); if (offset + count < myInstanceRef.getLength()) { node.tooManyChildren(myInstanceRef.getLength() - offset - count); } } @Override public void received(Sentinel sentinel) { node.setErrorMessage(sentinel.getValueAsString()); } @Override public void onError(RPCError error) { node.setErrorMessage(error.getMessage()); } }); } private void addListChildren(@NotNull final XCompositeNode node, @NotNull final ElementList<InstanceRef> listElements) { final XValueChildrenList childrenList = new XValueChildrenList(listElements.size()); int index = myCollectionChildrenAlreadyShown.get(); for (InstanceRef listElement : listElements) { childrenList.add(new DartVmServiceValue(myDebugProcess, myIsolateId, String.valueOf(index++), listElement, null, null, false)); } node.addChildren(childrenList, true); } private void addMapChildren(@NotNull final XCompositeNode node, @NotNull final ElementList<MapAssociation> mapAssociations) { final XValueChildrenList childrenList = new XValueChildrenList(mapAssociations.size()); int index = myCollectionChildrenAlreadyShown.get(); for (MapAssociation mapAssociation : mapAssociations) { final InstanceRef keyInstanceRef = mapAssociation.getKey(); final InstanceRef valueInstanceRef = mapAssociation.getValue(); childrenList.add(String.valueOf(index++), new XValue() { @Override public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) { final String value = getShortPresentableValue(keyInstanceRef) + " -> " + getShortPresentableValue(valueInstanceRef); node.setPresentation(AllIcons.Debugger.Value, "map entry", value, true); } @Override public void computeChildren(@NotNull XCompositeNode node) { final DartVmServiceValue key = new DartVmServiceValue(myDebugProcess, myIsolateId, "key", keyInstanceRef, null, null, false); final DartVmServiceValue value = new DartVmServiceValue(myDebugProcess, myIsolateId, "value", valueInstanceRef, null, null, false); node.addChildren(XValueChildrenList.singleton(key), false); node.addChildren(XValueChildrenList.singleton(value), true); } }); } node.addChildren(childrenList, true); } private void addFields(@NotNull final XCompositeNode node, @NotNull final ElementList<BoundField> fields) { final XValueChildrenList childrenList = new XValueChildrenList(fields.size()); for (BoundField field : fields) { final InstanceRef value = field.getValue(); if (value != null) { childrenList .add(new DartVmServiceValue(myDebugProcess, myIsolateId, field.getDecl().getName(), value, null, field.getDecl(), false)); } } node.addChildren(childrenList, true); } @NotNull private static String getShortPresentableValue(@NotNull final InstanceRef instanceRef) { // getValueAsString() is provided for the instance kinds: Null, Bool, Double, Int, String (value may be truncated), Float32x4, Float64x2, Int32x4, StackTrace switch (instanceRef.getKind()) { case String: String string = instanceRef.getValueAsString(); if (string.length() > 103) string = string.substring(0, 100) + "..."; return "\"" + StringUtil.replace(string, "\"", "\\\"") + "\""; case Null: case Bool: case Double: case Int: case Float32x4: case Float64x2: case Int32x4: // case StackTrace: getValueAsString() is too long for StackTrace return instanceRef.getValueAsString(); default: return "[" + instanceRef.getClassRef().getName() + "]"; } } private static boolean isListKind(@NotNull final InstanceKind kind) { // List, Uint8ClampedList, Uint8List, Uint16List, Uint32List, Uint64List, Int8List, Int16List, Int32List, Int64List, Float32List, Float64List, Int32x4List, Float32x4List, Float64x2List return kind == InstanceKind.List || kind == InstanceKind.Uint8ClampedList || kind == InstanceKind.Uint8List || kind == InstanceKind.Uint16List || kind == InstanceKind.Uint32List || kind == InstanceKind.Uint64List || kind == InstanceKind.Int8List || kind == InstanceKind.Int16List || kind == InstanceKind.Int32List || kind == InstanceKind.Int64List || kind == InstanceKind.Float32List || kind == InstanceKind.Float64List || kind == InstanceKind.Int32x4List || kind == InstanceKind.Float32x4List || kind == InstanceKind.Float64x2List; } @NotNull public InstanceRef getInstanceRef() { return myInstanceRef; } static class LocalVarSourceLocation { @NotNull private final ScriptRef myScriptRef; private final int myTokenPos; public LocalVarSourceLocation(@NotNull final ScriptRef scriptRef, final int tokenPos) { myScriptRef = scriptRef; myTokenPos = tokenPos; } } }