/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.codeInsight.upDownMover;
import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import kotlin.jvm.functions.Function1;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.psi.psiUtil.PsiUtilsKt;
import java.util.List;
import java.util.function.Predicate;
public class KotlinExpressionMover extends AbstractKotlinUpDownMover {
private static final Predicate<KtElement> IS_CALL_EXPRESSION = input -> input instanceof KtCallExpression;
public KotlinExpressionMover() {
}
private final static Class[] MOVABLE_ELEMENT_CLASSES = {
KtExpression.class,
KtWhenEntry.class,
KtValueArgument.class,
PsiComment.class
};
private static final Function1<PsiElement, Boolean> MOVABLE_ELEMENT_CONSTRAINT = new Function1<PsiElement, Boolean>() {
@NotNull
@Override
public Boolean invoke(PsiElement element) {
return (!(element instanceof KtExpression)
|| element instanceof KtDeclaration
|| element instanceof KtBlockExpression
|| element.getParent() instanceof KtBlockExpression);
}
};
private final static Class[] BLOCKLIKE_ELEMENT_CLASSES =
{KtBlockExpression.class, KtWhenExpression.class, KtClassBody.class, KtFile.class};
private final static Class[] FUNCTIONLIKE_ELEMENT_CLASSES =
{KtFunction.class, KtPropertyAccessor.class, KtAnonymousInitializer.class};
private static final Predicate<KtElement> CHECK_BLOCK_LIKE_ELEMENT =
input -> (input instanceof KtBlockExpression || input instanceof KtClassBody)
&& !PsiTreeUtil.instanceOf(input.getParent(), FUNCTIONLIKE_ELEMENT_CLASSES);
private static final Predicate<KtElement> CHECK_BLOCK =
input -> input instanceof KtBlockExpression && !PsiTreeUtil.instanceOf(input.getParent(), FUNCTIONLIKE_ELEMENT_CLASSES);
@Nullable
private static PsiElement getStandaloneClosingBrace(@NotNull PsiFile file, @NotNull Editor editor) {
LineRange range = getLineRangeFromSelection(editor);
if (range.endLine - range.startLine != 1) return null;
int offset = editor.getCaretModel().getOffset();
Document document = editor.getDocument();
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 static BraceStatus checkForMovableDownClosingBrace(
@NotNull PsiElement closingBrace,
@NotNull PsiElement block,
@NotNull Editor editor,
@NotNull MoveInfo info
) {
PsiElement current = block;
PsiElement nextElement = null;
PsiElement nextExpression = null;
do {
PsiElement sibling = firstNonWhiteElement(current.getNextSibling(), true);
if (sibling != null && nextElement == null) {
nextElement = sibling;
}
if (sibling instanceof KtExpression) {
nextExpression = sibling;
break;
}
current = current.getParent();
}
while (current != null && !(PsiTreeUtil.instanceOf(current, BLOCKLIKE_ELEMENT_CLASSES)));
if (nextExpression == null) return BraceStatus.NOT_MOVABLE;
Document doc = editor.getDocument();
info.toMove = new LineRange(closingBrace, closingBrace, doc);
info.toMove2 = new LineRange(nextElement, nextExpression);
info.indentSource = true;
return BraceStatus.MOVABLE;
}
private static BraceStatus checkForMovableUpClosingBrace(
@NotNull PsiElement closingBrace,
PsiElement block,
@NotNull Editor editor,
@NotNull MoveInfo info
) {
//noinspection unchecked
PsiElement prev = KtPsiUtil.getLastChildByType(block, KtExpression.class);
if (prev == null) return BraceStatus.NOT_MOVABLE;
Document doc = editor.getDocument();
info.toMove = new LineRange(closingBrace, closingBrace, doc);
info.toMove2 = new LineRange(prev, prev, doc);
info.indentSource = true;
return BraceStatus.MOVABLE;
}
private static enum BraceStatus {
NOT_FOUND,
MOVABLE,
NOT_MOVABLE
}
// Returns null if standalone closing brace is not found
private static BraceStatus checkForMovableClosingBrace(
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull MoveInfo info,
boolean down
) {
PsiElement closingBrace = getStandaloneClosingBrace(file, editor);
if (closingBrace == null) return BraceStatus.NOT_FOUND;
PsiElement blockLikeElement = closingBrace.getParent();
if (!(blockLikeElement instanceof KtBlockExpression)) return BraceStatus.NOT_MOVABLE;
PsiElement blockParent = blockLikeElement.getParent();
if (blockParent instanceof KtWhenEntry) return BraceStatus.NOT_FOUND;
if (PsiTreeUtil.instanceOf(blockParent, FUNCTIONLIKE_ELEMENT_CLASSES)) return BraceStatus.NOT_FOUND;
PsiElement enclosingExpression = PsiTreeUtil.getParentOfType(blockLikeElement, KtExpression.class);
if (enclosingExpression instanceof KtDoWhileExpression) return BraceStatus.NOT_MOVABLE;
if (enclosingExpression instanceof KtIfExpression) {
KtIfExpression ifExpression = (KtIfExpression) enclosingExpression;
if (blockLikeElement == ifExpression.getThen() && ifExpression.getElse() != null) return BraceStatus.NOT_MOVABLE;
}
return down
? checkForMovableDownClosingBrace(closingBrace, blockLikeElement, editor, info)
: checkForMovableUpClosingBrace(closingBrace, blockLikeElement, editor, info);
}
@Nullable
private static KtBlockExpression findClosestBlock(@NotNull PsiElement anchor, boolean down, boolean strict) {
PsiElement current = PsiTreeUtil.getParentOfType(anchor, KtBlockExpression.class, strict);
while (current != null) {
PsiElement parent = current.getParent();
if (parent instanceof KtClassBody ||
parent instanceof KtAnonymousInitializer ||
parent instanceof KtNamedFunction ||
(parent instanceof KtProperty && !((KtProperty) parent).isLocal())) {
return null;
}
if (parent instanceof KtBlockExpression) return (KtBlockExpression) parent;
PsiElement sibling = down ? current.getNextSibling() : current.getPrevSibling();
if (sibling != null) {
//noinspection unchecked
KtBlockExpression block = (KtBlockExpression) KtPsiUtil.getOutermostDescendantElement(sibling, down, CHECK_BLOCK);
if (block != null) return block;
current = sibling;
}
else {
current = parent;
}
}
return null;
}
@Nullable
private static KtBlockExpression getDSLLambdaBlock(@NotNull PsiElement element, boolean down) {
KtCallExpression callExpression =
(KtCallExpression) KtPsiUtil.getOutermostDescendantElement(element, down, IS_CALL_EXPRESSION);
if (callExpression == null) return null;
List<KtLambdaArgument> functionLiterals = callExpression.getLambdaArguments();
if (functionLiterals.isEmpty()) return null;
return functionLiterals.get(0).getLambdaExpression().getBodyExpression();
}
@Nullable
private static LineRange getExpressionTargetRange(@NotNull Editor editor, @NotNull PsiElement sibling, boolean down) {
if (sibling instanceof KtIfExpression && !down) {
KtExpression elseBranch = ((KtIfExpression) sibling).getElse();
if (elseBranch instanceof KtBlockExpression) {
sibling = elseBranch;
}
}
PsiElement start = sibling;
PsiElement end = sibling;
// moving out of code block
if (sibling.getNode().getElementType() == (down ? KtTokens.RBRACE : KtTokens.LBRACE)) {
PsiElement parent = sibling.getParent();
if (!(parent instanceof KtBlockExpression || parent instanceof KtFunctionLiteral)) return null;
KtBlockExpression newBlock;
if (parent instanceof KtFunctionLiteral) {
//noinspection ConstantConditions
newBlock = findClosestBlock(((KtFunctionLiteral) parent).getBodyExpression(), down, false);
if (!down) {
PsiElement arrow = ((KtFunctionLiteral) parent).getArrow();
if (arrow != null) {
end = arrow;
}
}
} else {
newBlock = findClosestBlock(sibling, down, true);
}
if (newBlock == null) return null;
if (PsiTreeUtil.isAncestor(newBlock, parent, true)) {
PsiElement outermostParent = KtPsiUtil.getOutermostParent(parent, newBlock, true);
if (down) {
end = outermostParent;
}
else {
start = outermostParent;
}
}
else {
if (down) {
end = newBlock.getLBrace();
}
else {
start = newBlock.getRBrace();
}
}
}
// moving into code block
else {
PsiElement blockLikeElement;
KtBlockExpression dslBlock = getDSLLambdaBlock(sibling, down);
if (dslBlock != null) {
// Use JetFunctionLiteral (since it contains braces)
blockLikeElement = dslBlock.getParent();
} else {
// JetBlockExpression and other block-like elements
blockLikeElement = KtPsiUtil.getOutermostDescendantElement(sibling, down, CHECK_BLOCK_LIKE_ELEMENT);
}
if (blockLikeElement != null) {
if (down) {
end = KtPsiUtil.findChildByType(blockLikeElement, KtTokens.LBRACE);
if (blockLikeElement instanceof KtFunctionLiteral) {
PsiElement arrow = ((KtFunctionLiteral) blockLikeElement).getArrow();
if (arrow != null) {
end = arrow;
}
}
}
else {
start = KtPsiUtil.findChildByType(blockLikeElement, KtTokens.RBRACE);
}
}
}
return start != null && end != null ? new LineRange(start, end, editor.getDocument()) : null;
}
@Nullable
private static LineRange getWhenEntryTargetRange(@NotNull Editor editor, @NotNull PsiElement sibling, boolean down) {
if (sibling.getNode().getElementType() == (down ? KtTokens.RBRACE : KtTokens.LBRACE) &&
PsiTreeUtil.getParentOfType(sibling, KtWhenEntry.class) == null) {
return null;
}
return new LineRange(sibling, sibling, editor.getDocument());
}
@Nullable
private LineRange getValueParamOrArgTargetRange(
@NotNull Editor editor,
@NotNull PsiElement elementToCheck,
@NotNull PsiElement sibling,
boolean down
) {
PsiElement next = sibling;
if (next.getNode().getElementType() == KtTokens.COMMA) {
next = firstNonWhiteSibling(next, down);
}
LineRange range = (next instanceof KtParameter || next instanceof KtValueArgument)
? new LineRange(next, next, editor.getDocument())
: null;
if (range != null) {
parametersOrArgsToMove = new Pair<PsiElement, PsiElement>(elementToCheck, next);
}
return range;
}
@Nullable
private LineRange getTargetRange(
@NotNull Editor editor,
@Nullable PsiElement elementToCheck,
@NotNull PsiElement sibling,
boolean down
) {
if (elementToCheck instanceof KtParameter || elementToCheck instanceof KtValueArgument) {
return getValueParamOrArgTargetRange(editor, elementToCheck, sibling, down);
}
if (elementToCheck instanceof KtExpression || elementToCheck instanceof PsiComment) {
return getExpressionTargetRange(editor, sibling, down);
}
if (elementToCheck instanceof KtWhenEntry) {
return getWhenEntryTargetRange(editor, sibling, down);
}
return null;
}
@Override
protected boolean checkSourceElement(@NotNull PsiElement element) {
return PsiTreeUtil.instanceOf(element, MOVABLE_ELEMENT_CLASSES);
}
@Override
protected LineRange getElementSourceLineRange(@NotNull PsiElement element, @NotNull Editor editor, @NotNull LineRange oldRange) {
TextRange textRange = element.getTextRange();
if (editor.getDocument().getTextLength() < textRange.getEndOffset()) return null;
int startLine = editor.offsetToLogicalPosition(textRange.getStartOffset()).line;
int endLine = editor.offsetToLogicalPosition(textRange.getEndOffset()).line + 1;
return new LineRange(startLine, endLine);
}
@Nullable
private static PsiElement getMovableElement(@NotNull PsiElement element, boolean lookRight) {
//noinspection unchecked
PsiElement movableElement = PsiUtilsKt.getParentOfTypesAndPredicate(
element,
false,
MOVABLE_ELEMENT_CLASSES,
MOVABLE_ELEMENT_CONSTRAINT
);
if (movableElement == null) return null;
if (isBracelessBlock(movableElement)) {
movableElement = firstNonWhiteElement(lookRight ? movableElement.getLastChild() : movableElement.getFirstChild(), !lookRight);
}
return movableElement;
}
private static boolean isLastOfItsKind(@NotNull PsiElement element, boolean down) {
return getSiblingOfType(element, down, element.getClass()) == null;
}
private static boolean isForbiddenMove(@NotNull PsiElement element, boolean down) {
if (element instanceof KtParameter || element instanceof KtValueArgument) {
return isLastOfItsKind(element, down);
}
return false;
}
private static boolean isBracelessBlock(@NotNull PsiElement element) {
if (!(element instanceof KtBlockExpression)) return false;
KtBlockExpression block = (KtBlockExpression) element;
return block.getLBrace() == null && block.getRBrace() == null;
}
protected static PsiElement adjustSibling(
@NotNull LineRange sourceRange,
@NotNull MoveInfo info,
boolean down
) {
PsiElement element = down ? sourceRange.lastElement : sourceRange.firstElement;
PsiElement sibling = down ? element.getNextSibling() : element.getPrevSibling();
PsiElement whiteSpaceTestSubject = sibling;
if (sibling == null) {
PsiElement parent = element.getParent();
if (parent != null && isBracelessBlock(parent)) {
whiteSpaceTestSubject = down ? parent.getNextSibling() : parent.getPrevSibling();
}
}
if (whiteSpaceTestSubject instanceof PsiWhiteSpace) {
if (KotlinRefactoringUtilKt.isMultiLine(whiteSpaceTestSubject)) {
int nearLine = down ? sourceRange.endLine : sourceRange.startLine - 1;
info.toMove = sourceRange;
info.toMove2 = new LineRange(nearLine, nearLine + 1);
info.indentTarget = false;
return null;
}
if (sibling != null) {
sibling = firstNonWhiteElement(sibling, down);
}
}
if (sibling == null) {
KtCallExpression callExpression = PsiTreeUtil.getParentOfType(element, KtCallExpression.class);
if (callExpression != null) {
KtBlockExpression dslBlock = getDSLLambdaBlock(callExpression, down);
if (PsiTreeUtil.isAncestor(dslBlock, element, false)) {
//noinspection ConstantConditions
PsiElement blockParent = dslBlock.getParent();
return down
? KtPsiUtil.findChildByType(blockParent, KtTokens.RBRACE)
: KtPsiUtil.findChildByType(blockParent, KtTokens.LBRACE);
}
}
info.toMove2 = null;
return null;
}
return sibling;
}
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
parametersOrArgsToMove = null;
if (!super.checkAvailable(editor, file, info, down)) return false;
switch (checkForMovableClosingBrace(editor, file, info, down)) {
case NOT_MOVABLE: {
info.toMove2 = null;
return true;
}
case MOVABLE:
return true;
default:
break;
}
LineRange oldRange = info.toMove;
Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, oldRange);
if (psiRange == null) return false;
//noinspection unchecked
PsiElement firstElement = getMovableElement(psiRange.getFirst(), false);
PsiElement lastElement = getMovableElement(psiRange.getSecond(), true);
if (firstElement == null || lastElement == null) return false;
if (isForbiddenMove(firstElement, down) || isForbiddenMove(lastElement, down)) {
info.toMove2 = null;
return true;
}
if ((firstElement instanceof KtParameter || firstElement instanceof KtValueArgument) &&
PsiTreeUtil.isAncestor(lastElement, firstElement, false)) {
lastElement = firstElement;
}
LineRange sourceRange = getSourceRange(firstElement, lastElement, editor, oldRange);
if (sourceRange == null) return false;
PsiElement sibling = getLastNonWhiteSiblingInLine(adjustSibling(sourceRange, info, down), editor, down);
// Either reached last sibling, or jumped over multi-line whitespace
if (sibling == null) return true;
info.toMove = sourceRange;
info.toMove2 = getTargetRange(editor, sourceRange.firstElement, sibling, down);
return true;
}
@Nullable
private Pair<PsiElement, PsiElement> parametersOrArgsToMove;
private static PsiElement getComma(@NotNull PsiElement element) {
PsiElement sibling = firstNonWhiteSibling(element, true);
return sibling != null && (sibling.getNode().getElementType() == KtTokens.COMMA) ? sibling : null;
}
private static void fixCommaIfNeeded(@NotNull PsiElement element, boolean willBeLast) {
PsiElement comma = getComma(element);
if (willBeLast && comma != null) {
comma.delete();
}
else if (!willBeLast && comma == null) {
PsiElement parent = element.getParent();
assert parent != null;
parent.addAfter(KtPsiFactoryKt.KtPsiFactory(parent.getProject()).createComma(), element);
}
}
@Override
public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
if (parametersOrArgsToMove != null) {
PsiElement element1 = parametersOrArgsToMove.first;
PsiElement element2 = parametersOrArgsToMove.second;
fixCommaIfNeeded(element1, down && isLastOfItsKind(element2, true));
fixCommaIfNeeded(element2, !down && isLastOfItsKind(element1, true));
//noinspection ConstantConditions
PsiDocumentManager.getInstance(editor.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument());
}
}
}