package com.jetbrains.lang.dart.ide.moveCode;
import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
import com.intellij.codeInsight.editorActions.moveUpDown.StatementUpDownMover;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.lang.dart.DartLanguage;
import com.jetbrains.lang.dart.DartTokenTypes;
import com.jetbrains.lang.dart.psi.*;
import com.jetbrains.lang.dart.util.DartPsiImplUtil;
import com.jetbrains.lang.dart.util.DartRefactoringUtil;
import com.jetbrains.lang.dart.util.UsefulPsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.jetbrains.lang.dart.DartTokenTypes.*;
import static com.jetbrains.lang.dart.util.DartRefactoringUtil.*;
/**
* Move executable statements within a method or function. Statements cannot be moved outside a component.
* TODO: What about moving statements from a local function to the surrounding method?
*/
public class DartStatementMover extends LineMover {
static final TokenSet NESTED_GUARDS = TokenSet.create(LIST_LITERAL_EXPRESSION, ARGUMENT_LIST, ARGUMENTS);
private SmartPsiElementPointer statementToSurroundWithCodeBlock;
@Override
public void afterMove(@NotNull final Editor editor, @NotNull final PsiFile file, @NotNull final MoveInfo info, final boolean down) {
super.afterMove(editor, file, info, down);
statementToSurroundWithCodeBlock = null;
}
@Override
public void beforeMove(@NotNull final Editor editor, @NotNull final MoveInfo info, final boolean down) {
super.beforeMove(editor, info, down);
if (statementToSurroundWithCodeBlock != null) {
surroundWithCodeBlock(info, down);
}
}
private void surroundWithCodeBlock(@NotNull final MoveInfo info, final boolean down) {
// TODO Implement surroundWithCodeBlock()
}
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!(file instanceof DartFile)) return false;
if (!super.checkAvailable(editor, file, info, down)) return false;
LineRange range = expandLineRangeToCoverPsiElements(info.toMove, editor, file);
if (range == null) return false;
info.toMove = range;
final int startOffset = editor.logicalPositionToOffset(new LogicalPosition(range.startLine, 0));
final int endOffset = editor.logicalPositionToOffset(new LogicalPosition(range.endLine, 0));
PsiElement[] statements = DartRefactoringUtil.findListExpressionInRange(file, startOffset, endOffset);
if (statements.length == 1) {
info.toMove2 = null; // Disallow component mover
return true; // Require trailing comma
}
if (statements.length == 0) {
statements = DartRefactoringUtil.findStatementsInRange(file, startOffset, endOffset);
}
if (statements.length == 0) return false;
range.firstElement = statements[0];
range.lastElement = statements[statements.length - 1];
info.indentTarget = true;
if (!checkMovingInsideOutside(file, editor, info, down)) {
info.toMove2 = null;
}
return true;
}
private static LineRange expandLineRangeToCoverPsiElements(final LineRange range, Editor editor, final PsiFile file) {
Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, range);
if (psiRange == null) {
return null;
}
if (psiRange.first instanceof DartStatements ||
psiRange.first instanceof DartExpressionList ||
psiRange.first instanceof DartArgumentList) {
PsiElement first = psiRange.first;
PsiElement last = psiRange.second;
if (last != null) {
PsiElement statement = first.getFirstChild();
if (statement != null) {
psiRange = Pair.create(statement, last);
}
}
}
else if (psiRange.first instanceof DartNamedArgument && psiRange.first.getParent() == psiRange.second) {
psiRange = Pair.create(psiRange.first, psiRange.first);
}
if (psiRange.second instanceof DartStatements) {
PsiElement first = psiRange.first;
PsiElement last = psiRange.second;
if (PsiTreeUtil.isAncestor(last, first, false)) {
PsiElement statement = last.getLastChild();
if (statement != null) {
psiRange = Pair.create(first, statement);
}
}
}
if (isComma(psiRange.second)) {
PsiElement first = psiRange.first;
PsiElement last = UsefulPsiTreeUtil.getPrevSiblingSkipWhiteSpacesAndComments(psiRange.second, true);
if (PsiTreeUtil.isAncestor(last, first, false)) {
PsiElement statement = last.getLastChild();
if (statement != null) {
psiRange = Pair.create(first, statement);
}
}
}
final PsiElement parent = PsiTreeUtil.findCommonParent(psiRange.first, psiRange.second);
Pair<PsiElement, PsiElement> elementRange = getElementRange(parent, psiRange.first, psiRange.second);
if (elementRange == null) {
return null;
}
int endOffset = elementRange.second.getTextRange().getEndOffset();
Document document = editor.getDocument();
if (endOffset > document.getTextLength()) {
return null;
}
int endLine;
if (endOffset == document.getTextLength()) {
endLine = document.getLineCount();
}
else {
endLine = editor.offsetToLogicalPosition(endOffset).line + 1;
endLine = Math.min(endLine, document.getLineCount());
}
int startLine = Math.min(range.startLine, editor.offsetToLogicalPosition(elementRange.first.getTextOffset()).line);
endLine = Math.max(endLine, range.endLine);
return new LineRange(startLine, endLine);
}
private boolean checkMovingInsideOutside(PsiFile file, final Editor editor, @NotNull final MoveInfo info, final boolean down) {
final int offset = editor.getCaretModel().getOffset();
PsiElement elementAtOffset = file.getViewProvider().findElementAt(offset, DartLanguage.INSTANCE);
if (elementAtOffset == null) return false;
PsiElement guard = elementAtOffset;
boolean isExpr = isMovingExpr(info.toMove);
if (isExpr) {
guard = PsiTreeUtil
.getParentOfType(guard, DartMethodDeclaration.class, DartListLiteralExpression.class, DartArgumentList.class, DartArguments.class,
DartFunctionDeclarationWithBodyOrNative.class, DartClass.class, PsiComment.class);
}
else {
guard = PsiTreeUtil.getParentOfType(guard, DartMethodDeclaration.class,
DartFunctionDeclarationWithBodyOrNative.class, DartClass.class, PsiComment.class);
}
PsiElement brace = soloRightBraceBeingMoved(file, editor);
if (brace != null) {
int line = editor.getDocument().getLineNumber(offset);
final LineRange toMove = new LineRange(line, line + 1);
toMove.firstElement = toMove.lastElement = brace;
info.toMove = toMove;
}
// Cannot move in/outside method/class/function/comment.
if (!calcInsertOffset(file, editor, info.toMove, info, down)) return false;
int insertOffset = down
? getLineStartSafeOffset(editor.getDocument(), info.toMove2.endLine)
: editor.getDocument().getLineStartOffset(info.toMove2.startLine);
PsiElement elementAtInsertOffset = file.getViewProvider().findElementAt(insertOffset, DartLanguage.INSTANCE);
PsiElement newGuard;
if (isExpr) {
newGuard = PsiTreeUtil.getParentOfType(
elementAtInsertOffset, DartMethodDeclaration.class, DartListLiteralExpression.class, DartArgumentList.class, DartArguments.class,
DartFunctionDeclarationWithBodyOrNative.class, DartClass.class, PsiComment.class);
}
else {
newGuard = PsiTreeUtil.getParentOfType(elementAtInsertOffset, DartMethodDeclaration.class,
DartFunctionDeclarationWithBodyOrNative.class, DartClass.class, PsiComment.class);
}
if (brace != null && PsiTreeUtil.getParentOfType(brace, IDartBlock.class, false) !=
PsiTreeUtil.getParentOfType(elementAtInsertOffset, IDartBlock.class, false)) {
info.indentSource = true;
}
if (newGuard == guard && isInside(insertOffset, newGuard) == isInside(offset, guard)) {
return true;
}
if (newGuard == null || guard == null) {
return false;
}
if (NESTED_GUARDS.contains(newGuard.getNode().getElementType()) && NESTED_GUARDS.contains(guard.getNode().getElementType())) {
PsiElement parent = PsiTreeUtil.findCommonParent(guard, newGuard);
if (parent == guard || parent == newGuard) {
return isInside(insertOffset, newGuard) == isInside(offset, guard);
}
}
return false;
}
private static PsiElement soloRightBraceBeingMoved(final PsiFile file, final Editor editor) {
// Return the right brace on the line with the cursor, or null if the line is not a single brace.
LineRange range = getLineRangeFromSelection(editor);
if (range.endLine - range.startLine != 1) {
return null;
}
Document document = editor.getDocument();
int offset = editor.getCaretModel().getOffset();
int line = document.getLineNumber(offset);
int lineStartOffset = document.getLineStartOffset(line);
String lineText = document.getText().substring(lineStartOffset, document.getLineEndOffset(line));
if (!lineText.trim().equals("}")) {
return null;
}
return file.findElementAt(lineStartOffset + lineText.indexOf('}'));
}
private boolean calcInsertOffset(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull LineRange range,
@NotNull final MoveInfo info,
final boolean down) {
int destLine = getDestLineForAnon(editor, range, down);
int startLine = down ? range.endLine : range.startLine - 1;
if (destLine < 0 || startLine < 0) return false;
boolean firstTime = true;
boolean isExpr = isMovingExpr(info.toMove);
PsiElement elementStart = null;
if (isExpr) {
int offset = editor.logicalPositionToOffset(new LogicalPosition(startLine, 0));
elementStart = firstNonWhiteMovableElement(offset, file, true);
if (elementStart instanceof DartArgumentList) {
elementStart = elementStart.getFirstChild();
}
else if (isRightBracket(elementStart) && info.toMove.firstElement instanceof DartNamedArgument) {
elementStart = elementStart.getParent().getParent(); // Possibly a named arg with list value
}
if (elementStart instanceof DartExpression || elementStart instanceof DartNamedArgument) {
TextRange elementTextRange = elementStart.getTextRange();
LogicalPosition pos = editor.offsetToLogicalPosition(elementTextRange.getEndOffset());
int endOffset = editor.logicalPositionToOffset(new LogicalPosition(pos.line + 1, 0));
PsiElement elementEnd = firstNonWhiteMovableElement(endOffset, file, false);
if (elementEnd instanceof DartArgumentList && elementStart instanceof DartNamedArgument) {
elementEnd = elementEnd.getLastChild();
if (!isComma(elementEnd)) {
return false; // Require trailing comma
}
else {
info.toMove2 = new LineRange(startLine, pos.line + 1);
return true;
}
}
if (elementEnd != null && isComma(elementEnd)) {
PsiElement elementTail = UsefulPsiTreeUtil.getPrevSiblingSkipWhiteSpacesAndComments(elementEnd, true);
if (elementTail instanceof DartExpressionList) {
elementTail = elementTail.getLastChild();
}
if (elementStart == elementTail) {
if (down) {
info.toMove2 = new LineRange(startLine, pos.line + 1);
}
else {
destLine = pos.line;
elementTextRange = elementTail.getTextRange();
pos = editor.offsetToLogicalPosition(elementTextRange.getStartOffset());
info.toMove2 = new LineRange(pos.line, destLine + 1);
}
return true;
}
}
}
else if (elementStart != null && isRightParen(elementStart)) {
PsiElement start = elementStart.getParent().getParent();
TextRange elementTextRange = start.getTextRange();
LogicalPosition pos = editor.offsetToLogicalPosition(elementTextRange.getStartOffset());
int startOffset = editor.logicalPositionToOffset(new LogicalPosition(pos.line, 0));
PsiElement startElement = firstNonWhiteMovableElement(startOffset, file, true);
if (startElement == start) {
info.toMove2 = new LineRange(pos.line, down ? startLine : startLine + 1);
return true;
}
}
}
while (true) {
int offset = editor.logicalPositionToOffset(new LogicalPosition(destLine, 0));
PsiElement element = firstNonWhiteMovableElement(offset, file, !isExpr || !down);
if (firstTime) {
if (element != null && element.getNode().getElementType() == (down ? DartTokenTypes.RBRACE : DartTokenTypes.LBRACE)) {
PsiElement elementParent = element.getParent();
if (elementParent != null && (isStatement(elementParent) || elementParent instanceof DartBlock)) {
return true;
}
}
}
if (element instanceof DartStatements) element = element.getFirstChild();
while (element != null && !(element instanceof PsiFile)) {
TextRange elementTextRange = element.getTextRange();
if (elementTextRange.isEmpty() || !elementTextRange.grown(-1).shiftRight(1).contains(offset)) {
PsiElement elementToSurround = null;
boolean found = false;
if (isExpr) {
if (firstTime && element instanceof DartExpression) {
found = true;
}
else if (isComma(element) && UsefulPsiTreeUtil.getPrevSiblingSkipWhiteSpacesAndComments(element, true) == elementStart) {
found = true;
}
else if (element instanceof DartArgumentList) {
element = element.getParent();
if (element.getParent() == elementStart) {
element = element.getParent().getNextSibling();
boolean hasComma = false;
while (element != null) {
if (UsefulPsiTreeUtil.isWhitespaceOrComment(element)) {
if (element.getText().contains("\n")) {
destLine += 1;
break;
}
}
else if (isComma(element)) {
hasComma = true;
}
else {
break;
}
element = element.getNextSibling();
}
if (!hasComma) {
return false; // Disallow move if following expr has no trailing comma
}
found = true;
}
}
}
else if ((isStatement(element) || element instanceof PsiComment)
&& statementCanBePlacedAlong(element)) {
found = true;
if (!(element.getParent() instanceof IDartBlock)) {
elementToSurround = element;
}
}
else if ((element.getNode().getElementType() == DartTokenTypes.RBRACE &&
element.getParent() instanceof IDartBlock &&
(!isStatement(element.getParent().getParent()) || statementCanBePlacedAlong(element.getParent().getParent())))
|| (!down && element instanceof DartStatements)) {
// Before/after code block closing/opening brace.
found = true;
}
if (found) {
if (elementToSurround != null) {
final SmartPointerManager manager = SmartPointerManager.getInstance(elementToSurround.getProject());
statementToSurroundWithCodeBlock = manager.createSmartPsiElementPointer(elementToSurround);
}
info.toMove = range;
int endLine = destLine;
if (startLine > endLine) {
int tmp = endLine;
endLine = startLine;
startLine = tmp;
}
info.toMove2 = new LineRange(startLine, down ? endLine : endLine + 1);
return true;
}
}
element = element.getParent();
}
firstTime = false;
destLine += down ? 1 : -1;
if (destLine <= 0 || destLine >= editor.getDocument().getLineCount()) {
return false;
}
}
}
private static int getDestLineForAnon(Editor editor, LineRange range, boolean down) {
int destLine = down ? range.endLine + 1 : range.startLine - 1;
if (!isStatement(range.firstElement)) {
return destLine;
}
PsiElement sibling =
StatementUpDownMover.firstNonWhiteElement(down ? range.lastElement.getNextSibling() : range.firstElement.getPrevSibling(), down);
final DartFunctionExpression fn = findChildOfType(sibling, DartFunctionExpression.class, DartPsiCompositeElement.class);
if (fn != null && sibling != null && PsiTreeUtil.getParentOfType(fn, DartPsiCompositeElement.class) == sibling) {
destLine =
editor.getDocument().getLineNumber(down ? sibling.getTextRange().getEndOffset() + 1 : sibling.getTextRange().getStartOffset());
}
return destLine;
}
@Nullable
private static <T extends PsiElement> T findChildOfType(@Nullable final PsiElement element,
@NotNull final Class<T> aClass,
@Nullable final Class<? extends PsiElement> stopAt) {
final PsiElementProcessor.FindElement<PsiElement> processor;
processor = new PsiElementProcessor.FindElement<PsiElement>() {
@Override
public boolean execute(@NotNull PsiElement each) {
if (element == each) return true; // strict
if (aClass.isInstance(each)) {
return setFound(each);
}
return stopAt == null || !stopAt.isInstance(each);
}
};
PsiTreeUtil.processElements(element, processor);
//noinspection unchecked
return (T)processor.getFoundElement();
}
private static boolean isInside(final int offset, final PsiElement guard) {
if (guard == null) return false;
TextRange inside = guard.getTextRange();
if (guard instanceof DartMethodDeclaration) {
DartFunctionBody body = ((DartMethodDeclaration)guard).getFunctionBody();
if (body != null) {
inside = body.getTextRange();
}
}
else if (guard instanceof DartFunctionDeclarationWithBodyOrNative) {
DartFunctionBody body = ((DartFunctionDeclarationWithBodyOrNative)guard).getFunctionBody();
if (body != null) {
inside = body.getTextRange();
}
}
else if (guard instanceof DartClassDefinition) {
DartClassBody body = ((DartClassDefinition)guard).getClassBody();
PsiElement lBrace = PsiTreeUtil.getChildOfType(body, LeafPsiElement.class);
if (lBrace != null && lBrace.getText().equals("{")) {
PsiElement rBrace = PsiTreeUtil.lastChild(body);
rBrace = PsiTreeUtil.skipSiblingsBackward(rBrace, PsiWhiteSpace.class, PsiComment.class);
if (rBrace != null && rBrace.getText().equals("}")) {
inside = new TextRange(lBrace.getTextOffset(), rBrace.getTextOffset());
}
}
}
return inside != null && inside.contains(offset);
}
private static boolean statementCanBePlacedAlong(final PsiElement element) {
if (element instanceof IDartBlock) {
return false;
}
PsiElement parent = element.getParent();
if (parent instanceof DartStatements) {
parent = parent.getParent();
if (parent instanceof IDartBlock) {
if (!isStatement(parent.getParent())) {
return true;
}
parent = parent.getParent();
}
else {
return false;
}
}
if (parent instanceof DartIfStatement) {
PsiElement thenBranch = DartPsiImplUtil.getThenBranch((DartIfStatement)parent);
PsiElement elseBranch = DartPsiImplUtil.getElseBranch((DartIfStatement)parent);
if (isSameStatement(element, thenBranch) || isSameStatement(element, elseBranch)) {
return true;
}
}
if (parent instanceof DartWhileStatement) {
PsiElement body = DartPsiImplUtil.getWhileBody((DartWhileStatement)parent);
if (isSameStatement(element, body)) {
return true;
}
}
if (parent instanceof DartDoWhileStatement) {
PsiElement body = DartPsiImplUtil.getDoBody((DartDoWhileStatement)parent);
if (isSameStatement(element, body)) {
return true;
}
}
if (parent instanceof DartForStatement) {
PsiElement body = DartPsiImplUtil.getForBody((DartForStatement)parent);
if (isSameStatement(element, body)) {
return true;
}
}
return false;
}
private static boolean isSameStatement(PsiElement element, PsiElement statementOrBlock) {
if (element == statementOrBlock) return true;
if (PsiTreeUtil.findCommonParent(statementOrBlock, element) == statementOrBlock) return true;
return expressionStatementTeminator(statementOrBlock) == element;
}
private static boolean isStatement(PsiElement element) {
boolean[] result = new boolean[1];
result[0] = false;
element.accept(new DartVisitor() {
public void visitAssertStatement(@NotNull DartAssertStatement o) {
result[0] = true;
}
public void visitBreakStatement(@NotNull DartBreakStatement o) {
result[0] = true;
}
public void visitContinueStatement(@NotNull DartContinueStatement o) {
result[0] = true;
}
public void visitDoWhileStatement(@NotNull DartDoWhileStatement o) {
result[0] = true;
}
public void visitForStatement(@NotNull DartForStatement o) {
result[0] = true;
}
public void visitIfStatement(@NotNull DartIfStatement o) {
result[0] = true;
}
public void visitRethrowStatement(@NotNull DartRethrowStatement o) {
result[0] = true;
}
public void visitReturnStatement(@NotNull DartReturnStatement o) {
result[0] = true;
}
public void visitSwitchStatement(@NotNull DartSwitchStatement o) {
result[0] = true;
}
public void visitTryStatement(@NotNull DartTryStatement o) {
result[0] = true;
}
public void visitWhileStatement(@NotNull DartWhileStatement o) {
result[0] = true;
}
public void visitYieldEachStatement(@NotNull DartYieldEachStatement o) {
result[0] = true;
}
public void visitYieldStatement(@NotNull DartYieldStatement o) {
result[0] = true;
}
public void visitVarDeclarationList(@NotNull DartVarDeclarationList o) {
result[0] = expressionStatementTeminator(o) != null;
}
public void visitExpression(@NotNull DartExpression o) {
result[0] = expressionStatementTeminator(o) != null;
}
});
return result[0];
}
@Nullable
private static PsiElement expressionStatementTeminator(PsiElement element) {
if (element instanceof DartExpression || element instanceof DartVarDeclarationList) {
PsiElement token = PsiTreeUtil.skipSiblingsForward(element, PsiComment.class, PsiWhiteSpace.class);
if (token != null && token.getNode().getElementType() == DartTokenTypes.SEMICOLON) {
return token;
}
}
return null;
}
@Nullable
private static PsiElement firstNonWhiteMovableElement(int offset, PsiFile file, boolean lookRight) {
PsiElement element = firstNonWhiteElement(offset, file, lookRight);
if (element == null) return null;
if (element instanceof DartExpressionList && lookRight) {
element = element.getFirstChild();
}
return element;
}
private static boolean isMovingExpr(@NotNull LineRange range) {
return isComma(range.lastElement) && (range.firstElement instanceof DartExpression || range.firstElement instanceof DartNamedArgument);
}
}