/* * Copyright 2013 Guidewire Software, Inc. */ package gw.plugin.ij.actions; import com.intellij.codeInsight.editorActions.moveUpDown.LineMover; import com.intellij.codeInsight.editorActions.moveUpDown.LineRange; import com.intellij.lang.Language; import com.intellij.openapi.diagnostic.Logger; 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.FileViewProvider; import com.intellij.psi.JspPsiUtil; import com.intellij.psi.PsiAnonymousClass; import com.intellij.psi.PsiBlockStatement; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiClassInitializer; import com.intellij.psi.PsiCodeBlock; import com.intellij.psi.PsiCodeFragment; import com.intellij.psi.PsiComment; import com.intellij.psi.PsiDoWhileStatement; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiIfStatement; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiStatement; import com.intellij.psi.PsiWhileStatement; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.impl.PsiDocumentManagerImpl; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtilBase; import gw.plugin.ij.lang.GosuLanguage; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; class GosuStatementMover extends LineMover { private static final Logger LOG = Logger.getInstance("gw.plugin.ij.actions.GosuStatementMover"); private PsiElement statementToSurroundWithCodeBlock; @NotNull public static PsiElement[] findStatementsInRange(@NotNull PsiFile file, int startOffset, int endOffset) { Language language = GosuLanguage.instance(); if (language == null) return PsiElement.EMPTY_ARRAY; FileViewProvider viewProvider = file.getViewProvider(); PsiElement element1 = viewProvider.findElementAt(startOffset, language); PsiElement element2 = viewProvider.findElementAt(endOffset - 1, language); if (element1 instanceof PsiWhiteSpace) { startOffset = element1.getTextRange().getEndOffset(); element1 = file.findElementAt(startOffset); } if (element2 instanceof PsiWhiteSpace) { endOffset = element2.getTextRange().getStartOffset(); element2 = file.findElementAt(endOffset - 1); } if (element1 == null || element2 == null) return PsiElement.EMPTY_ARRAY; PsiElement parent = PsiTreeUtil.findCommonParent(element1, element2); if (parent == null) return PsiElement.EMPTY_ARRAY; while (true) { if (parent instanceof PsiStatement) { parent = parent.getParent(); break; } if (parent instanceof PsiCodeBlock) break; if (JspPsiUtil.isInJspFile(parent) && parent instanceof PsiFile) break; if (parent instanceof PsiCodeFragment) break; if (parent == null || parent instanceof PsiFile) return PsiElement.EMPTY_ARRAY; parent = parent.getParent(); } if (!parent.equals(element1)) { while (!parent.equals(element1.getParent())) { element1 = element1.getParent(); } } if (startOffset != element1.getTextRange().getStartOffset()) return PsiElement.EMPTY_ARRAY; if (!parent.equals(element2)) { while (!parent.equals(element2.getParent())) { element2 = element2.getParent(); } } if (endOffset != element2.getTextRange().getEndOffset()) return PsiElement.EMPTY_ARRAY; if (parent instanceof PsiCodeBlock && parent.getParent() instanceof PsiBlockStatement && element1 == ((PsiCodeBlock) parent).getLBrace() && element2 == ((PsiCodeBlock) parent).getRBrace()) { return new PsiElement[]{parent.getParent()}; } PsiElement[] children = parent.getChildren(); ArrayList<PsiElement> array = new ArrayList<>(); boolean flag = false; for (PsiElement child : children) { if (child.equals(element1)) { flag = true; } if (flag && !(child instanceof PsiWhiteSpace)) { array.add(child); } if (child.equals(element2)) { break; } } for (PsiElement element : array) { if (!(element instanceof PsiStatement || element instanceof PsiWhiteSpace || element instanceof PsiComment)) { return PsiElement.EMPTY_ARRAY; } } return PsiUtilBase.toPsiElementArray(array); } @Override public boolean checkAvailable(@NotNull final Editor editor, @NotNull final PsiFile file, @NotNull final MoveInfo info, final boolean down) { if (file instanceof PsiJavaFile) return false; final boolean available = super.checkAvailable(editor, file, info, down); if (!available) return false; LineRange range = info.toMove; range = expandLineRangeToCoverPsiElements(range, 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)); final PsiElement[] statements = findStatementsInRange(file, startOffset, endOffset); if (statements.length == 0) return false; range.firstElement = statements[0]; range.lastElement = statements[statements.length - 1]; if (!checkMovingInsideOutside(file, editor, range, info, down)) { info.toMove2 = null; return true; } return true; } private boolean calcInsertOffset(PsiFile file, final Editor editor, LineRange range, @NotNull final MoveInfo info, final boolean down) { int line = down ? range.endLine + 1 : range.startLine - 1; int startLine = down ? range.endLine : range.startLine - 1; if (line < 0 || startLine < 0) return false; while (true) { final int offset = editor.logicalPositionToOffset(new LogicalPosition(line, 0)); PsiElement element = firstNonWhiteElement(offset, file, true); while (element != null && !(element instanceof PsiFile)) { if (!element.getTextRange().grown(-1).shiftRight(1).contains(offset)) { PsiElement elementToSurround = null; boolean found = false; if ((element instanceof PsiStatement || element instanceof PsiComment) && statementCanBePlacedAlong(element)) { found = true; if (!(element.getParent() instanceof PsiCodeBlock)) { elementToSurround = element; } } else if (element instanceof LeafPsiElement && element.getParent() instanceof PsiCodeBlock) { String rBrace = ((LeafPsiElement) element).getElementType().toString(); if ("}".equals(rBrace)) { // before code block closing brace found = true; } } if (found) { statementToSurroundWithCodeBlock = elementToSurround; info.toMove = range; int endLine = line; if (startLine > endLine) { int tmp = endLine; endLine = startLine; startLine = tmp; } info.toMove2 = down ? new LineRange(startLine, endLine) : new LineRange(startLine, endLine + 1); return true; } } element = element.getParent(); } line += down ? 1 : -1; if (line == 0 || line >= editor.getDocument().getLineCount()) { return false; } } } private static boolean statementCanBePlacedAlong(final PsiElement element) { if (element instanceof PsiBlockStatement) return false; final PsiElement parent = element.getParent(); if (parent instanceof PsiCodeBlock) return true; if (parent instanceof PsiIfStatement && (element == ((PsiIfStatement) parent).getThenBranch() || element == ((PsiIfStatement) parent).getElseBranch())) { return true; } if (parent instanceof PsiWhileStatement && element == ((PsiWhileStatement) parent).getBody()) { return true; } if (parent instanceof PsiDoWhileStatement && element == ((PsiDoWhileStatement) parent).getBody()) { return true; } // know nothing about that return false; } private boolean checkMovingInsideOutside(PsiFile file, final Editor editor, LineRange range, @NotNull final MoveInfo info, final boolean down) { final int offset = editor.getCaretModel().getOffset(); PsiElement elementAtOffset = file.getViewProvider().findElementAt(offset, GosuLanguage.instance()); if (elementAtOffset == null) return false; PsiElement guard = elementAtOffset; do { guard = PsiTreeUtil.getParentOfType(guard, PsiMethod.class, PsiClassInitializer.class, PsiClass.class, PsiComment.class); } while (guard instanceof PsiAnonymousClass); PsiElement brace = itIsTheClosingCurlyBraceWeAreMoving(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/initializer/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, GosuLanguage.instance()); PsiElement newGuard = elementAtInsertOffset; do { newGuard = PsiTreeUtil.getParentOfType(newGuard, PsiMethod.class, PsiClassInitializer.class, PsiClass.class, PsiComment.class); } while (newGuard instanceof PsiAnonymousClass); if (brace != null && PsiTreeUtil.getParentOfType(brace, PsiCodeBlock.class, false) != PsiTreeUtil.getParentOfType(elementAtInsertOffset, PsiCodeBlock.class, false)) { info.indentSource = true; } if (newGuard == guard && isInside(insertOffset, newGuard) == isInside(offset, guard)) return true; // moving in/out nested class is OK if (guard instanceof PsiClass && guard.getParent() instanceof PsiClass) return true; if (newGuard instanceof PsiClass && newGuard.getParent() instanceof PsiClass) return true; return false; } private static boolean isInside(final int offset, final PsiElement guard) { if (guard == null) return false; TextRange inside = guard instanceof PsiMethod ? ((PsiMethod) guard).getBody().getTextRange() : guard instanceof PsiClassInitializer ? ((PsiClassInitializer) guard).getBody().getTextRange() : guard instanceof PsiClass && ((PsiClass) guard).getLBrace() != null && ((PsiClass) guard).getRBrace() != null ? new TextRange(((PsiClass) guard).getLBrace().getTextOffset(), ((PsiClass) guard).getRBrace().getTextOffset()) : guard.getTextRange(); return inside != null && inside.contains(offset); } 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; final PsiElement parent = PsiTreeUtil.findCommonParent(psiRange.getFirst(), psiRange.getSecond()); Pair<PsiElement, PsiElement> elementRange = getElementRange(parent, psiRange.getFirst(), psiRange.getSecond()); if (elementRange == null) return null; int endOffset = elementRange.getSecond().getTextRange().getEndOffset(); Document document = editor.getDocument(); if (endOffset > document.getTextLength()) { LOG.assertTrue(!PsiDocumentManager.getInstance(file.getProject()).isUncommited(document)); LOG.assertTrue(PsiDocumentManagerImpl.checkConsistency(file, document)); } 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.getFirst().getTextOffset()).line); endLine = Math.max(endLine, range.endLine); return new LineRange(startLine, endLine); } private static PsiElement itIsTheClosingCurlyBraceWeAreMoving(final PsiFile file, final 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('}')); } }