/*
* 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.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.*;
import java.util.ArrayList;
import java.util.List;
public class KotlinDeclarationMover extends AbstractKotlinUpDownMover {
private boolean moveEnumConstant = false;
private static int findNearestNonWhitespace(@NotNull CharSequence sequence, int index) {
char ch = sequence.charAt(--index);
while (Character.isWhitespace(ch)) {
ch = sequence.charAt(--index);
}
return index;
}
@Override
public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
super.afterMove(editor, file, info, down);
if (moveEnumConstant) {
Document document = editor.getDocument();
CharSequence cs = document.getCharsSequence();
int end1 = findNearestNonWhitespace(cs, info.range1.getEndOffset());
char c1 = cs.charAt(end1);
int end2 = findNearestNonWhitespace(cs, info.range2.getEndOffset());
char c2 = cs.charAt(end2);
if (c1 == c2 || (c1 != ',' && c2 != ',')) return;
if (c1 == ';' || c2 == ';') {
// Replace comma with semicolon and vice versa
document.replaceString(end1, end1 + 1, String.valueOf(c2));
document.replaceString(end2, end2 + 1, String.valueOf(c1));
}
else if (c1 == ',') {
// Move comma from the end of range1 to the end of range2
document.deleteString(end1, end1 + 1);
document.insertString(end2 + 1, ",");
}
else {
// Move comma from the end of range2 to the end of range1
document.deleteString(end2, end2 + 1);
document.insertString(end1 + 1, ",");
}
}
}
@NotNull
private static List<PsiElement> getDeclarationAnchors(@NotNull KtDeclaration declaration) {
final List<PsiElement> memberSuspects = new ArrayList<PsiElement>();
KtModifierList modifierList = declaration.getModifierList();
if (modifierList != null) memberSuspects.add(modifierList);
if (declaration instanceof KtNamedDeclaration) {
PsiElement nameIdentifier = ((KtNamedDeclaration) declaration).getNameIdentifier();
if (nameIdentifier != null) memberSuspects.add(nameIdentifier);
}
declaration.accept(
new KtVisitorVoid() {
@Override
public void visitClassInitializer(@NotNull KtClassInitializer initializer) {
PsiElement brace = initializer.getOpenBraceNode();
if (brace != null) {
memberSuspects.add(brace);
}
}
@Override
public void visitNamedFunction(@NotNull KtNamedFunction function) {
PsiElement equalsToken = function.getEqualsToken();
if (equalsToken != null) memberSuspects.add(equalsToken);
KtTypeParameterList typeParameterList = function.getTypeParameterList();
if (typeParameterList != null) memberSuspects.add(typeParameterList);
KtTypeReference receiverTypeRef = function.getReceiverTypeReference();
if (receiverTypeRef != null) memberSuspects.add(receiverTypeRef);
KtTypeReference returnTypeRef = function.getTypeReference();
if (returnTypeRef != null) memberSuspects.add(returnTypeRef);
}
@Override
public void visitProperty(@NotNull KtProperty property) {
PsiElement valOrVarKeyword = property.getValOrVarKeyword();
if (valOrVarKeyword != null) memberSuspects.add(valOrVarKeyword);
KtTypeParameterList typeParameterList = property.getTypeParameterList();
if (typeParameterList != null) memberSuspects.add(typeParameterList);
KtTypeReference receiverTypeRef = property.getReceiverTypeReference();
if (receiverTypeRef != null) memberSuspects.add(receiverTypeRef);
KtTypeReference returnTypeRef = property.getTypeReference();
if (returnTypeRef != null) memberSuspects.add(returnTypeRef);
}
}
);
return memberSuspects;
}
private static final Class[] DECLARATION_CONTAINER_CLASSES =
{KtClassBody.class, KtAnonymousInitializer.class, KtFunction.class, KtPropertyAccessor.class, KtFile.class};
private static final Class[] CLASSBODYLIKE_DECLARATION_CONTAINER_CLASSES = {KtClassBody.class, KtFile.class};
@Nullable
private static KtDeclaration getMovableDeclaration(@Nullable PsiElement element) {
if (element == null) return null;
KtDeclaration declaration = PsiTreeUtil.getParentOfType(element, KtDeclaration.class, false);
if (declaration instanceof KtParameter) return null;
if (declaration instanceof KtTypeParameter) {
return getMovableDeclaration(declaration.getParent());
}
return PsiTreeUtil.instanceOf(PsiTreeUtil.getParentOfType(declaration,
DECLARATION_CONTAINER_CLASSES),
CLASSBODYLIKE_DECLARATION_CONTAINER_CLASSES) ? declaration : null;
}
@Override
protected boolean checkSourceElement(@NotNull PsiElement element) {
return element instanceof KtDeclaration;
}
@Override
protected LineRange getElementSourceLineRange(@NotNull PsiElement element, @NotNull Editor editor, @NotNull LineRange oldRange) {
PsiElement first;
PsiElement last;
if (element instanceof KtDeclaration) {
first = element.getFirstChild();
last = element.getLastChild();
if (first == null || last == null) return null;
}
else {
first = last = element;
}
TextRange textRange1 = first.getTextRange();
TextRange textRange2 = last.getTextRange();
Document doc = editor.getDocument();
if (doc.getTextLength() < textRange2.getEndOffset()) return null;
int startLine = editor.offsetToLogicalPosition(textRange1.getStartOffset()).line;
int endLine = editor.offsetToLogicalPosition(textRange2.getEndOffset()).line + 1;
if (element instanceof PsiComment
|| startLine == oldRange.startLine || startLine == oldRange.endLine
|| endLine == oldRange.startLine || endLine == oldRange.endLine) {
return new LineRange(startLine, endLine);
}
TextRange lineTextRange = new TextRange(doc.getLineStartOffset(oldRange.startLine),
doc.getLineEndOffset(oldRange.endLine));
if (element instanceof KtDeclaration) {
for (PsiElement anchor : getDeclarationAnchors((KtDeclaration) element)) {
TextRange suspectTextRange = anchor.getTextRange();
if (suspectTextRange != null && lineTextRange.intersects(suspectTextRange)) return new LineRange(startLine, endLine);
}
}
return null;
}
@Nullable
private static LineRange getTargetRange(
@NotNull Editor editor,
@NotNull PsiElement sibling,
boolean down,
@NotNull PsiElement target
) {
PsiElement start = sibling;
PsiElement end = sibling;
PsiElement nextParent = null;
// moving out of code block
if (sibling.getNode().getElementType() == (down ? KtTokens.RBRACE : KtTokens.LBRACE)) {
// elements which aren't immediately placed in class body can't leave the block
PsiElement parent = sibling.getParent();
if (!(parent instanceof KtClassBody)) return null;
if (target instanceof KtEnumEntry) return null;
KtClassOrObject ktClassOrObject = (KtClassOrObject) parent.getParent();
assert ktClassOrObject != null;
nextParent = ktClassOrObject.getParent();
if (!down) {
start = ktClassOrObject;
}
}
// moving into code block
// element may move only into class body
else {
if (sibling instanceof KtClassOrObject) {
KtClassOrObject ktClassOrObject = (KtClassOrObject) sibling;
KtClassBody classBody = ktClassOrObject.getBody();
// confined elements can't leave their block
if (classBody != null) {
nextParent = classBody;
if (!down) {
start = classBody.getRBrace();
}
end = down ? classBody.getLBrace() : classBody.getRBrace();
}
}
}
if (nextParent != null) {
if (target instanceof KtAnonymousInitializer && !(nextParent instanceof KtClassBody)) return null;
if (target instanceof KtEnumEntry) {
if (!(nextParent instanceof KtClassBody)) return null;
KtClassOrObject nextClassOrObject = (KtClassOrObject) nextParent.getParent();
assert nextClassOrObject != null;
if (!nextClassOrObject.hasModifier(KtTokens.ENUM_KEYWORD)) return null;
}
}
if (target instanceof KtPropertyAccessor && !(sibling instanceof KtPropertyAccessor)) return null;
return start != null && end != null ? new LineRange(start, end, editor.getDocument()) : null;
}
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!super.checkAvailable(editor, file, info, down)) return false;
LineRange oldRange = info.toMove;
Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, oldRange);
if (psiRange == null) return false;
KtDeclaration firstDecl = getMovableDeclaration(psiRange.getFirst());
if (firstDecl == null) return false;
moveEnumConstant = firstDecl instanceof KtEnumEntry;
KtDeclaration lastDecl = getMovableDeclaration(psiRange.getSecond());
if (lastDecl == null) return false;
//noinspection ConstantConditions
LineRange sourceRange = getSourceRange(firstDecl, lastDecl, editor, oldRange);
if (sourceRange == null) return false;
PsiElement sibling = getLastNonWhiteSiblingInLine(firstNonWhiteSibling(sourceRange, down), editor, down);
// Either reached last sibling, or jumped over multi-line whitespace
if (sibling == null) {
info.toMove2 = null;
return true;
}
info.toMove = sourceRange;
info.toMove2 = getTargetRange(editor, sibling, down, sourceRange.firstElement);
return true;
}
}