package com.intellij.lang.javascript.flex.debug;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.javascript.flex.FlexBundle;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.lang.javascript.ui.JSFormatUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.PsiFormatUtilBase;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Processor;
import com.intellij.xdebugger.XDebuggerUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.stepping.PsiBackedSmartStepIntoVariant;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* @author Maxim.Mossienko
*/
class FlexSmartStepIntoHandler extends XSmartStepIntoHandler<PsiBackedSmartStepIntoVariant> {
private final FlexDebugProcess myDebugProcess;
FlexSmartStepIntoHandler(FlexDebugProcess debugProcess) {
myDebugProcess = debugProcess;
}
@NotNull
@Override
public List<PsiBackedSmartStepIntoVariant> computeSmartStepVariants(@NotNull XSourcePosition position) {
final Document document = FileDocumentManager.getInstance().getDocument(position.getFile());
final SortedMap<PsiElement, PsiBackedSmartStepIntoVariant> element2candidateMap =
new TreeMap<>((o1, o2) -> o1.getTextOffset() - o2.getTextOffset());
compute(document, element2candidateMap, new THashSet<>(), position.getLine(), position.getOffset());
final List<PsiBackedSmartStepIntoVariant> variants = new ArrayList<>();
for (final PsiElement key : element2candidateMap.keySet()) {
final PsiBackedSmartStepIntoVariant variant = element2candidateMap.get(key);
if (!variants.contains(variant)) {
variants.add(variant);
}
}
return variants;
}
private void compute(Document document,
final Map<PsiElement, PsiBackedSmartStepIntoVariant> element2candidateMap,
final THashSet<PsiElement> visited,
final int line,
final int offset) {
XDebuggerUtil.getInstance().iterateLine(myDebugProcess.getSession().getProject(), document, line, psiElement -> {
addVariants(psiElement, element2candidateMap, visited, offset);
return true;
});
}
private void addVariants(PsiElement psiElement,
final Map<PsiElement, PsiBackedSmartStepIntoVariant> element2candidateMap,
final THashSet<PsiElement> visited,
final int offset) {
PsiLanguageInjectionHost injectionHost = PsiTreeUtil.getParentOfType(psiElement, PsiLanguageInjectionHost.class);
if (injectionHost != null) {
visited.add(injectionHost);
InjectedLanguageUtil.enumerate(injectionHost, new JSResolveUtil.JSInjectedFilesVisitor() {
@Override
protected void process(JSFile file) {
Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
int fileOffsetInHost =
InjectedLanguageManager.getInstance(file.getProject()).injectedToHost(file, file.getTextRange().getStartOffset());
int offsetInInjected = offset - fileOffsetInHost;
compute(document, element2candidateMap, visited, document.getLineNumber(offsetInInjected), offset);
}
});
return;
}
JSReferenceExpression expr = PsiTreeUtil.getParentOfType(psiElement, JSReferenceExpression.class);
if (expr != null) {
if (!visited.contains(expr)) {
visited.add(expr);
PsiElement resolve = expr.resolve();
if (resolve instanceof JSFunction) {
JSFunction fun = (JSFunction)resolve;
PsiElement responsibleElement = expr;
PsiElement parent = responsibleElement.getParent();
if (parent instanceof JSDefinitionExpression) {
responsibleElement = parent.getParent();
}
else if (parent instanceof JSCallExpression) responsibleElement = parent;
element2candidateMap.put(responsibleElement, new JSFunctionSmartStepIntoVariant(fun));
}
}
}
}
@Override
public void startStepInto(@NotNull final PsiBackedSmartStepIntoVariant stepIntoVariant) {
myDebugProcess.sendCommand(new DebuggerCommand("bt", CommandOutputProcessingType.SPECIAL_PROCESSING) {
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls String s) {
startStepInto(stepIntoVariant, getStackTraceFromBtCommandOutput(s));
return super.onTextAvailable(s);
}
});
}
private void startStepInto(final PsiBackedSmartStepIntoVariant stepIntoVariant, final String[] originalStackTrace) {
myDebugProcess.sendCommand(new DebuggerCommand("step", CommandOutputProcessingType.SPECIAL_PROCESSING) {
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls String s) {
myDebugProcess.sendCommand(new DebuggerCommand("bt", CommandOutputProcessingType.SPECIAL_PROCESSING) {
@Override
CommandOutputProcessingMode onTextAvailable(@NonNls String s) {
handleStepInto(stepIntoVariant, originalStackTrace, s);
return super.onTextAvailable(s);
}
});
return super.onTextAvailable(s);
}
});
}
private void handleStepInto(final PsiBackedSmartStepIntoVariant stepIntoVariant,
final String[] originalStackTrace,
final String btCommandOutput) {
if (isCorrectFrameReached(stepIntoVariant, btCommandOutput)) {
// ok
myDebugProcess.sendCommand(new DumpSourceLocationCommand(myDebugProcess));
}
else if (!arrayEndsWith(getStackTraceFromBtCommandOutput(btCommandOutput), originalStackTrace)) {
// Impossible to perform step into, so just continue.
myDebugProcess.sendCommand(new FlexDebugProcess.ContinueCommand());
}
else {
// step out and try to step in once more
myDebugProcess.sendCommand(new DebuggerCommand("finish", CommandOutputProcessingType.SPECIAL_PROCESSING));
startStepInto(stepIntoVariant, originalStackTrace);
}
}
/**
* Splits input string and removes frame number
*/
private static String[] getStackTraceFromBtCommandOutput(final String btCommandOutput) {
final String[] frames = FlexSuspendContext.splitStackFrames(btCommandOutput);
for (int i = 0; i < frames.length; i++) {
final String frame = frames[i];
frames[i] = frame.substring(frame.indexOf(" ") + 1).trim();
}
return frames;
}
private static boolean isCorrectFrameReached(final PsiBackedSmartStepIntoVariant stepIntoVariant, final String btCommandOutput) {
final String functionName = stepIntoVariant.getElement().getName();
final String scope = FlexSuspendContext.extractScope(FlexSuspendContext.splitStackFrames(btCommandOutput)[0]);
return scope.equals(functionName) || scope.startsWith(functionName + ":") || scope.contains(" " + functionName + ":");
}
private static boolean arrayEndsWith(final String[] array, final String[] subArray) {
return ArrayUtil.startsWith(ArrayUtil.reverseArray(array), ArrayUtil.reverseArray(subArray));
}
@Override
public String getPopupTitle(@NotNull XSourcePosition position) {
return FlexBundle.message("popup.title.step.into.function");
}
private static class JSFunctionSmartStepIntoVariant extends PsiBackedSmartStepIntoVariant<JSFunction> {
public JSFunctionSmartStepIntoVariant(@NotNull JSFunction element) {
super(element);
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final JSFunctionSmartStepIntoVariant that = (JSFunctionSmartStepIntoVariant)o;
if (!getElement().equals(that.getElement())) return false;
return true;
}
public int hashCode() {
return getElement().hashCode();
}
@Override
public String getText() {
return JSFormatUtil.formatMethod(getElement(), PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_PARAMETERS, PsiFormatUtilBase.SHOW_TYPE);
}
}
}