package com.jetbrains.lang.dart.ide.moveCode;
import com.google.common.annotations.VisibleForTesting;
import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
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.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.lang.dart.DartTokenTypesSets;
import com.jetbrains.lang.dart.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Move top-level functions, entire class declarations, and class member declarations above or below the neighboring component.
*/
public class DartComponentMover extends LineMover {
private enum CommentType {
SINGLE_LINE_COMMENT,
MULTI_LINE_COMMENT,
SINGLE_LINE_DOC_COMMENT,
MULTI_LINE_DOC_COMMENT,
INVALID,
NONE
}
private static class CodeMover {
@NotNull final Editor editor;
@NotNull final PsiFile file;
@NotNull final MoveInfo info;
final boolean isMovingDown;
Pair<PsiElement, PsiElement> sourceComponents;
Pair<PsiElement, PsiElement> targetComponents;
LineRange sourceRange;
LineRange targetRange;
CodeMover(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
this.editor = editor;
this.file = file;
this.info = info;
this.isMovingDown = down;
}
@Nullable
static Pair<PsiElement, PsiElement> findCommentRange(@NotNull PsiElement element) {
if (!isComment(element) && !(element instanceof PsiWhiteSpace)) return null;
PsiElement first = findFinalComment(element, false);
PsiElement last = findFinalComment(element, true);
if (commentTypeOf(first) != commentTypeOf(last)) {
// Could be starting from whitespace at end of line comment.
last = element.getPrevSibling();
}
return Pair.create(first, last);
}
boolean hasSourceComponents() {
return sourceComponents != null && sourceComponents.first != null;
}
void findSourceComponents() {
Pair<PsiElement, PsiElement> psiRange = getElementRange(editor, file, info.toMove);
if (psiRange == null) return;
PsiElement firstMember = getDeclarationParent(firstMoveableComponent(psiRange.first));
if (firstMember == null) return;
PsiElement lastMember;
if (isComment(firstMember)) {
lastMember = findAttachedDeclaration(firstMember);
if (lastMember == firstMember) lastMember = getDeclarationParent(psiRange.second);
}
else {
lastMember = firstMember;
firstMember = findAttachedComment(firstMember);
if (firstMember == lastMember && !isMovingDown) {
if (lastMember.getParent() instanceof DartClassMembers) {
DartClassMembers members = (DartClassMembers)lastMember.getParent();
PsiElement next = nextSib(members, false);
if (next instanceof PsiWhiteSpace && !isCommentSeparator(next)) next = nextSib(next, false);
if (isComment(next)) firstMember = next;
}
}
}
if (lastMember == null) return;
PsiElement sibling = lastMember.getNextSibling();
if (!isCommentSeparator(sibling)) {
PsiElement next = firstNonWhiteElement(lastMember.getNextSibling(), true);
if (isSemicolon(next)) lastMember = next;
if (isMovingDown) {
next = lastMember.getNextSibling();
if (next == null && lastMember.getParent() instanceof DartClassMembers) {
DartClassMembers members = (DartClassMembers)lastMember.getParent();
next = nextSib(members, true);
}
if (next instanceof PsiWhiteSpace && !StringUtil.containsLineBreak(next.getText())) next = next.getNextSibling();
if (next != null && isLineComment(next)) lastMember = next;
}
}
sourceComponents = Pair.create(firstMember, lastMember);
}
boolean hasSourceLineRange() {
return sourceRange != null;
}
void findSourceLineRange() {
LineRange range;
if (sourceComponents.first == sourceComponents.second) {
range = memberRange(sourceComponents.first);
if (range == null) return;
range.firstElement = range.lastElement = sourceComponents.first;
}
else {
final PsiElement parent = PsiTreeUtil.findCommonParent(sourceComponents.first, sourceComponents.second);
if (parent == null) return;
if (parent instanceof DartClassBody) {
// This is an edge case that occurs when attempting to move a declaration out of a class body
// and the declaration to be moved ends with a line comment (down) or has a preceding comment (up).
// TODO Handle multi-line declarations. (This functions for single lines by defaulting to line mover.)
return;
}
Pair<PsiElement, PsiElement> combinedRange;
combinedRange = getElementRange(parent, sourceComponents.first, sourceComponents.second);
if (combinedRange == null) return;
final LineRange lineRange1 = memberRange(combinedRange.first);
if (lineRange1 == null) return;
final LineRange lineRange2 = memberRange(combinedRange.second);
if (lineRange2 == null) return;
range = new LineRange(lineRange1.startLine, lineRange2.endLine);
range.firstElement = combinedRange.first;
range.lastElement = combinedRange.second;
}
sourceRange = range;
}
boolean hasTargetComponents() {
return targetComponents != null && targetComponents.first != null;
}
void findTargetComponents() {
PsiElement ref = isMovingDown ? sourceRange.lastElement : sourceRange.firstElement;
PsiElement sibling = nextSib(ref, isMovingDown);
if (sibling instanceof PsiWhiteSpace && StringUtil.countNewLines(sibling.getText()) == 0) {
PsiElement next = sibling.getNextSibling();
if (isLineComment(next)) sibling = next.getNextSibling();
}
if (sibling == null && ref.getParent() instanceof DartClassMembers) {
DartClassMembers members = (DartClassMembers)ref.getParent();
sibling = nextSib(members, isMovingDown);
}
PsiElement firstElement = firstNonWhiteElement(sibling, isMovingDown);
if (firstElement == null) firstElement = sibling == null ? ref : sibling;
PsiElement lastElement;
if (isComment(firstElement)) {
lastElement = isCommentSeparator(sibling) ? firstElement : findAttachedDeclaration(firstElement);
}
else {
lastElement = isMovingDown ? firstElement : findAttachedComment(firstElement);
}
if (firstElement instanceof PsiWhiteSpace || lastElement instanceof PsiWhiteSpace) {
info.prohibitMove();
return;
}
//PsiElement lastElement = isComment(firstElement) ? findAttachedDeclaration(firstElement) : findAttachedComment(firstElement);
targetComponents = isMovingDown ? Pair.create(firstElement, lastElement) : Pair.create(lastElement, firstElement);
}
boolean hasTargetLineRange() {
return targetRange != null;
}
void findTargetLineRange() {
PsiElement source = isMovingDown ? sourceRange.lastElement : sourceRange.firstElement;
PsiElement target = isMovingDown ? targetComponents.first : targetComponents.second;
if (crossesHeaderBoundary(source, target)) {
info.prohibitMove();
return;
}
targetRange = new LineRange(targetComponents.first, targetComponents.second, editor.getDocument());
}
boolean areTargetsAtSameLevel() {
return sourceComponents.second.getParent() == targetComponents.second.getParent();
}
private LineRange memberRange(@NotNull PsiElement member) {
TextRange textRange = member.getTextRange();
if (editor.getDocument().getTextLength() < textRange.getEndOffset()) return null;
LogicalPosition startPosition = editor.offsetToLogicalPosition(textRange.getStartOffset());
LogicalPosition endPosition = editor.offsetToLogicalPosition(textRange.getEndOffset());
int endLine = endPosition.line + 1;
return new LineRange(startPosition.line, endLine);
}
@NotNull
private static PsiElement findAttachedDeclaration(@NotNull PsiElement element) {
// Skip to the end of the comment (element) then return the next declaration if any, else element.
PsiElement commentEnd = findFinalComment(element, true);
if (isCommentSeparator(commentEnd.getNextSibling())) {
return commentEnd;
}
PsiElement next = PsiTreeUtil.skipSiblingsForward(commentEnd, PsiWhiteSpace.class);
return next == null ? element : (isComment(next) ? element : next);
}
@NotNull
private static PsiElement findAttachedComment(@NotNull PsiElement element) {
// Identify the first element that represents a comment prior to the given element.
// The comment may be a single block comment or a series of line comments.
// Do not mix types of comments. A line comment preceding a line-doc comment is not included.
PsiElement sib = isComment(element) ? element : element.getPrevSibling();
PsiElement commentStart = findFinalComment(sib == null ? element : sib, false);
return commentStart == sib && (commentStart instanceof PsiWhiteSpace) ? element : commentStart;
}
private static PsiElement nextSib(@NotNull PsiElement element, boolean isForward) {
return isForward ? element.getNextSibling() : element.getPrevSibling();
}
private static boolean isCommentSeparator(@Nullable PsiElement element) {
return element instanceof PsiWhiteSpace && StringUtil.countNewLines(element.getText()) > 1;
}
@NotNull
private static PsiElement findFinalComment(@NotNull PsiElement element, boolean isForward) {
// The element argument may be either a comment or a whitespace node. Find the end of the comment
// moving forward if isForward, or backward, until something other than the same kind of comment is found.
PsiElement target = element, sib = element;
CommentType groupType = null;
while (sib != null) {
if (sib instanceof PsiWhiteSpace) {
if (isCommentSeparator(sib)) {
break; // A "block" of line comments may not contain an empty line.
}
else {
sib = nextSib(sib, isForward);
continue;
}
}
CommentType type;
switch (type = commentTypeOf(sib)) {
case INVALID:
PsiElement parent = sib.getParent();
if (parent instanceof DartFile) return sib;
return findFinalComment(parent, isForward);
case MULTI_LINE_DOC_COMMENT:
return sib;
case MULTI_LINE_COMMENT:
case SINGLE_LINE_DOC_COMMENT:
case SINGLE_LINE_COMMENT:
if (groupType == null) {
groupType = type;
}
if (groupType == type) {
target = sib;
sib = nextSib(sib, isForward);
}
else {
sib = null;
}
break;
case NONE:
sib = null;
break;
}
}
return target;
}
private static boolean isLineComment(@NotNull PsiElement element) {
switch (commentTypeOf(element)) {
case SINGLE_LINE_DOC_COMMENT:
case SINGLE_LINE_COMMENT:
return true;
default:
return false;
}
}
@NotNull
private static CommentType commentTypeOf(@NotNull PsiElement element) {
IElementType type = element.getNode().getElementType();
if (type == DartTokenTypesSets.SINGLE_LINE_COMMENT) {
return CommentType.SINGLE_LINE_COMMENT;
}
else if (type == DartTokenTypesSets.MULTI_LINE_COMMENT) {
return CommentType.MULTI_LINE_COMMENT;
}
else if (type == DartTokenTypesSets.SINGLE_LINE_DOC_COMMENT) {
return CommentType.SINGLE_LINE_DOC_COMMENT;
}
else if (type == DartTokenTypesSets.MULTI_LINE_DOC_COMMENT) {
return CommentType.MULTI_LINE_DOC_COMMENT;
}
else if (DartTokenTypesSets.DOC_COMMENT_CONTENTS.contains(type)) {
// Somehow we got a child of a multi-line comment. Shouldn't happen, but might.
return CommentType.INVALID;
}
else {
return CommentType.NONE;
}
}
private static PsiElement getDeclarationParent(PsiElement element) {
if (isComment(element)) return element;
PsiElement parent = getHeaderParent(element);
if (parent != null) return parent;
parent = PsiTreeUtil.getParentOfType(element, DartVarDeclarationList.class, false);
if (parent != null && (parent.getParent() instanceof DartFile || parent.getParent() instanceof DartClassMembers)) {
return parent;
}
if (element instanceof LeafPsiElement &&
(element.getParent() instanceof DartFile || element.getParent() instanceof DartClassMembers)) {
return element;
}
if (element instanceof DartClassMembers) {
PsiElement last = element.getLastChild();
last = PsiTreeUtil.skipSiblingsBackward(last, LeafPsiElement.class, PsiWhiteSpace.class);
if (last != null) return last;
}
return PsiTreeUtil.getParentOfType(element, DartComponent.class, false);
}
private static boolean crossesHeaderBoundary(PsiElement base, PsiElement sibling) {
// We may want to be more flexible and allow header statements to move up past top-level declarations.
if (isComment(base) || isComment(sibling)) return false;
PsiElement baseType = getHeaderParent(base);
PsiElement sibType = getHeaderParent(sibling);
if (baseType == null && sibType == null) return false;
if (baseType == null || sibType == null) return true;
// Having two library statements is not legal but could happen while editing.
if (baseType instanceof DartLibraryStatement && sibType instanceof DartLibraryStatement) return false;
// Having mixed library and import is also possible but not allowed in this context.
if (baseType instanceof DartLibraryStatement || sibType instanceof DartLibraryStatement) return true;
return false; // both uri-based -- allow moving part & import
}
private static PsiElement getHeaderParent(PsiElement element) {
return PsiTreeUtil.getNonStrictParentOfType(element, DartUriBasedDirective.class, DartLibraryStatement.class);
}
private static boolean isComment(@NotNull final PsiElement element) {
final IElementType type = element.getNode().getElementType();
return DartTokenTypesSets.COMMENTS.contains(type) || DartTokenTypesSets.DOC_COMMENT_CONTENTS.contains(type);
}
private static boolean isSemicolon(PsiElement element) {
return element instanceof LeafPsiElement;
}
private static PsiElement firstMoveableComponent(PsiElement element) {
if (element instanceof DartClassMembers) {
return element.getFirstChild();
}
return element;
}
}
@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;
CodeMover codeMover = new CodeMover(editor, file, info, down);
codeMover.findSourceComponents();
if (!codeMover.hasSourceComponents()) {
return false;
}
codeMover.findSourceLineRange();
if (!codeMover.hasSourceLineRange()) {
return false;
}
codeMover.findTargetComponents();
if (!codeMover.hasTargetComponents()) {
return info.toMove2 == null; // Null if move is prohibited.
}
codeMover.findTargetLineRange();
if (!codeMover.hasTargetLineRange()) {
return info.toMove2 == null; // Null if move is prohibited.
}
if (codeMover.areTargetsAtSameLevel()) {
info.indentTarget = false;
}
info.toMove = codeMover.sourceRange;
info.toMove2 = codeMover.targetRange;
return true;
}
@VisibleForTesting
static Pair<PsiElement, PsiElement> findCommentRange(@NotNull PsiElement element) {
return CodeMover.findCommentRange(element);
}
}