| package org.jetbrains.debugger; |
| |
| import com.intellij.icons.AllIcons; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.ActionCallback; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.pom.Navigatable; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiReference; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.PairConsumer; |
| import com.intellij.util.SmartList; |
| import com.intellij.util.ThreeState; |
| import com.intellij.xdebugger.ObsolescentAsyncResults; |
| import com.intellij.xdebugger.XSourcePositionWrapper; |
| 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.intellij.xdebugger.frame.presentation.XValuePresentation; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.debugger.values.*; |
| |
| import javax.swing.*; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.regex.Pattern; |
| |
| public final class VariableView extends XNamedValue implements VariableContext { |
| private static final Pattern ARRAY_DESCRIPTION_PATTERN = Pattern.compile("^[a-zA-Z]+\\[\\d+\\]$"); |
| |
| private static final class ArrayPresentation extends XValuePresentation { |
| private final String length; |
| |
| private ArrayPresentation(int length) { |
| this.length = Integer.toString(length); |
| } |
| |
| @Override |
| public void renderValue(@NotNull XValueTextRenderer renderer) { |
| renderer.renderSpecialSymbol("Array["); |
| renderer.renderSpecialSymbol(length); |
| renderer.renderSpecialSymbol("]"); |
| } |
| } |
| |
| private final VariableContext context; |
| |
| private final Variable variable; |
| |
| private volatile Value value; |
| |
| private volatile List<Variable> remainingChildren; |
| private volatile int remainingChildrenOffset; |
| |
| public VariableView(@NotNull Variable variable, @NotNull VariableContext context) { |
| this(context.getViewSupport().normalizeMemberName(variable), variable, context); |
| } |
| |
| public VariableView(@NotNull String name, @NotNull Variable variable, @NotNull VariableContext context) { |
| super(name); |
| |
| this.context = context; |
| this.variable = variable; |
| } |
| |
| @NotNull |
| public static String getClassName(@NotNull ObjectValue value) { |
| String className = value.getClassName(); |
| return StringUtil.isEmpty(className) ? "Object" : className; |
| } |
| |
| @NotNull |
| public static String getObjectValueDescription(@NotNull ObjectValue value) { |
| String description = value.getValueString(); |
| return StringUtil.isEmpty(description) ? getClassName(value) : description; |
| } |
| |
| public static void setObjectPresentation(@NotNull ObjectValue value, @NotNull Icon icon, @NotNull XValueNode node) { |
| node.setPresentation(icon, new ObjectValuePresentation(getObjectValueDescription(value)), value.hasProperties() != ThreeState.NO); |
| } |
| |
| public static void setArrayPresentation(@NotNull Value value, @NotNull VariableContext context, @NotNull final Icon icon, @NotNull XValueNode node) { |
| assert value.getType() == ValueType.ARRAY; |
| |
| if (value instanceof ArrayValue) { |
| int length = ((ArrayValue)value).getLength(); |
| node.setPresentation(icon, new ArrayPresentation(length), length > 0); |
| return; |
| } |
| |
| String valueString = value.getValueString(); |
| // only WIP reports normal description |
| if (valueString != null && valueString.endsWith("]") && ARRAY_DESCRIPTION_PATTERN.matcher(valueString).find()) { |
| node.setPresentation(icon, null, valueString, true); |
| } |
| else { |
| ObsolescentAsyncResults.consume(context.getEvaluateContext().evaluate("a.length", Collections.<String, EvaluateContextAdditionalParameter>singletonMap("a", value)), node, |
| new PairConsumer<Value, XValueNode>() { |
| @Override |
| public void consume(Value lengthValue, XValueNode node) { |
| node.setPresentation(icon, null, "Array[" + lengthValue.getValueString() + ']', true); |
| } |
| }); |
| } |
| } |
| |
| @NotNull |
| public static Icon getIcon(@NotNull Value value) { |
| ValueType type = value.getType(); |
| switch (type) { |
| case FUNCTION: |
| return AllIcons.Nodes.Function; |
| case ARRAY: |
| return AllIcons.Debugger.Db_array; |
| default: |
| return type.isObjectType() ? AllIcons.Debugger.Value : AllIcons.Debugger.Db_primitive; |
| } |
| } |
| |
| @Override |
| public boolean watchableAsEvaluationExpression() { |
| return context.watchableAsEvaluationExpression(); |
| } |
| |
| @NotNull |
| @Override |
| public DebuggerViewSupport getViewSupport() { |
| return context.getViewSupport(); |
| } |
| |
| @Nullable |
| @Override |
| public VariableContext getParent() { |
| return context; |
| } |
| |
| @NotNull |
| @Override |
| public MemberFilter createMemberFilter() { |
| return context.getViewSupport().createMemberFilter(this); |
| } |
| |
| @Override |
| public void computePresentation(@NotNull final XValueNode node, @NotNull XValuePlace place) { |
| value = variable.getValue(); |
| if (value != null) { |
| computePresentation(value, node); |
| return; |
| } |
| |
| if (!(variable instanceof ObjectProperty) || ((ObjectProperty)variable).getGetter() == null) { |
| // it is "used" expression (WEB-6779 Debugger/Variables: Automatically show used variables) |
| getEvaluateContext().evaluate(variable.getName()).doWhenDone(new Consumer<Value>() { |
| @Override |
| public void consume(Value value) { |
| if (!node.isObsolete()) { |
| VariableView.this.value = value; |
| computePresentation(value, node); |
| } |
| } |
| }).doWhenRejected(new PairConsumer<Value, String>() { |
| @Override |
| public void consume(Value value, String error) { |
| if (!node.isObsolete()) { |
| setEvaluatedValue(getViewSupport().transformErrorOnGetUsedReferenceValue(value, error), error, node); |
| } |
| } |
| }); |
| return; |
| } |
| |
| node.setPresentation(null, new XValuePresentation() { |
| @Override |
| public void renderValue(@NotNull XValueTextRenderer renderer) { |
| renderer.renderValue("\u2026"); |
| } |
| }, false); |
| node.setFullValueEvaluator(new XFullValueEvaluator(" (invoke getter)") { |
| @Override |
| public void startEvaluation(@NotNull final XFullValueEvaluationCallback callback) { |
| ValueModifier valueModifier = variable.getValueModifier(); |
| assert valueModifier != null; |
| valueModifier.evaluateGet(variable, getEvaluateContext()).doWhenProcessed(new Consumer<Value>() { |
| @Override |
| public void consume(Value value) { |
| if (!node.isObsolete()) { |
| callback.evaluated(""); |
| setEvaluatedValue(value, null, node); |
| } |
| } |
| }); |
| } |
| }.setShowValuePopup(false)); |
| } |
| |
| private void setEvaluatedValue(@Nullable Value value, @Nullable String error, @NotNull XValueNode node) { |
| if (value == null) { |
| node.setPresentation(AllIcons.Debugger.Db_primitive, null, error == null ? "Internal Error" : error, false); |
| } |
| else { |
| this.value = value; |
| computePresentation(value, node); |
| } |
| } |
| |
| @NotNull |
| static String trimFunctionDescription(@NotNull Value value) { |
| String presentableValue = value.getValueString(); |
| if (presentableValue == null) { |
| return ""; |
| } |
| |
| int endIndex = 0; |
| while (endIndex < presentableValue.length() && !StringUtil.isLineBreak(presentableValue.charAt(endIndex))) { |
| endIndex++; |
| } |
| while (endIndex > 0 && Character.isWhitespace(presentableValue.charAt(endIndex - 1))) { |
| endIndex--; |
| } |
| return presentableValue.substring(0, endIndex); |
| } |
| |
| private void computePresentation(@NotNull Value value, @NotNull XValueNode node) { |
| String valueString = value.getValueString(); |
| switch (value.getType()) { |
| case OBJECT: |
| case NODE: |
| context.getViewSupport().computeObjectPresentation(((ObjectValue)value), variable, context, node, getIcon()); |
| break; |
| |
| case FUNCTION: |
| node.setPresentation(getIcon(), new ObjectValuePresentation(trimFunctionDescription(value)), true); |
| break; |
| |
| case ARRAY: |
| context.getViewSupport().computeArrayPresentation(value, variable, context, node, getIcon()); |
| break; |
| |
| case BOOLEAN: |
| case NULL: |
| case UNDEFINED: |
| node.setPresentation(getIcon(), new XKeywordValuePresentation(valueString), false); |
| break; |
| |
| case NUMBER: |
| node.setPresentation(getIcon(), createNumberPresentation(valueString), false); |
| break; |
| |
| case STRING: { |
| node.setPresentation(getIcon(), new XStringValuePresentation(valueString), false); |
| // isTruncated in terms of debugger backend, not in our terms (i.e. sometimes we cannot control truncation), |
| // so, even in case of StringValue, we check value string length |
| if ((value instanceof StringValue && ((StringValue)value).isTruncated()) || valueString.length() > XValueNode.MAX_VALUE_LENGTH) { |
| node.setFullValueEvaluator(new MyFullValueEvaluator(value)); |
| } |
| } |
| break; |
| |
| default: |
| node.setPresentation(getIcon(), null, valueString, true); |
| } |
| } |
| |
| private static XValuePresentation createNumberPresentation(@NotNull String value) { |
| return value.equals("NaN") || value.equals("Infinity") ? new XKeywordValuePresentation(value) : new XNumericValuePresentation(value); |
| } |
| |
| @Override |
| public void computeChildren(@NotNull final XCompositeNode node) { |
| node.setAlreadySorted(true); |
| |
| if (!(value instanceof ObjectValue)) { |
| node.addChildren(XValueChildrenList.EMPTY, true); |
| return; |
| } |
| |
| List<Variable> list = remainingChildren; |
| if (list != null) { |
| int to = Math.min(remainingChildrenOffset + XCompositeNode.MAX_CHILDREN_TO_SHOW, list.size()); |
| boolean isLast = to == list.size(); |
| node.addChildren(Variables.createVariablesList(list, remainingChildrenOffset, to, this), isLast); |
| if (!isLast) { |
| node.tooManyChildren(list.size() - to); |
| remainingChildrenOffset += XCompositeNode.MAX_CHILDREN_TO_SHOW; |
| } |
| return; |
| } |
| |
| final ObjectValue objectValue = (ObjectValue)value; |
| |
| final boolean hasNamedProperties = objectValue.hasProperties() != ThreeState.NO; |
| boolean hasIndexedProperties = objectValue.hasIndexedProperties() != ThreeState.NO; |
| ActionCallback.Chunk chunk = new ActionCallback.Chunk(); |
| |
| ActionCallback additionalProperties = getViewSupport().computeAdditionalObjectProperties(objectValue, variable, this, node); |
| if (additionalProperties != null) { |
| chunk.add(additionalProperties); |
| } |
| |
| // we don't support indexed properties if additional properties added - behavior is undefined if object has indexed properties and additional properties also specified |
| if (hasIndexedProperties) { |
| chunk.add(computeIndexedProperties((ArrayValue)objectValue, node, !hasNamedProperties && additionalProperties == null)); |
| } |
| |
| if (hasNamedProperties) { |
| // named properties should be added after additional properties |
| final ActionCallback namedPropertiesCallback; |
| if (additionalProperties == null || additionalProperties.isProcessed()) { |
| namedPropertiesCallback = computeNamedProperties(objectValue, node, !hasIndexedProperties && additionalProperties == null); |
| } |
| else { |
| namedPropertiesCallback = new ActionCallback(); |
| additionalProperties.doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| computeNamedProperties(objectValue, node, true).notify(namedPropertiesCallback); |
| } |
| }).notifyWhenRejected(namedPropertiesCallback); |
| } |
| chunk.add(namedPropertiesCallback); |
| } |
| |
| if (hasIndexedProperties == hasNamedProperties || additionalProperties != null) { |
| chunk.create().doWhenProcessed(new Runnable() { |
| @Override |
| public void run() { |
| if (!node.isObsolete()) { |
| node.addChildren(XValueChildrenList.EMPTY, true); |
| } |
| } |
| }); |
| } |
| } |
| |
| public abstract static class ObsolescentIndexedVariablesConsumer extends IndexedVariablesConsumer { |
| protected final XCompositeNode node; |
| |
| protected ObsolescentIndexedVariablesConsumer(@NotNull XCompositeNode node) { |
| this.node = node; |
| } |
| |
| @Override |
| public boolean isObsolete() { |
| return node.isObsolete(); |
| } |
| } |
| |
| @NotNull |
| private ActionCallback computeIndexedProperties(@NotNull final ArrayValue value, @NotNull final XCompositeNode node, final boolean isLastChildren) { |
| return value.getIndexedProperties(0, value.getLength(), XCompositeNode.MAX_CHILDREN_TO_SHOW, new ObsolescentIndexedVariablesConsumer(node) { |
| @Override |
| public void consumeRanges(@Nullable int[] ranges) { |
| if (ranges == null) { |
| XValueChildrenList groupList = new XValueChildrenList(); |
| LazyVariablesGroup.addGroups(value, LazyVariablesGroup.GROUP_FACTORY, groupList, 0, value.getLength(), XCompositeNode.MAX_CHILDREN_TO_SHOW, VariableView.this); |
| node.addChildren(groupList, isLastChildren); |
| } |
| else { |
| LazyVariablesGroup.addRanges(value, ranges, node, VariableView.this, isLastChildren); |
| } |
| } |
| |
| @Override |
| public void consumeVariables(@NotNull List<Variable> variables) { |
| node.addChildren(Variables.createVariablesList(variables, VariableView.this, null), isLastChildren); |
| } |
| }, null); |
| } |
| |
| private ActionCallback computeNamedProperties(@NotNull final ObjectValue value, @NotNull XCompositeNode node, final boolean isLastChildren) { |
| return ObsolescentAsyncResults.consume(value.getProperties(), node, new PairConsumer<List<Variable>, XCompositeNode>() { |
| @Override |
| public void consume(List<Variable> variables, XCompositeNode node) { |
| if (value.getType() == ValueType.ARRAY && !(value instanceof ArrayValue)) { |
| computeArrayRanges(variables, node); |
| return; |
| } |
| |
| FunctionValue functionValue = value instanceof FunctionValue ? (FunctionValue)value : null; |
| if (functionValue != null && functionValue.hasScopes() == ThreeState.NO) { |
| functionValue = null; |
| } |
| |
| remainingChildren = Variables.sortFilterAndAddValueList(variables, node, VariableView.this, XCompositeNode.MAX_CHILDREN_TO_SHOW, isLastChildren && functionValue == null); |
| if (remainingChildren != null) { |
| remainingChildrenOffset = XCompositeNode.MAX_CHILDREN_TO_SHOW; |
| } |
| |
| if (functionValue != null) { |
| // we pass context as variable context instead of this variable value - we cannot watch function scopes variables, so, this variable name doesn't matter |
| node.addChildren(XValueChildrenList.bottomGroup(new FunctionScopesValueGroup(functionValue, context)), isLastChildren); |
| } |
| } |
| }); |
| } |
| |
| private void computeArrayRanges(@NotNull List<? extends Variable> properties, @NotNull XCompositeNode node) { |
| final List<Variable> variables = Variables.filterAndSort(properties, this, false); |
| int count = variables.size(); |
| int bucketSize = XCompositeNode.MAX_CHILDREN_TO_SHOW; |
| if (count <= bucketSize) { |
| node.addChildren(Variables.createVariablesList(variables, this, null), true); |
| return; |
| } |
| |
| for (; count > 0; count--) { |
| if (Character.isDigit(variables.get(count - 1).getName().charAt(0))) { |
| break; |
| } |
| } |
| |
| XValueChildrenList groupList = new XValueChildrenList(); |
| if (count > 0) { |
| LazyVariablesGroup.addGroups(variables, VariablesGroup.GROUP_FACTORY, groupList, 0, count, bucketSize, this); |
| } |
| |
| int notGroupedVariablesOffset; |
| if ((variables.size() - count) > bucketSize) { |
| for (notGroupedVariablesOffset = variables.size(); notGroupedVariablesOffset > 0; notGroupedVariablesOffset--) { |
| if (!variables.get(notGroupedVariablesOffset - 1).getName().startsWith("__")) { |
| break; |
| } |
| } |
| |
| if (notGroupedVariablesOffset > 0) { |
| LazyVariablesGroup.addGroups(variables, VariablesGroup.GROUP_FACTORY, groupList, count, notGroupedVariablesOffset, bucketSize, this); |
| } |
| } |
| else { |
| notGroupedVariablesOffset = count; |
| } |
| |
| for (int i = notGroupedVariablesOffset; i < variables.size(); i++) { |
| groupList.add(new VariableView(variables.get(i), this)); |
| } |
| |
| node.addChildren(groupList, true); |
| } |
| |
| @NotNull |
| private Icon getIcon() { |
| return getIcon(value); |
| } |
| |
| @Override |
| @Nullable |
| public XValueModifier getModifier() { |
| if (!variable.isMutable()) { |
| return null; |
| } |
| |
| return new XValueModifier() { |
| @Override |
| public String getInitialValueEditorText() { |
| if (value.getType() == ValueType.STRING) { |
| String string = value.getValueString(); |
| StringBuilder builder = new StringBuilder(string.length()); |
| builder.append('"'); |
| StringUtil.escapeStringCharacters(string.length(), string, builder); |
| builder.append('"'); |
| return builder.toString(); |
| } |
| else { |
| return value.getType().isObjectType() ? null : value.getValueString(); |
| } |
| } |
| |
| @Override |
| public void setValue(@NotNull String expression, @NotNull final XModificationCallback callback) { |
| ValueModifier valueModifier = variable.getValueModifier(); |
| assert valueModifier != null; |
| valueModifier.setValue(variable, expression, getEvaluateContext()).doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| value = null; |
| callback.valueModified(); |
| } |
| }).doWhenRejected(createErrorMessageConsumer(callback)); |
| } |
| }; |
| } |
| |
| private static Consumer<String> createErrorMessageConsumer(@NotNull final XValueCallback callback) { |
| return new Consumer<String>() { |
| @Override |
| public void consume(@Nullable String errorMessage) { |
| callback.errorOccurred(errorMessage == null ? "Internal error" : errorMessage); |
| } |
| }; |
| } |
| |
| @NotNull |
| @Override |
| public EvaluateContext getEvaluateContext() { |
| return context.getEvaluateContext(); |
| } |
| |
| @Nullable |
| public Value getValue() { |
| return variable.getValue(); |
| } |
| |
| @Override |
| public boolean canNavigateToSource() { |
| return value instanceof FunctionValue || getViewSupport().canNavigateToSource(variable, context); |
| } |
| |
| @Override |
| public void computeSourcePosition(@NotNull final XNavigatable navigatable) { |
| if (value instanceof FunctionValue) { |
| ((FunctionValue)value).resolve().doWhenDone(new Consumer<FunctionValue>() { |
| @Override |
| public void consume(final FunctionValue function) { |
| getViewSupport().getVm().getScriptManager().getScript(function).doWhenDone(new Consumer<Script>() { |
| @Override |
| public void consume(Script script) { |
| SourceInfo position = script == null ? null : getViewSupport().getSourceInfo(null, script, function.getOpenParenLine(), function.getOpenParenColumn()); |
| navigatable.setSourcePosition(position == null ? null : new XSourcePositionWrapper(position) { |
| @NotNull |
| @Override |
| public Navigatable createNavigatable(@NotNull Project project) { |
| Navigatable result = PsiVisitors.visit(myPosition, project, new PsiVisitors.Visitor<Navigatable>() { |
| @Override |
| public Navigatable visit(@NotNull PsiElement element, int positionOffset, @NotNull Document document) { |
| // element will be "open paren", but we should navigate to function name, |
| // we cannot use specific PSI type here (like JSFunction), so, we try to find reference expression (i.e. name expression) |
| PsiElement referenceCandidate = element; |
| PsiElement psiReference = null; |
| while ((referenceCandidate = referenceCandidate.getPrevSibling()) != null) { |
| if (referenceCandidate instanceof PsiReference) { |
| psiReference = referenceCandidate; |
| break; |
| } |
| } |
| |
| if (psiReference == null) { |
| referenceCandidate = element.getParent(); |
| if (referenceCandidate != null) { |
| while ((referenceCandidate = referenceCandidate.getPrevSibling()) != null) { |
| if (referenceCandidate instanceof PsiReference) { |
| psiReference = referenceCandidate; |
| break; |
| } |
| } |
| } |
| } |
| |
| PsiElement navigationElement = psiReference == null ? element.getNavigationElement() : psiReference.getNavigationElement(); |
| return navigationElement instanceof Navigatable ? (Navigatable)navigationElement : null; |
| } |
| }, null); |
| return result == null ? super.createNavigatable(project) : result; |
| } |
| }); |
| } |
| }); |
| } |
| }); |
| } |
| else { |
| getViewSupport().computeSourcePosition(variable, context, navigatable); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public String getEvaluationExpression() { |
| if (!watchableAsEvaluationExpression()) { |
| return null; |
| } |
| |
| SmartList<String> list = new SmartList<String>(variable.getName()); |
| VariableContext parent = context; |
| while (parent != null && parent.getName() != null) { |
| list.add(parent.getName()); |
| parent = parent.getParent(); |
| } |
| return context.getViewSupport().propertyNamesToString(list, false); |
| } |
| |
| private static class MyFullValueEvaluator extends XFullValueEvaluator { |
| private final Value value; |
| |
| public MyFullValueEvaluator(@NotNull Value value) { |
| super(value instanceof StringValue ? ((StringValue)value).getLength() : value.getValueString().length()); |
| |
| this.value = value; |
| } |
| |
| @Override |
| public void startEvaluation(@NotNull final XFullValueEvaluationCallback callback) { |
| if (!(value instanceof StringValue) || !((StringValue)value).isTruncated()) { |
| callback.evaluated(value.getValueString()); |
| return; |
| } |
| |
| final AtomicBoolean evaluated = new AtomicBoolean(); |
| ((StringValue)value).getFullString().doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| if (!callback.isObsolete() && evaluated.compareAndSet(false, true)) { |
| callback.evaluated(value.getValueString()); |
| } |
| } |
| }).doWhenRejected(createErrorMessageConsumer(callback)); |
| } |
| } |
| |
| @Nullable |
| @Override |
| public Scope getScope() { |
| return context.getScope(); |
| } |
| } |