package com.google.jstestdriver.idea.assertFramework.codeInsight;
import com.google.jstestdriver.idea.util.JsPsiUtils;
import com.intellij.codeInsight.template.Template;
import com.intellij.codeInsight.template.impl.ConstantNode;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.lang.ASTNode;
import com.intellij.lang.javascript.JSTokenTypes;
import com.intellij.lang.javascript.psi.*;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JsGeneratorUtils {
private JsGeneratorUtils() {
}
public static void generateProperty(
@NotNull JSObjectLiteralExpression objectLiteralExpression,
@NotNull GenerateActionContext context,
@NotNull String markedPropertyStr
) {
PsiElement precedingAnchor = objectLiteralExpression.getFirstChild();
if (precedingAnchor == null) {
return;
}
final int caretOffset = context.getDocumentCaretOffset();
JSProperty precedingProperty = findPrecedingProperty(objectLiteralExpression, caretOffset);
JSProperty followingProperty = findFollowingProperty(objectLiteralExpression, caretOffset);
boolean generateCommaBefore = false;
boolean generateCommaAfter = followingProperty != null;
if (precedingProperty != null) {
precedingAnchor = precedingProperty;
PsiElement comma = findNextSiblingComma(precedingProperty);
generateCommaBefore = comma == null;
if (comma != null) {
precedingAnchor = comma;
}
}
TextRange whitespaceTextRange = unionFollowingWhitespaceTextRanges(precedingAnchor);
generateProperty(context, markedPropertyStr, whitespaceTextRange, generateCommaBefore, generateCommaAfter);
}
private static void generateProperty(
@NotNull GenerateActionContext context,
@NotNull String markedPropertyStr,
@NotNull TextRange whitespaceTextRange,
boolean commaBeforeRequired,
boolean commaAfterRequired
) {
final int caretOffset = context.getDocumentCaretOffset();
final boolean insideWhitespaceArea = whitespaceTextRange.contains(caretOffset);
int moveCaretToOffset = insideWhitespaceArea ? caretOffset : whitespaceTextRange.getStartOffset();
if (commaBeforeRequired) {
generateCommaAt(context, whitespaceTextRange.getStartOffset());
moveCaretToOffset++;
}
context.getCaretModel().moveToOffset(moveCaretToOffset);
LineRange whitespaceLineRange = createLineRangeByTextRange(context, whitespaceTextRange);
int caretLineNumber = getLineNumberAtOffset(context, moveCaretToOffset);
String leadingNewLine = "";
if (caretLineNumber == whitespaceLineRange.getStartLine()) {
leadingNewLine = "\n";
}
Template template = createDefaultTemplate(leadingNewLine + markedPropertyStr);
if (commaAfterRequired) {
template.addTextSegment(",");
}
if (whitespaceLineRange.getStartLine() == whitespaceLineRange.getEndLine()) {
template.addTextSegment("\n");
}
context.startTemplate(template);
}
private static void generateCommaAt(@NotNull GenerateActionContext context, final int offset) {
context.getCaretModel().moveToOffset(offset);
Template template = createDefaultTemplate(",");
context.startTemplate(template);
}
@Nullable
private static JSProperty findPrecedingProperty(@NotNull JSObjectLiteralExpression objectLiteralExpression, int caretOffset) {
JSProperty[] properties = JsPsiUtils.getProperties(objectLiteralExpression);
JSProperty preceding = null;
for (JSProperty currentProperty : properties) {
int endOffset = currentProperty.getTextRange().getEndOffset();
if (currentProperty.getTextRange().getStartOffset() < caretOffset && caretOffset < endOffset) {
return currentProperty;
}
if (endOffset <= caretOffset) {
if (preceding == null || preceding.getTextRange().getEndOffset() < endOffset) {
preceding = currentProperty;
}
}
}
return preceding;
}
@Nullable
private static JSProperty findFollowingProperty(@NotNull JSObjectLiteralExpression objectLiteralExpression, int caretOffset) {
JSProperty[] properties = JsPsiUtils.getProperties(objectLiteralExpression);
JSProperty following = null;
for (JSProperty property : properties) {
int startOffset = property.getTextRange().getStartOffset();
if (caretOffset <= startOffset) {
if (following == null || startOffset < following.getTextRange().getStartOffset()) {
following = property;
}
}
}
return following;
}
@Nullable
public static PsiElement findNextSiblingComma(@NotNull PsiElement precedingAnchor) {
PsiElement next = precedingAnchor.getNextSibling();
while (next instanceof ASTNode) {
ASTNode node = (ASTNode) next;
IElementType elementType = node.getElementType();
if (elementType == JSTokenTypes.COMMA) {
return next;
} else if (elementType != JSTokenTypes.WHITE_SPACE) {
break;
}
next = next.getNextSibling();
}
return null;
}
@NotNull
private static TextRange unionFollowingWhitespaceTextRanges(@NotNull final PsiElement element) {
int startOffset = element.getTextRange().getEndOffset();
int endOffset = startOffset;
PsiElement e = element.getNextSibling();
while (e != null) {
if (JsPsiUtils.isElementOfType(e, JSTokenTypes.WHITE_SPACE)) {
endOffset = e.getTextRange().getEndOffset();
e = e.getNextSibling();
} else {
break;
}
}
return TextRange.create(startOffset, endOffset);
}
public static void generateObjectLiteralWithPropertyAsArgument(
@NotNull GenerateActionContext context,
@NotNull String markedPropertyStr,
@NotNull JSArgumentList argumentList,
int addAtPosition
) {
JSExpression[] expressions = JsPsiUtils.getArguments(argumentList);
if (expressions.length < addAtPosition) {
return;
}
PsiElement precedingElement = addAtPosition == 0 ? argumentList.getFirstChild() : expressions[addAtPosition - 1];
if (precedingElement == null) {
return;
}
PsiElement comma = findNextSiblingComma(precedingElement);
if (comma != null) {
precedingElement = comma;
}
context.getCaretModel().moveToOffset(precedingElement.getTextRange().getEndOffset());
String leadingPrefix = comma == null && addAtPosition != 0 ? "," : "";
Template template = createDefaultTemplate(leadingPrefix + markedPropertyStr);
context.startTemplate(template);
}
/**
* @param psiElement {@code com.intellij.psi.PsiElement} under caret
* @param caretOffset caret document offset
* @return suitable offset for new statement insertion, preferably without caret moving.
*/
public static int findSuitableOffsetForNewStatement(@NotNull PsiElement psiElement, final int caretOffset) {
PsiElement parent = psiElement.getParent();
while (parent != null) {
if (parent instanceof JSBlockStatement) {
break;
}
if (parent instanceof PsiFile) {
break;
}
psiElement = parent;
parent = parent.getParent();
}
if (JsPsiUtils.isElementOfType(psiElement, JSTokenTypes.RBRACE)) {
return psiElement.getTextRange().getStartOffset();
}
final TextRange whitespaceTextRange;
if (JsPsiUtils.isElementOfType(psiElement, JSTokenTypes.WHITE_SPACE)) {
whitespaceTextRange = psiElement.getTextRange();
} else {
whitespaceTextRange = unionFollowingWhitespaceTextRanges(psiElement);
}
if (whitespaceTextRange.containsOffset(caretOffset)) {
return caretOffset;
}
return whitespaceTextRange.getStartOffset();
}
@NotNull
public static Template createDefaultTemplate(@Nullable String markedText) {
Template template = new TemplateImpl("", "");
template.setToIndent(true);
template.setToReformat(true);
template.setToShortenLongNames(false);
template.setInline(false);
fillTemplateWithMarkedText(template, markedText);
return template;
}
public static void fillTemplateWithMarkedText(@NotNull Template template, @Nullable String markedText) {
if (markedText == null) {
return;
}
Pattern p = Pattern.compile("\\$\\{(.+?)\\}");
Matcher m = p.matcher(markedText);
int startInd = 0;
do {
boolean variableFound = m.find();
final String plainText;
if (variableFound) {
plainText = markedText.substring(startInd, m.start());
} else {
plainText = markedText.substring(startInd);
}
fillTemplateWithPlainText(template, plainText);
if (variableFound) {
String variableName = m.group(1);
template.addVariable(
variableName.replaceAll(" ", "_"),
new ConstantNode(variableName),
new ConstantNode(variableName),
true
);
startInd = m.end();
} else {
startInd = markedText.length();
}
} while (startInd < markedText.length());
}
private static void fillTemplateWithPlainText(Template template, String plaintText) {
int startInd = 0;
do {
int caretIndex = plaintText.indexOf('|', startInd);
int endInd = caretIndex >= 0 ? caretIndex : plaintText.length();
String txt = plaintText.substring(startInd, endInd);
if (txt.length() > 0) {
template.addTextSegment(txt);
}
if (caretIndex >= 0) {
template.addEndVariable();
}
startInd = endInd + 1;
} while (startInd < plaintText.length());
}
private static int getLineNumberAtOffset(GenerateActionContext context, int offset) {
return context.getDocument().getLineNumber(offset);
}
private static LineRange createLineRangeByTextRange(GenerateActionContext context, TextRange textRange) {
return new LineRange(
getLineNumberAtOffset(context, textRange.getStartOffset()),
getLineNumberAtOffset(context, textRange.getEndOffset())
);
}
private static class LineRange {
private final int myStartLine;
private final int myEndLine;
private LineRange(int startLine, int endLine) {
myStartLine = startLine;
myEndLine = endLine;
}
public int getStartLine() {
return myStartLine;
}
public int getEndLine() {
return myEndLine;
}
}
}