package com.intellij.lang.javascript.flex.debug;
import com.intellij.icons.AllIcons;
import com.intellij.javascript.JSDebuggerSupportUtils;
import com.intellij.lang.javascript.JavaScriptSupportLoader;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.resolve.JSImportHandlingUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.ui.ColoredTextContainer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.xdebugger.XDebuggerUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.evaluation.ExpressionInfo;
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator;
import com.intellij.xdebugger.frame.XCompositeNode;
import com.intellij.xdebugger.frame.XStackFrame;
import com.intellij.xdebugger.frame.XValueChildrenList;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
/**
* @author nik
*/
public class FlexStackFrame extends XStackFrame {
private static final String ANONYMOUS = "<anonymous>";
private final FlexDebugProcess myDebugProcess;
@Nullable private final XSourcePosition mySourcePosition;
@Nullable private final String myFileNameIfSourcePositionIsNull; // for presentation only
private final int myLineIfSourcePositionIsNull; // for presentation only
@NonNls static final String DELIM = " = ";
private Map<String,String> qName2IdMap;
private List<String> scopeChain;
private final XDebuggerEvaluator myXDebuggerEvaluator = new FlexDebuggerEvaluator();
private String myScope = UNKNOWN_SCOPE;
private int myFrameIndex;
@NonNls protected static final String UNKNOWN_SCOPE = "<unknown>";
static final String CLASS_MARKER = ", class='";
static final String CANNOT_EVALUATE_EXPRESSION = "Cannot evaluate expression: ";
FlexStackFrame(final FlexDebugProcess debugProcess, final @Nullable XSourcePosition sourcePosition) {
myDebugProcess = debugProcess;
mySourcePosition = sourcePosition;
myFileNameIfSourcePositionIsNull = null;
myLineIfSourcePositionIsNull = -1;
}
/**
* Use this constructor only if it is not possible to find existing VirtualFile and create corresponding XSourcePosition
*/
FlexStackFrame(final FlexDebugProcess debugProcess, @Nullable final String fileName, final int line) {
myDebugProcess = debugProcess;
mySourcePosition = null;
myFileNameIfSourcePositionIsNull = "<null>".equals(fileName) ? null : fileName;
myLineIfSourcePositionIsNull = line;
}
@Override
@Nullable
public XSourcePosition getSourcePosition() {
return mySourcePosition;
}
@Override
public void computeChildren(@NotNull final XCompositeNode node) {
List<DebuggerCommand> commands = new ArrayList<>();
commands.add(new MyDebuggerCommand("print this", node, true, FlexValue.ValueType.This));
commands.add(new MyDebuggerCommand("info arguments", node, false, FlexValue.ValueType.Parameter));
commands.add(new MyDebuggerCommand("info locals", node, false, FlexValue.ValueType.Variable));
//commands.add(new MyDebuggerCommand("info variables", node, false));
if (mySourcePosition != null) {
commands.add(new DebuggerCommand("does not matter", CommandOutputProcessingType.SPECIAL_PROCESSING) {
@Override
public void post(FlexDebugProcess flexDebugProcess) throws IOException {
ensureQName2IdMapLoaded();
final XValueChildrenList resultChildren = new XValueChildrenList(1);
Boolean insideFunExpr = ReadAction.compute(() -> {
Project project = getDebugProcess().getSession().getProject();
PsiElement element =
XDebuggerUtil.getInstance().findContextElement(mySourcePosition.getFile(), mySourcePosition.getOffset(), project, true);
JSFunction function = PsiTreeUtil.getParentOfType(element, JSFunction.class);
return function instanceof JSFunctionExpression;
});
if (Boolean.TRUE.equals(insideFunExpr)) {
// public function outer(outerArg:String):Function {
// return function middle(middleArg:String):Function {
// return function inner(innerArg:String):String {
// return outerArg + middleArg + innerArg; // [BREAKPOINT]
// }
// }
// }
//
// info scopechain gives following:
// 0 = [Object 103989057, class='global']
// 1 = [Object 101988361, class='Object']
// 2 = [Object 101989657, class='<anonymous>']
// 3 = [Object 103989057, class='global']
// 4 = [Object 102001417, class='Object']
// 5 = [Object 102001873, class='pack::HelloFlex4/outer']
// 6 = [Object 106803361, class='pack::HelloFlex4']
// 7 = [Object 105768929, class='pack::HelloFlex4$']
// 8 = [Object 54750369, class='spark.components::Application$']
// 9 = ...
// Interesting for us: closures and one object after the last closure,
// i.e. in this case #2, #5 and #6.
scopeChain = new ArrayList<>(2);
String firstTokenAfterLastClosure = null;
for (String token : qName2IdMap.keySet()) {
final int slashIndex = token.indexOf('/');
if (slashIndex != -1 || token.contains(ANONYMOUS)) {
final String funName = token.substring(slashIndex + 1);
addScopeChainElement(token, funName, resultChildren);
}
else if (firstTokenAfterLastClosure == null && resultChildren.size() > 0) {
firstTokenAfterLastClosure = token;
}
}
if (firstTokenAfterLastClosure != null) {
addScopeChainElement(firstTokenAfterLastClosure, firstTokenAfterLastClosure, resultChildren);
}
}
node.addChildren(resultChildren, false);
}
@Override
public String read(FlexDebugProcess flexDebugProcess) throws IOException {
return "";
}
private void addScopeChainElement(final String token, final String funName, final XValueChildrenList resultChildren) {
final String id = qName2IdMap.get(token);
final String path = "#" + validObjectId(id);
scopeChain.add(path);
final String name = "Locals of " + funName;
final String flexValueResult = "[Object " + id + CLASS_MARKER + token + "']";
resultChildren.add(name, new FlexValue(FlexStackFrame.this, myDebugProcess, mySourcePosition, name, path, flexValueResult, null,
FlexValue.ValueType.ScopeChainEntry));
}
});
}
myDebugProcess.sendCommand(
new CompositeDebuggerCommand(node, commands.toArray(new DebuggerCommand[commands.size()])) {
@Override
protected void obsolete() {
super.obsolete();
node.addChildren(XValueChildrenList.EMPTY, true);
}
@Override
protected void succeeded() {
super.succeeded();
node.addChildren(XValueChildrenList.EMPTY, true);
}
}
);
}
private String addFrameOffset(String text) {
text="frame " + (myFrameIndex != 0 ? myFrameIndex: "")+ "\n"+text;
return text;
}
@Override
public XDebuggerEvaluator getEvaluator() {
return myXDebuggerEvaluator;
}
public void setFrameIndex(final int frameIndex) {
myFrameIndex = frameIndex;
}
private @NonNls String buildCommandForExpression(final String _expression) {
if (_expression.indexOf('=') != -1) {
String evalCommand = ReadAction.compute(() -> {
final PsiFile fromText =
PsiFileFactory.getInstance(myDebugProcess.getSession().getProject())
.createFileFromText("A.js2", JavaScriptSupportLoader.ECMA_SCRIPT_L4, _expression);
final PsiElement[] elements = fromText.getChildren();
if (elements.length == 1 && elements[0] instanceof JSExpressionStatement) {
final JSExpression expression = ((JSExpressionStatement)elements[0]).getExpression();
if (expression instanceof JSAssignmentExpression) {
JSAssignmentExpression expr = (JSAssignmentExpression)expression;
final JSExpression lOperand = expr.getLOperand();
final String lOperandText = lOperand == null ? null : lOperand.getText();
final JSExpression rOperand = expr.getROperand();
if (lOperandText != null && rOperand != null) {
return addFrameOffset("set " + lOperandText + " = " + rOperand.getText() + "\nprint " + lOperandText);
}
}
}
return null;
});
if (evalCommand != null) return evalCommand;
}
return addFrameOffset("print " + _expression);
}
private void ensureQName2IdMapLoaded() {
if (qName2IdMap != null) return;
qName2IdMap = myDebugProcess.getQName2IdIfSameEqualityObject(getEqualityObject());
if (qName2IdMap != null) return;
qName2IdMap = new LinkedHashMap<>();
final DebuggerCommand command = new DebuggerCommand("info scopechain", CommandOutputProcessingType.SPECIAL_PROCESSING) {
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls final String s) {
final StringTokenizer tokenizer = new StringTokenizer(s, "\r\n");
while (tokenizer.hasMoreElements()) {
String line = tokenizer.nextToken();
// 1 = [Object 22610377, class='A$']
int lBracketPos = line.indexOf('[');
int rBracketPos = line.lastIndexOf(']');
if (lBracketPos == -1 || rBracketPos == -1) continue;
line = line.substring(lBracketPos + 1, rBracketPos);
String id = line.substring(line.indexOf(' ') + 1, line.indexOf(','));
String qName = line.substring(line.indexOf('\'') + 1, line.lastIndexOf('\''));
qName = qName.replace("::",".");
qName2IdMap.put(qName, id);
}
myDebugProcess.setQName2Id(qName2IdMap, getEqualityObject());
return CommandOutputProcessingMode.DONE;
}
};
myDebugProcess.sendAndProcessOneCommand(command, e -> {
FlexDebugProcess.log(e);
return null;
});
}
class EvaluateCommand extends DebuggerCommand {
private String result;
private final XDebuggerEvaluator.XEvaluationCallback callback;
private final String expression;
private int responseCount;
private boolean myFinished;
EvaluateCommand(String _expression, final XDebuggerEvaluator.XEvaluationCallback _callback) {
super(buildCommandForExpression(_expression), CommandOutputProcessingType.SPECIAL_PROCESSING);
expression = _expression;
callback = _callback;
}
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls String line) {
if (myDebugProcess.filterStdResponse(line)) return CommandOutputProcessingMode.PROCEEDING;
return proceedWithEvaluationResponse(line);
}
private CommandOutputProcessingMode proceedWithEvaluationResponse(String line) {
++responseCount;
if (responseCount == 1) { // skip frame
return CommandOutputProcessingMode.PROCEEDING;
}
return doOnTextAvailable(line);
}
CommandOutputProcessingMode doOnTextAvailable(@NonNls String s) {
if (cannotEvaluateResponse(s) && mySourcePosition != null) {
ensureQName2IdMapLoaded();
evaluateFromTypeMap();
return CommandOutputProcessingMode.DONE;
}
if (getText().contains("\n") && s.length() == 0) { // implicit set command was issued with empty result
return CommandOutputProcessingMode.PROCEEDING;
}
dispatchResult(s);
return CommandOutputProcessingMode.DONE;
}
private boolean cannotEvaluateResponse(String s) {
return s.contains("could not be evaluated");
}
private void evaluateFromTypeMap() {
assert mySourcePosition != null;
final int dotPos = expression.indexOf('.');
final String typeName = dotPos != -1 ? expression.substring(0, dotPos):expression;
final String resolvedName = DumbService.getInstance(myDebugProcess.getSession().getProject()).runReadActionInSmartMode(() -> {
final VirtualFile virtualFile = mySourcePosition.getFile();
final PsiFile file = PsiManager.getInstance(myDebugProcess.getSession().getProject()).findFile(virtualFile);
final int offset = mySourcePosition.getOffset();
PsiElement element = file == null ? null : file.findElementAt(offset);
if (file instanceof XmlFile) {
final PsiLanguageInjectionHost psiLanguageInjectionHost =
PsiTreeUtil.getParentOfType(element, PsiLanguageInjectionHost.class);
if (psiLanguageInjectionHost != null) {
final Ref<PsiElement> result = new Ref<>();
InjectedLanguageUtil.enumerate(psiLanguageInjectionHost, (injectedPsi, places) -> {
final int injectedStart = InjectedLanguageUtil.getInjectedStart(places);
result.set(injectedPsi.findElementAt(offset - injectedStart + (places.get(0).getPrefix().length())));
});
element = result.get();
}
}
return element == null ? typeName : JSImportHandlingUtil.resolveTypeName(typeName, element);
});
boolean isGlobal = false;
boolean handled = false;
if (!resolvedName.equals(typeName) || (isGlobal = typeName.equals("global"))) {
final String id = qName2IdMap.get(resolvedName + (isGlobal ? "":"$"));
if (id != null) {
handled = true;
DebuggerCommand evaluateCommand = new EvaluateCommand("#"+id+(dotPos != -1 ?expression.substring(dotPos):""), callback) {
@Override
CommandOutputProcessingMode doOnTextAvailable(@NonNls final String s) {
dispatchResult(s);
return CommandOutputProcessingMode.DONE;
}
};
myDebugProcess.sendAndProcessOneCommand(evaluateCommand, e -> {
FlexDebugProcess.log(e);
return null;
});
}
} else if (scopeChain != null) {
for(String id2:scopeChain) {
final Ref<Boolean> resolved = new Ref<>();
DebuggerCommand evaluateCommand = new EvaluateCommand(id2 + "." + expression, callback) {
@Override
CommandOutputProcessingMode doOnTextAvailable(@NonNls final String s) {
if (!cannotEvaluateResponse(s)) {
resolved.set(Boolean.TRUE);
dispatchResult(s);
}
return CommandOutputProcessingMode.DONE;
}
};
myDebugProcess.sendAndProcessOneCommand(evaluateCommand, e -> {
FlexDebugProcess.log(e);
return null;
});
if (resolved.get() == Boolean.TRUE) {
handled = true;
break;
}
}
}
if (!handled) {
dispatchResult(CANNOT_EVALUATE_EXPRESSION + expression);
}
}
protected void dispatchResult(String s) {
final int i = s.indexOf(DELIM);
if (i != -1) s = s.substring(i + DELIM.length());
result = s.trim();
if (callback != null) {
ApplicationManager.getApplication().executeOnPooledThread(
() -> callback.evaluated(new FlexValue(FlexStackFrame.this, myDebugProcess, mySourcePosition, expression, expression, result, null,
FlexValue.ValueType.Other)));
} else {
synchronized (this) {
myFinished = true;
notify();
}
}
}
void waitTillExecutionEnd() {
synchronized (this) {
if (myFinished) return;
try { while(true) { wait(); if (result != null) break;} }
catch (InterruptedException ex) {
FlexDebugProcess.log(ex);
return;
}
}
}
}
static String validObjectId(String s) {
// some object ids from Flash player are negative (e.g. on Linux) and can not be consumed back e.g. for tracing
// so we transform them into unsigned ones assuming there is just sign transmition problem (see IDEA-49837)
long idVal = Long.parseLong(s);
return s.charAt(0) == '-' ? Long.toString(idVal & 0xFFFFFFFFL) : Long.toString(idVal);
}
private class FlexDebuggerEvaluator extends XDebuggerEvaluator {
public boolean isCodeFragmentEvaluationSupported() {
return false;
}
@Override
public void evaluate(@NotNull final String expression, @NotNull final XEvaluationCallback callback, @Nullable XSourcePosition expressionPosition) {
final EvaluateCommand command = new EvaluateCommand(expression, callback);
myDebugProcess.sendCommand(command);
}
@Nullable
@Override
public ExpressionInfo getExpressionInfoAtOffset(@NotNull Project project, @NotNull Document document, final int offset, boolean sideEffectsAllowed) {
return JSDebuggerSupportUtils.getExpressionAtOffset(project, document, offset);
}
}
String eval(final String expression, FlexDebugProcess process) {
final EvaluateCommand command = new EvaluateCommand(expression, null);
process.sendAndProcessOneCommand(command, null);
return command.result;
}
public FlexDebugProcess getDebugProcess() {
return myDebugProcess;
}
@Override
public void customizePresentation(@NotNull final ColoredTextContainer component) {
component.append(myScope, SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (mySourcePosition != null) {
component.append(" in ", SimpleTextAttributes.REGULAR_ATTRIBUTES);
component.append(mySourcePosition.getFile().getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
component.append(":" + (mySourcePosition.getLine() + 1), SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
else if (myFileNameIfSourcePositionIsNull != null) {
component.append(" in ", SimpleTextAttributes.REGULAR_ATTRIBUTES);
component.append(myFileNameIfSourcePositionIsNull, SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (myLineIfSourcePositionIsNull >= 0) {
component.append(":" + myLineIfSourcePositionIsNull, SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
}
component.setIcon(AllIcons.Debugger.StackFrame);
}
public void setScope(final String scope) {
myScope = scope;
}
private String myQualifiedFunctionName;
@Override
public Object getEqualityObject() {
if (myQualifiedFunctionName == null) { // myScope is filled async
if (mySourcePosition != null) {
final FlexDebugProcess flexDebugProcess = getDebugProcess();
final Project project = flexDebugProcess.getSession().getProject();
final VirtualFile file = mySourcePosition.getFile();
final JSFunction function = ApplicationManager.getApplication().runReadAction((NullableComputable<JSFunction>)() -> {
final PsiElement element = XDebuggerUtil.getInstance().findContextElement(file, mySourcePosition.getOffset(), project, true);
return PsiTreeUtil.getParentOfType(element, JSFunction.class);
});
String name;
myQualifiedFunctionName = file.getPath() + (function != null ? "#" + ((name = function.getName()) != null ? name:function.getTextOffset()) :"");
} else {
myQualifiedFunctionName = "unknown";
}
}
return myQualifiedFunctionName;
}
private class MyDebuggerCommand extends DebuggerCommand {
private final boolean hasFrame;
private final XValueChildrenList resultChildren;
private int current;
private final XCompositeNode myNode;
private final FlexValue.ValueType myValueType;
public MyDebuggerCommand(String text, XCompositeNode node, boolean _hasFrame, FlexValue.ValueType valueType) {
super(_hasFrame ? addFrameOffset(text):text, CommandOutputProcessingType.SPECIAL_PROCESSING);
myNode = node;
resultChildren = new XValueChildrenList(3);
hasFrame = _hasFrame;
myValueType = valueType;
}
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls final String s) {
final int offsetIndex = hasFrame ? 1:0; // frame command
if (current >= offsetIndex) {
final StringTokenizer tokenizer = new StringTokenizer(s, "\r\n", true);
Pair<String, StringBuilder> previousNameAndValue = null; // may be incomplete if value contains new line symbols
while (tokenizer.hasMoreElements()) {
final String token = tokenizer.nextToken();
if (token.length() == 0) continue;
if (token.charAt(0) == '\r' || token.charAt(0) == '\n') {
// Tokenizer delimiter may be a part of String variable value
if (previousNameAndValue != null) {
previousNameAndValue.second.append(token);
}
else {
FlexDebugProcess.log("Unexpected token: [" + token + "], full string: [" + s + "]");
}
continue;
}
final int i = token.indexOf(DELIM);
if (i == -1) {
if (previousNameAndValue != null) {
previousNameAndValue.second.append(token);
}
else {
FlexDebugProcess.log("Unexpected token: [" + token + "], full string: [" + s + "]");
}
continue;
}
@NonNls String name = token.substring(0, i);
if (name.startsWith("$")) { // $x is legal variable name, this evaluation is $1
boolean completeDigits = name.length() > 1;
for (int j = 1; j < name.length(); ++j) {
completeDigits &= Character.isDigit(name.charAt(j));
if (!completeDigits) break;
}
if (completeDigits) name = "this";
}
if (previousNameAndValue != null) {
String prevName = previousNameAndValue.first;
resultChildren.add(prevName, new FlexValue(FlexStackFrame.this, myDebugProcess, mySourcePosition, prevName, prevName,
removeTrailingNewLines(previousNameAndValue.second), null, myValueType));
}
previousNameAndValue = Pair.create(name, new StringBuilder(token.substring(i + DELIM.length())));
}
if (previousNameAndValue != null) {
String prevName = previousNameAndValue.first;
resultChildren.add(prevName, new FlexValue(FlexStackFrame.this, myDebugProcess, mySourcePosition, prevName, prevName,
removeTrailingNewLines(previousNameAndValue.second), null, myValueType));
}
}
++current;
if (current == offsetIndex + 1) {
myNode.addChildren(resultChildren, false);
return CommandOutputProcessingMode.DONE;
} else {
return CommandOutputProcessingMode.PROCEEDING;
}
}
private String removeTrailingNewLines(final StringBuilder builder) {
while (builder.length() > 0 && ((builder.charAt(builder.length() - 1) == '\r') || builder.charAt(builder.length() - 1) == '\n')) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
}
}
}