blob: 9ee54199ff8c324973ed695b4bb6865038a731e9 [file] [log] [blame]
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();
}
}