meet brand new python move statement

work over PSI instead of document lines
This commit is contained in:
Ekaterina Tuzova
2013-08-24 17:35:25 +04:00
parent 9e53670fff
commit 25504900e1
34 changed files with 549 additions and 531 deletions
+1 -1
View File
@@ -352,7 +352,7 @@
<gotoDeclarationHandler implementation="com.jetbrains.python.codeInsight.PyBreakContinueGotoProvider" order="FIRST"/>
<lang.smartEnterProcessor language="Python"
implementationClass="com.jetbrains.python.codeInsight.editorActions.smartEnter.PySmartEnterProcessor"/>
<statementUpDownMover implementation="com.jetbrains.python.codeInsight.editorActions.moveUpDown.StatementMover" id="statement"
<statementUpDownMover implementation="com.jetbrains.python.codeInsight.editorActions.moveUpDown.PyStatementMover" id="statement"
order="before line"/>
<filePropertyPusher implementation="com.jetbrains.python.psi.impl.PythonLanguageLevelPusher"/>
@@ -0,0 +1,439 @@
package com.jetbrains.python.codeInsight.editorActions.moveUpDown;
import com.intellij.codeInsight.editorActions.moveUpDown.LineMover;
import com.intellij.codeInsight.editorActions.moveUpDown.LineRange;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* User : ktisha
*/
public class PyStatementMover extends LineMover {
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!(file instanceof PyFile)) return false;
final int offset = editor.getCaretModel().getOffset();
PsiElement elementToMove = PyUtil.findNonWhitespaceAtOffset(file, offset);
if (elementToMove == null) return false;
elementToMove = getCommentOrStatement(editor.getDocument(), elementToMove);
info.toMove = new MyLineRange(elementToMove);
info.toMove2 = getDestinationScope(file, editor, elementToMove, down);
info.indentTarget = false;
info.indentSource = false;
return true;
}
@Nullable
private static LineRange getDestinationScope(@NotNull final PsiFile file, @NotNull final Editor editor,
@NotNull final PsiElement elementToMove, boolean down) {
final Document document = file.getViewProvider().getDocument();
if (document == null) return null;
if (elementToMove instanceof PyPassStatement || elementToMove instanceof PyContinueStatement ||
elementToMove instanceof PyBreakStatement) return null;
final int offset = down ? elementToMove.getTextRange().getEndOffset() : elementToMove.getTextRange().getStartOffset();
int lineNumber = down ? document.getLineNumber(offset) + 1 : document.getLineNumber(offset) - 1;
if (moveOutsideFile(elementToMove, document, lineNumber)) return null;
int lineEndOffset = document.getLineEndOffset(lineNumber);
final int startOffset = document.getLineStartOffset(lineNumber);
lineEndOffset = startOffset != lineEndOffset ? lineEndOffset - 1 : lineEndOffset;
final PyStatementList statementList = getStatementList(elementToMove);
final PsiElement destination = getDestinationElement(elementToMove, document, lineEndOffset, down);
if (elementToMove instanceof PsiComment && destination instanceof PsiComment) {
return new LineRange(lineNumber, lineNumber + 1);
}
if (elementToMove instanceof PyClass || elementToMove instanceof PyFunction) {
PyElement scope = statementList == null ? (PyElement)elementToMove.getContainingFile() : statementList;
if (destination != null)
return new ScopeRange(scope, destination, !down, true);
}
final String lineText = document.getText(TextRange.create(startOffset, lineEndOffset));
final boolean isEmptyLine = StringUtil.isEmptyOrSpaces(lineText);
if (isEmptyLine && moveToEmptyLine(elementToMove, down)) return new LineRange(lineNumber, lineNumber + 1);
LineRange scopeRange = moveOut(elementToMove, editor, down);
if (scopeRange != null) return scopeRange;
scopeRange = moveInto(elementToMove, file, editor, down, lineEndOffset);
if (scopeRange != null) return scopeRange;
final PyElement scope = statementList == null ? (PyElement)elementToMove.getContainingFile() : statementList;
return new ScopeRange(scope, destination, !down, true);
}
private static boolean moveOutsideFile(@NotNull final PsiElement elementToMove, @NotNull final Document document, int lineNumber) {
if (lineNumber >= document.getLineCount() || lineNumber < 0) {
final int elementOffset = elementToMove.getTextRange().getStartOffset();
final int lineStartOffset = document.getLineStartOffset(document.getLineNumber(elementOffset));
final int insertIndex = lineNumber < 0 ? 0 : document.getTextLength();
if (elementOffset != lineStartOffset) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
document.insertString(insertIndex, "\n");
PsiDocumentManager.getInstance(elementToMove.getProject()).commitAllDocuments();
}
});
}
else return true;
}
return false;
}
private static boolean moveToEmptyLine(@NotNull final PsiElement elementToMove, boolean down) {
final PyStatementList statementList = getStatementList(elementToMove);
if (statementList != null) {
if (down) {
final PsiElement child = statementList.getLastChild();
if (elementToMove == child && PsiTreeUtil.getNextSiblingOfType(statementList.getParent(), PyStatementPart.class) != null
|| child != elementToMove) {
return true;
}
}
else {
return true;
}
}
return statementList == null;
}
private static PyStatementList getStatementList(@NotNull final PsiElement elementToMove) {
return PsiTreeUtil.getParentOfType(elementToMove, PyStatementList.class, true,
PyStatementWithElse.class, PyLoopStatement.class,
PyFunction.class, PyClass.class);
}
@Nullable
private static ScopeRange moveOut(@NotNull final PsiElement elementToMove, @NotNull final Editor editor, boolean down) {
final PyStatementList statementList = getStatementList(elementToMove);
if (statementList == null) return null;
if ((!down || statementList.getLastChild() != elementToMove) && (down || statementList.getFirstChild() != elementToMove)) {
return null;
}
boolean addBefore = !down;
final PsiElement parent = statementList.getParent();
final PyStatementPart sibling = down ? PsiTreeUtil.getNextSiblingOfType(parent, PyStatementPart.class)
: PsiTreeUtil.getPrevSiblingOfType(parent, PyStatementPart.class);
if (sibling != null) {
final PyStatementList list = sibling.getStatementList();
assert list != null;
return new ScopeRange(list, down ? list.getFirstChild() : list.getLastChild(), !addBefore);
}
else {
PsiElement scope = getScopeForComment(elementToMove, editor, parent, !down);
PsiElement anchor = PsiTreeUtil.getParentOfType(statementList, PyStatement.class);
return scope == null ? null : new ScopeRange(scope, anchor, addBefore);
}
}
private static PsiElement getScopeForComment(@NotNull final PsiElement elementToMove, @NotNull final Editor editor,
@Nullable PsiElement parent, boolean down) {
PsiElement scope = PsiTreeUtil.getParentOfType(parent, PyStatementList.class, PyFile.class);
final int offset = elementToMove.getTextOffset();
PsiElement sibling = elementToMove;
while (scope != null && elementToMove instanceof PsiComment) { // stupid workaround for PY-6408. Related to PSI structure
final PsiElement prevSibling = down ? PsiTreeUtil.getNextSiblingOfType(sibling, PyStatement.class) :
PsiTreeUtil.getPrevSiblingOfType(sibling, PyStatement.class);
if (prevSibling == null) break;
if (editor.offsetToLogicalPosition(prevSibling.getTextOffset()).column ==
editor.offsetToLogicalPosition(offset).column) break;
sibling = scope;
scope = PsiTreeUtil.getParentOfType(scope, PyStatementList.class, PyFile.class);
}
return scope;
}
@Nullable
private static LineRange moveInto(@NotNull final PsiElement elementToMove, @NotNull final PsiFile file,
@NotNull final Editor editor, boolean down, int offset) {
PsiElement rawElement = PyUtil.findNonWhitespaceAtOffset(file, offset);
if (rawElement == null) return null;
return down ? moveDownInto(editor.getDocument(), rawElement) : moveUpInto(elementToMove, editor, rawElement, false);
}
@Nullable
private static LineRange moveUpInto(@NotNull final PsiElement elementToMove, @NotNull final Editor editor,
@NotNull final PsiElement rawElement, boolean down) {
final Document document = editor.getDocument();
PsiElement element = getCommentOrStatement(document, rawElement);
final PyStatementList statementList = getStatementList(elementToMove);
final PsiElement scopeForComment = statementList == null ? null :
getScopeForComment(elementToMove, editor, elementToMove, down);
PyStatementList statementList2 = getStatementList(element);
final int start1 = elementToMove.getTextOffset() - document.getLineStartOffset(document.getLineNumber(elementToMove.getTextOffset()));
final int start2 = element.getTextOffset() - document.getLineStartOffset(document.getLineNumber(element.getTextOffset()));
if (start1 != start2) {
PyStatementList parent2 = PsiTreeUtil.getParentOfType(statementList2, PyStatementList.class);
while (parent2 != scopeForComment && parent2 != null) {
element = PsiTreeUtil.getParentOfType(statementList2, PyStatement.class);
statementList2 = parent2;
parent2 = PsiTreeUtil.getParentOfType(parent2, PyStatementList.class);
}
}
if (statementList2 != null && scopeForComment != statementList2 &&
(statementList2.getLastChild() == element || statementList2.getLastChild() == elementToMove)) {
return new ScopeRange(statementList2, element, false);
}
return null;
}
@Nullable
private static LineRange moveDownInto(@NotNull final Document document, @NotNull final PsiElement rawElement) {
PsiElement element = getCommentOrStatement(document, rawElement);
PyStatementList statementList2 = getStatementList(element);
if (statementList2 != null) { // move to one-line conditional/loop statement
final int number = document.getLineNumber(element.getTextOffset());
final int number2 = document.getLineNumber(statementList2.getParent().getTextOffset());
if (number == number2) {
return new ScopeRange(statementList2, statementList2.getFirstChild(), true);
}
}
final PyStatementPart statementPart = PsiTreeUtil.getParentOfType(rawElement, PyStatementPart.class, true, PyStatement.class,
PyStatementList.class);
final PyFunction functionDefinition = PsiTreeUtil.getParentOfType(rawElement, PyFunction.class, true, PyStatement.class,
PyStatementList.class);
final PyClass classDefinition = PsiTreeUtil.getParentOfType(rawElement, PyClass.class, true, PyStatement.class,
PyStatementList.class);
PyStatementList list = null;
if (statementPart != null) list = statementPart.getStatementList();
else if (functionDefinition != null) list = functionDefinition.getStatementList();
else if (classDefinition != null) list = classDefinition.getStatementList();
if (list != null) {
return new ScopeRange(list, list.getFirstChild(), true);
}
return null;
}
private static PsiElement getDestinationElement(@NotNull final PsiElement elementToMove, @NotNull final Document document,
int lineEndOffset, boolean down) {
PsiElement destination = elementToMove.getContainingFile().findElementAt(lineEndOffset);
if (destination == null) return null;
PsiElement sibling = down ? PsiTreeUtil.getNextSiblingOfType(elementToMove, PyStatement.class) :
PsiTreeUtil.getPrevSiblingOfType(elementToMove, PyStatement.class);
if (elementToMove instanceof PyClass) {
destination = sibling;
}
else if (elementToMove instanceof PyFunction) {
if (!(sibling instanceof PyClass))
destination = sibling;
else destination = null;
}
else {
destination = getCommentOrStatement(document, destination);
}
return destination;
}
@NotNull
private static PsiElement getCommentOrStatement(@NotNull final Document document, @NotNull PsiElement destination) {
final PsiElement statement = PsiTreeUtil.getParentOfType(destination, PyStatement.class, false);
if (statement == null) return destination;
if (destination instanceof PsiComment) {
if (document.getLineNumber(destination.getTextOffset()) == document.getLineNumber(statement.getTextOffset()))
destination = statement;
}
else
destination = statement;
return destination;
}
@Override
public void beforeMove(@NotNull final Editor editor, @NotNull final MoveInfo info, final boolean down) {
final LineRange toMove = info.toMove;
final LineRange toMove2 = info.toMove2;
if (toMove instanceof MyLineRange && toMove2 instanceof ScopeRange) {
PostprocessReformattingAspect.getInstance(editor.getProject()).disablePostprocessFormattingInside(new Runnable() {
@Override
public void run() {
final PsiElement elementToMove = ((MyLineRange)toMove).myElement;
final SelectionModel selectionModel = editor.getSelectionModel();
final CaretModel caretModel = editor.getCaretModel();
final int shift = caretModel.getOffset() - elementToMove.getTextOffset();
final boolean hasSelection = selectionModel.hasSelection();
final int selectionStart = selectionModel.getSelectionStart();
final int selectionEnd = selectionModel.getSelectionEnd();
final int selectionShift = selectionStart - elementToMove.getTextOffset();
int offset;
if (((ScopeRange)toMove2).isTheSameLevel()) {
offset = moveTheSameLevel((ScopeRange)toMove2, (MyLineRange)toMove);
}
else {
offset = moveInOut(((MyLineRange)toMove).myElement, editor, info);
}
caretModel.moveToOffset(offset + shift);
info.toMove2 = info.toMove; //do not move further
if (hasSelection) {
int newSelectionStart = offset + selectionShift;
int newSelectionEnd = newSelectionStart + selectionEnd - selectionStart;
selectionModel.setSelection(newSelectionStart, newSelectionEnd);
}
}
});
}
}
private static int moveTheSameLevel(@NotNull final ScopeRange toMove2, @NotNull final MyLineRange toMove) {
final PsiElement anchor = toMove2.getAnchor();
final PsiElement elementToMove = toMove.myElement;
final PsiElement anchorCopy = anchor.copy();
final PsiElement addedElement = anchor.replace(elementToMove);
elementToMove.replace(anchorCopy);
return addedElement.getTextOffset();
}
private static int moveInOut(@NotNull final PsiElement elementToMove, @NotNull final Editor editor, @NotNull final MoveInfo info) {
boolean removePass = false;
final ScopeRange toMove2 = (ScopeRange)info.toMove2;
final PsiElement scope = toMove2.getScope();
final PsiElement anchor = toMove2.getAnchor();
final Project project = scope.getProject();
if (scope instanceof PyStatementList && !(elementToMove instanceof PsiComment)) {
final PyStatement[] statements = ((PyStatementList)scope).getStatements();
if (statements.length == 1 && statements[0] == anchor && statements[0] instanceof PyPassStatement) {
removePass = true;
}
}
final PsiElement addedElement = toMove2.isAddBefore() ? scope.addBefore(elementToMove, anchor) : scope.addAfter(elementToMove, anchor);
addPassStatement(elementToMove, project);
elementToMove.delete();
final int addedElementLine = editor.getDocument().getLineNumber(addedElement.getTextOffset());
final PsiFile file = scope.getContainingFile();
adjustLineIndents(editor, scope, project, addedElement);
if (removePass) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
final Document document = editor.getDocument();
final int lineNumber = document.getLineNumber(anchor.getTextOffset());
final int endOffset = document.getLineCount() <= lineNumber + 1 ? document.getLineEndOffset(lineNumber)
: document.getLineStartOffset(lineNumber + 1);
document.deleteString(document.getLineStartOffset(lineNumber), endOffset);
PsiDocumentManager.getInstance(elementToMove.getProject()).commitAllDocuments();
}
});
}
int offset = addedElement.getTextRange().getStartOffset();
if (addedElement instanceof PsiComment && offset == 0) { // PsiComment gets broken after adjust indent
final PsiElement psiElement = PyUtil.findNonWhitespaceAtOffset(file, editor.getDocument().getLineEndOffset(addedElementLine) - 1);
if (psiElement != null) {
offset = psiElement.getTextOffset();
}
}
return offset;
}
private static void adjustLineIndents(@NotNull final Editor editor, @NotNull final PsiElement scope, @NotNull final Project project,
@NotNull final PsiElement addedElement) {
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
final Document document = editor.getDocument();
if (!(scope instanceof PsiFile)) {
int line1 = editor.offsetToLogicalPosition(scope.getTextRange().getStartOffset()).line;
int line2 = editor.offsetToLogicalPosition(scope.getTextRange().getEndOffset()).line;
codeStyleManager.adjustLineIndent(scope.getContainingFile(),
new TextRange(document.getLineStartOffset(line1), document.getLineStartOffset(line2)));
}
else {
int line1 = editor.offsetToLogicalPosition(addedElement.getTextRange().getStartOffset()).line;
int line2 = editor.offsetToLogicalPosition(addedElement.getTextRange().getEndOffset()).line;
codeStyleManager.adjustLineIndent(scope.getContainingFile(),
new TextRange(document.getLineStartOffset(line1), document.getLineStartOffset(line2)));
}
}
private static void addPassStatement(@NotNull final PsiElement elementToMove, @NotNull final Project project) {
final PyStatementList initialScope = getStatementList(elementToMove);
if (initialScope != null && !(elementToMove instanceof PsiComment)) {
if (initialScope.getStatements().length == 1) {
final PyPassStatement passStatement = PyElementGenerator.getInstance(project).createPassStatement();
initialScope.addAfter(passStatement, initialScope.getStatements()[initialScope.getStatements().length - 1]);
}
}
}
// use to keep element
static class MyLineRange extends LineRange {
public PsiElement myElement;
public MyLineRange(@NotNull PsiElement element) {
super(element);
myElement = element;
}
}
// Use when element scope changed
static class ScopeRange extends LineRange {
private PsiElement myScope;
private PsiElement myAnchor;
private boolean addBefore;
private boolean theSameLevel;
public ScopeRange(@NotNull PsiElement scope, @Nullable PsiElement anchor, boolean before) {
super(scope);
myScope = scope;
myAnchor = anchor;
addBefore = before;
}
public ScopeRange(PyElement scope, PsiElement anchor, boolean before, boolean b) {
super(scope);
myScope = scope;
myAnchor = anchor;
addBefore = before;
theSameLevel = b;
}
public PsiElement getAnchor() {
return myAnchor;
}
public PsiElement getScope() {
return myScope;
}
public boolean isAddBefore() {
return addBefore;
}
public boolean isTheSameLevel() {
return theSameLevel;
}
}
}
@@ -1,510 +0,0 @@
package com.jetbrains.python.codeInsight.editorActions.moveUpDown;
import com.intellij.codeInsight.CodeInsightUtilCore;
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.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* User : catherine
*/
public class StatementMover extends LineMover {
@Nullable private PyStatementList myStatementListToAddPass;
@Nullable private PyStatementList myStatementListToAddPassAfter;
@Nullable private PsiElement myStatementToIncreaseIndent;
@Nullable private PsiElement myStatementToDecreaseIndent;
@Nullable private PyStatement myStatementToAddLinebreak;
@Nullable private PsiElement myStatementToMove;
@Nullable private PyStatementPart myStatementPartToRemovePass;
private boolean moveToEmptyLine = false;
private boolean theSameLevel;
private void init(@NotNull final Editor editor, @NotNull final MoveInfo info, final boolean down) {
LineRange range = StatementUpDownMover.getLineRangeFromSelection(editor);
int nearLine = down ? range.endLine : range.startLine;
info.toMove = range;
info.toMove2 = new LineRange(nearLine, nearLine + 1);
}
@Override
public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
if (!(file instanceof PyFile)) return false;
init(editor, info, down);
Document document = editor.getDocument();
String lineToMove = document.getText(new TextRange(getLineStartSafeOffset(document, info.toMove.startLine), getLineStartSafeOffset(document, info.toMove.endLine)));
if (StringUtil.isEmptyOrSpaces(lineToMove)) {
info.toMove2 = info.toMove;
return true;
}
cleanUp();
//find statement
myStatementToMove = findStatement(editor, file, info);
// check if we want to move statement
if (myStatementToMove == null) return false;
//do not move pass statement
if (myStatementToMove instanceof PyPassStatement) {
info.toMove2 = info.toMove;
return true;
}
if ( myStatementToMove instanceof PyBreakStatement
|| myStatementToMove instanceof PyContinueStatement) {
final PyLoopStatement parent = PsiTreeUtil.getParentOfType(myStatementToMove, PyLoopStatement.class);
if (parent != null) {
final PyStatementPart part = PsiTreeUtil.getChildOfType(parent, PyStatementPart.class);
if (part != null && part.getStatementList() != null) {
PyStatementList statementList = part.getStatementList();
if (myStatementToMove.getParent() instanceof PyStatementList) {
statementList = (PyStatementList)myStatementToMove.getParent();
}
if (statementList != null && statementList.getStatements().length > 0) {
if ((statementList.getStatements()[0] == myStatementToMove && !down)
|| (statementList.getStatements()[statementList.getStatements().length-1] == myStatementToMove && down)) {
info.toMove2 = info.toMove;
return true;
}
}
}
}
}
// do not move last statement down and first up
if (isFirstOrLastStatement(down)) {
info.toMove2 = info.toMove;
return true;
}
expandLineRangeToStatement(info, editor, down, file);
//is move from one part of compound statement to another
theSameLevel = isTheSameIndentLevel(info, editor, file, down);
//check we move statement into compound or out of compound
if (isMoveOut(info, editor, file, down)) {
myStatementToDecreaseIndent = myStatementToMove;
if (down)
info.toMove2 = new LineRange(myStatementToMove);
}
else {
LineRange range = StatementUpDownMover.getLineRangeFromSelection(editor);
final int maxLine = editor.getDocument().getLineCount();
if (range.startLine == 0 && !down) return false;
if (range.endLine >= maxLine && down) return false;
}
if (isMoveToCompound(info, editor, file, down) && !moveToEmptyLine) {
myStatementToIncreaseIndent = myStatementToMove;
if (!down && !theSameLevel)
info.toMove2 = new LineRange(myStatementToMove);
}
//find statement to add pass if we removed last statement from PyStatementPart
PyElement statementPart = getStatementParts(info, editor, file, down).first;
if (statementPart instanceof PyStatementPart) {
PyStatementList statementList = ((PyStatementPart)statementPart).getStatementList();
if (statementList != null && statementList.getStatements().length == 1 && !(myStatementToMove instanceof PsiComment) &&
!isMoveToEmptyLine(editor, info, down)) {
if (theSameLevel) {
myStatementListToAddPassAfter = statementList;
}
else
myStatementListToAddPass = statementList;
}
}
return true;
}
private void cleanUp() {
//reset
myStatementListToAddPass = null;
myStatementListToAddPassAfter = null;
myStatementToDecreaseIndent = null;
myStatementToIncreaseIndent = null;
myStatementToAddLinebreak = null;
myStatementPartToRemovePass = null;
}
private boolean isFirstOrLastStatement(boolean down) {
PyFunction function = PsiTreeUtil.getParentOfType(myStatementToMove, PyFunction.class, true);
if (function != null) {
PyStatementList statementList = function.getStatementList();
if (statementList != null && statementList.getStatements().length > 0) {
if ((statementList.getStatements()[0] == myStatementToMove && !down)
|| (statementList.getStatements()[statementList.getStatements().length-1] == myStatementToMove && down)) {
return true;
}
}
}
PyClass cl = PsiTreeUtil.getParentOfType(myStatementToMove, PyClass.class, true);
if (cl != null) {
PyStatementList statementList = cl.getStatementList();
if (statementList.getStatements().length > 0) {
if ((statementList.getStatements()[0] == myStatementToMove && !down)
|| (statementList.getStatements()[statementList.getStatements().length-1] == myStatementToMove && down)) {
return true;
}
}
}
return false;
}
@Nullable
private PsiElement findStatement(Editor editor, PsiFile file, MoveInfo info) {
final Document doc = editor.getDocument();
final int offset1 = getLineStartSafeOffset(doc, info.toMove.startLine);
PsiElement element1 = file.findElementAt(offset1);
if (element1 != null) {
if (element1 instanceof PsiWhiteSpace) {
element1 = PyPsiUtils.getSignificantToTheRight(element1, false);
}
final PyStatement statement = PsiTreeUtil.getParentOfType(element1, PyStatement.class, false);
if (element1 instanceof PsiComment) return element1;
return statement;
}
return null;
}
private void expandLineRangeToStatement(MoveInfo info, Editor editor, boolean down, PsiFile file) {
Document doc = editor.getDocument();
TextRange textRange = myStatementToMove.getTextRange();
LineRange range = new LineRange(doc.getLineNumber(textRange.getStartOffset()),
doc.getLineNumber(textRange.getEndOffset())+1);
int nearLine = down ? range.endLine : range.startLine - 1;
if (nearLine < 0) nearLine = 0;
info.toMove = range;
info.toMove2 = new LineRange(nearLine, nearLine + 1);
moveToEmptyLine = isMoveToEmptyLine(editor, info, down);
if (moveToEmptyLine) return;
// if try to move to the function or to the class
int offset2 = getLineStartSafeOffset(doc, info.toMove2.startLine);
PsiElement element2 = file.findElementAt(offset2);
if (element2 != null) {
if (element2 instanceof PsiWhiteSpace) {
PsiElement tmp = PyPsiUtils.getSignificantToTheRight(element2, false);
if (tmp != null &&
editor.offsetToLogicalPosition(tmp.getTextRange().getStartOffset()).line
== info.toMove2.startLine) {
element2 = tmp;
}
}
//in case of multiline docstring
PyElement parent2 = PsiTreeUtil.getParentOfType(element2, PyStringLiteralExpression.class);
if (parent2 != null && parent2.getParent() instanceof PyExpressionStatement) {
TextRange textRange2 = parent2.getTextRange();
info.toMove2 = new LineRange(doc.getLineNumber(textRange2.getStartOffset()),
doc.getLineNumber(textRange2.getEndOffset())+1);
}
parent2 = PsiTreeUtil.getParentOfType(element2, PyFunction.class);
PyElement parent1 = PsiTreeUtil.getParentOfType(myStatementToMove, PyFunction.class);
if (parent2 != null && parent2 != parent1) {
final TextRange textRange2 = parent2.getTextRange();
if (myStatementToMove instanceof PsiComment) {
myStatementToIncreaseIndent = myStatementToMove;
return;
}
final int endOffset = doc.getLineNumber(textRange2.getEndOffset())+1;
info.toMove2 = new LineRange(doc.getLineNumber(textRange2.getStartOffset()), endOffset);
}
parent2 = PsiTreeUtil.getParentOfType(element2, PyClass.class);
parent1 = PsiTreeUtil.getParentOfType(myStatementToMove, PyClass.class);
if (parent2 != null && parent2 != parent1) {
final TextRange textRange2 = parent2.getTextRange();
if (myStatementToMove instanceof PsiComment) {
myStatementToIncreaseIndent = myStatementToMove;
return;
}
final int endOffset = doc.getLineNumber(textRange2.getEndOffset())+1;
info.toMove2 = new LineRange(doc.getLineNumber(textRange2.getStartOffset()), endOffset);
}
}
}
private boolean isMoveToEmptyLine(Editor editor, MoveInfo info, boolean down) {
final Document document = editor.getDocument();
if (document.getLineCount() >= info.toMove2.endLine) {
final TextRange range = new TextRange(getLineStartSafeOffset(document,info.toMove2.startLine),
getLineStartSafeOffset(document, info.toMove2.endLine));
final String lineToMoveTo = document.getText(range);
if (StringUtil.isEmptyOrSpaces(lineToMoveTo)) {
if (myStatementToMove instanceof PyFunction || myStatementToMove instanceof PyClass) {
final CommonCodeStyleSettings indentOptions = CodeStyleSettingsManager.getInstance(editor.getProject()).
getCurrentSettings().getCommonSettings(PythonLanguage.getInstance());
final int blankLines = myStatementToMove instanceof PyFunction? indentOptions.BLANK_LINES_AROUND_METHOD :
indentOptions.BLANK_LINES_AROUND_CLASS;
final int nearLine = down ? info.toMove2.endLine + blankLines - 1: info.toMove2.startLine - blankLines;
if (document.getLineCount() > nearLine && nearLine > 0)
info.toMove2 = new LineRange(nearLine, nearLine + 1);
return false;
}
return true;
}
}
return false;
}
/**
* main indent logic
* @return first is the element which we move
* second is the element we move to
*/
private Pair<PyElement, PyElement> getStatementParts(MoveInfo info, Editor editor, PsiFile file, boolean down) {
PsiElement element1 = myStatementToMove;
PyElement statementPart1 = PsiTreeUtil.getParentOfType(element1, PyStatementPart.class, PyWithStatement.class);
int offset2 = getLineStartSafeOffset(editor.getDocument(), info.toMove2.startLine);
PsiElement element2 = file.findElementAt(offset2-1);
if (element2 instanceof PsiWhiteSpace) {
if (down) {
final PsiElement tmp = PyPsiUtils.getSignificantToTheRight(element2, false);
TokenSet secondParts = TokenSet.create(PyTokenTypes.ELSE_KEYWORD, PyTokenTypes.ELIF_KEYWORD,
PyTokenTypes.FINALLY_KEYWORD, PyTokenTypes.EXCEPT_KEYWORD);
if (tmp != null &&
(editor.offsetToLogicalPosition(tmp.getTextRange().getStartOffset()).line == info.toMove2.startLine ||
secondParts.contains(tmp.getNode().getElementType())))
element2 = tmp;
} else {
final PsiElement tmp = PyPsiUtils.getSignificantToTheRight(element2, false);
if (tmp != null) {
int start = editor.offsetToLogicalPosition(tmp.getParent().getTextRange().getStartOffset()).line;
int end = editor.offsetToLogicalPosition(tmp.getParent().getTextRange().getEndOffset()).line;
if (start == info.toMove2.startLine && (start == end || tmp.getParent() instanceof PyClass || tmp.getParent() instanceof PyFunction))
element2 = tmp;
else
element2 = PyPsiUtils.getSignificantToTheLeft(element2, true);
}
else {
element2 = PyPsiUtils.getSignificantToTheLeft(element2, true);
}
}
}
PyElement statementPart2 = PsiTreeUtil.getParentOfType(element2, PyStatementPart.class, PyWithStatement.class);
//in case we move first or last line outside if statement
if (statementPart2 instanceof PyStatementPart) {
int start = editor.offsetToLogicalPosition(statementPart2.getTextRange().getStartOffset()).line;
int end = editor.offsetToLogicalPosition(statementPart2.getTextRange().getEndOffset()).line;
if (!down && info.toMove2.startLine == start && start != end) {
statementPart2 = PsiTreeUtil.getParentOfType(statementPart2, PyStatementPart.class);
}
else {
final PsiElement parent2 = statementPart2.getParent();
if (parent2 instanceof PyTryExceptStatement && statementPart1 != null && parent2 != statementPart1.getParent() && !down) {
if (parent2.getParent() instanceof PyStatementList) {
final PyStatementList stList = (PyStatementList)parent2.getParent();
final PyStatement[] statements = stList.getStatements();
if (statements[statements.length-1] == parent2) {
statementPart2 = PsiTreeUtil.getParentOfType(stList, PyStatementPart.class);
}
}
}
if (statementPart2 != null) {
PyStatementList stList = ((PyStatementPart)statementPart2).getStatementList();
if (stList != null && stList.getStatements().length > 0) {
if (down && stList.getStatements()[stList.getStatements().length-1] == element2) {
PyStatementPart parent = PsiTreeUtil.getParentOfType(statementPart2, PyStatementPart.class);
if (parent != null) {
stList = parent.getStatementList();
if (stList != null && stList.getStatements().length > 0) {
if (stList.getStatements()[stList.getStatements().length-1] == statementPart2) {
statementPart2 = parent;
}
}
}
}
}
}
}
}
return new Pair<PyElement, PyElement>(statementPart1, statementPart2);
}
private boolean isMoveToCompound(MoveInfo info, Editor editor, PsiFile file, boolean down) {
final Pair<PyElement, PyElement> statementParts = getStatementParts(info, editor, file, down);
final PyElement statementPart1 = statementParts.first;
final PyElement statementPart2 = statementParts.second;
if (statementPart2 != null) {
if (statementPart2 instanceof PyStatementPart)
prepareToStatement((PyStatementPart)statementPart2, editor.getDocument());
if (statementPart1 == null) return true;
if (statementPart1.getParent() != statementPart2.getParent()) {
final PsiElement commonParent = PsiTreeUtil.findCommonParent(statementPart1, statementPart2);
if (PsiTreeUtil.isAncestor(statementPart2, statementPart1, false)) return false;
if ((commonParent instanceof PyIfStatement) || (commonParent instanceof PyLoopStatement) ||
(commonParent instanceof PyStatementPart) || (commonParent instanceof PyWithStatement) ||
(commonParent instanceof PyTryExceptStatement))
return true;
}
}
return false;
}
private boolean isMoveOut(MoveInfo info, Editor editor, PsiFile file, boolean down) {
Pair<PyElement, PyElement> insertDeleteParts = getStatementParts(info, editor, file, down);
PyElement statementPart1 = insertDeleteParts.first;
PyElement statementPart2 = insertDeleteParts.second;
if (statementPart1 != null) {
if (statementPart2 == null) return true;
if (statementPart1.getParent() != statementPart2.getParent()) {
PsiElement commonParent = PsiTreeUtil.findCommonParent(statementPart1, statementPart2);
if (!(commonParent instanceof PyIfStatement) && !(commonParent instanceof PyLoopStatement) && !(commonParent instanceof PyStatementPart)
&& !(commonParent instanceof PyWithStatement))
return true;
if (PsiTreeUtil.isAncestor(statementPart2, statementPart1, false)) return true;
}
}
return false;
}
private void prepareToStatement(PyStatementPart part, Document document) {
if (part.getParent() == myStatementToMove) return;
int partLine = document.getLineNumber(part.getTextRange().getStartOffset());
PyStatementList stList = part.getStatementList();
if (stList == null) return;
if (stList.getStatements().length < 1) return;
int statementLine = document.getLineNumber(stList.getStatements()[0].getTextRange().getStartOffset());
if (partLine == statementLine) {
myStatementToAddLinebreak = stList.getStatements()[0];
}
}
private boolean isTheSameIndentLevel(MoveInfo info, Editor editor, PsiFile file, boolean down) {
Pair<PyElement, PyElement> statementParts = getStatementParts(info, editor, file, down);
PyElement statementPart1 = statementParts.first;
PyElement statementPart2 = statementParts.second;
if (statementPart2 instanceof PyStatementPart) {
PyStatementList stList = ((PyStatementPart)statementPart2).getStatementList();
if (stList != null && stList.getStatements().length == 1 && stList.getStatements()[0] instanceof PyPassStatement ||
moveToEmptyLine) {
myStatementPartToRemovePass = (PyStatementPart)statementParts.second;
}
}
final boolean fromExceptToElse = statementPart1 instanceof PyExceptPart && statementPart2 instanceof PyTryPart;
return !fromExceptToElse &&
statementPart2 != null &&
statementPart1 != null &&
statementPart1.getParent() == statementPart2.getParent() ||
statementPart2 == statementPart1;
}
private void decreaseIndent(Editor editor) {
CodeStyleSettings.IndentOptions indentOptions = CodeStyleSettingsManager.getInstance(editor.getProject()).
getCurrentSettings().getIndentOptions(PythonFileType.INSTANCE);
assert indentOptions != null;
final Document document = editor.getDocument();
final int startLine = editor.offsetToLogicalPosition(myStatementToDecreaseIndent.getTextRange().getStartOffset()).line;
final int endLine = editor.offsetToLogicalPosition(myStatementToDecreaseIndent.getTextRange().getEndOffset()).line;
for (int line = startLine; line <= endLine; ++line) {
final int offset = document.getLineStartOffset(line);
document.deleteString(offset, offset + indentOptions.INDENT_SIZE);
}
myStatementToDecreaseIndent = null;
}
private void increaseIndent(final Editor editor) {
CodeStyleSettings.IndentOptions indentOptions = CodeStyleSettingsManager.getInstance(editor.getProject()).
getCurrentSettings().getIndentOptions(PythonFileType.INSTANCE);
assert indentOptions != null;
final Document document = editor.getDocument();
final int startLine = editor.offsetToLogicalPosition(myStatementToIncreaseIndent.getTextRange().getStartOffset()).line;
final int endLine = editor.offsetToLogicalPosition(myStatementToIncreaseIndent.getTextRange().getEndOffset()).line;
for (int line = startLine; line <= endLine; ++line) {
final int offset = document.getLineStartOffset(line);
document.insertString(offset, StringUtil.repeat(" ", indentOptions.INDENT_SIZE));
}
myStatementToIncreaseIndent = null;
}
@Override
public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) {
if (myStatementToAddLinebreak != null) {
//PY-950
TextRange textRange = myStatementToAddLinebreak.getTextRange();
int line = editor.getDocument().getLineNumber(textRange.getStartOffset());
CodeStyleSettings.IndentOptions indentOptions = CodeStyleSettingsManager.getInstance(editor.getProject()).
getCurrentSettings().getIndentOptions(PythonFileType.INSTANCE);
PsiElement whiteSpace = myStatementToAddLinebreak.getContainingFile().findElementAt(
editor.getDocument().getLineStartOffset(line));
String indent = StringUtil.repeatSymbol(' ', indentOptions.INDENT_SIZE);
PyStatementWithElse statementWithElse = PsiTreeUtil.getParentOfType(myStatementToAddLinebreak, PyStatementWithElse.class);
if (statementWithElse != null && statementWithElse.getParent() instanceof PyFile) indent = "\n";
if (whiteSpace instanceof PsiWhiteSpace) indent += whiteSpace.getText();
if (down || theSameLevel) indent += StringUtil.repeatSymbol(' ', indentOptions.INDENT_SIZE);
if (theSameLevel) info.toMove = info.toMove2;
editor.getDocument().insertString(textRange.getStartOffset(), indent);
}
// add pass statement if needed
if (myStatementListToAddPass != null) {
final PyPassStatement passStatement =
PyElementGenerator.getInstance(editor.getProject()).createFromText(LanguageLevel.getDefault(), PyPassStatement.class, PyNames.PASS);
if (!down) {
myStatementListToAddPass.add(passStatement);
}
else
myStatementListToAddPass.addBefore(passStatement, myStatementListToAddPass.getStatements()[0]);
CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myStatementListToAddPass);
}
if (myStatementToIncreaseIndent != null) {
if (!down && myStatementPartToRemovePass != null && myStatementToAddLinebreak == null) {
info.toMove2 = new LineRange(info.toMove2.startLine-1, info.toMove2.endLine);
}
increaseIndent(editor);
}
if (myStatementToDecreaseIndent != null) {
decreaseIndent(editor);
}
}
@Override
public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) {
// add pass statement if needed
if (myStatementListToAddPassAfter != null && myStatementListToAddPassAfter.isValid()) {
final PyPassStatement passStatement =
PyElementGenerator.getInstance(editor.getProject()).createFromText(LanguageLevel.getDefault(), PyPassStatement.class, PyNames.PASS);
myStatementListToAddPassAfter.add(passStatement);
CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(myStatementListToAddPassAfter);
}
// remove obsolete pass statement
if (myStatementPartToRemovePass != null) {
PyStatementList statementList = myStatementPartToRemovePass.getStatementList();
if (statementList != null) {
PyPsiUtils.removeRedundantPass(statementList);
final PsiElement whitespace = myStatementPartToRemovePass.getNextSibling();
if (whitespace instanceof PsiWhiteSpace)
whitespace.delete();
}
}
cleanUp();
}
}
@@ -1167,7 +1167,7 @@ public class PyUtil {
@Nullable
public static PsiElement findPrevAtOffset(PsiFile psiFile, int caretOffset, Class ... toSkip) {
PsiElement element = psiFile.findElementAt(caretOffset);
if (element == null) {
if (element == null || caretOffset < 0) {
return null;
}
int lineStartOffset = 0;
@@ -1187,7 +1187,7 @@ public class PyUtil {
public static PsiElement findNonWhitespaceAtOffset(PsiFile psiFile, int caretOffset) {
PsiElement element = findNextAtOffset(psiFile, caretOffset, PsiWhiteSpace.class);
if (element == null)
element = findPrevAtOffset(psiFile, caretOffset, PsiWhiteSpace.class);
element = findPrevAtOffset(psiFile, caretOffset-1, PsiWhiteSpace.class);
return element;
}
@@ -1212,7 +1212,7 @@ public class PyUtil {
int lineNumber = document.getLineNumber(caretOffset);
lineEndOffset = document.getLineEndOffset(lineNumber);
}
while (caretOffset <= lineEndOffset && instanceOf(element, toSkip)) {
while (caretOffset < lineEndOffset && instanceOf(element, toSkip)) {
caretOffset++;
element = psiFile.findElementAt(caretOffset);
}
@@ -1,3 +1,4 @@
def f():
#comment
if True:
@@ -2,4 +2,4 @@ if True:
a = 1
else:
a = 2
#comment <caret>
#comment <caret>
+1 -1
View File
@@ -3,4 +3,4 @@ def f():
a = 1
else:
a = 2
#comment
#comment
@@ -7,4 +7,4 @@ class A:
pass
<selection>def<caret> bar(self):
pass</selection>
pass</selection>
@@ -1,7 +1,7 @@
class A:
<selection>def<caret> bar(self):
pass</selection>
pass</selection>
def foo(self):
pass
@@ -2,4 +2,4 @@ def a():
zoo(1)
def b():
pass
pass
@@ -1,5 +1,5 @@
def a():
def b():
pass
zoo(1)
+1 -1
View File
@@ -4,4 +4,4 @@ class A:
def c(self):
doSomething3()
def b(self):
doSomething2()
doSomething2()
@@ -1,6 +1,6 @@
def a():
if True:
a = 1
else:
c = 1
else:
b = 2
+2 -1
View File
@@ -1,3 +1,4 @@
if value is not None:
if not False or value <= 2:
print "here"
pass
print "here"
+6
View File
@@ -0,0 +1,6 @@
def f():
if True:
a = 1
else:
a = 2
#comment <caret>
+6
View File
@@ -0,0 +1,6 @@
def f():
if True:
a = 1
else:
a = 2
#comment <caret>
@@ -0,0 +1,6 @@
def f():
if True:
a = 1
else:
a = 2
#comment <caret>
@@ -0,0 +1,7 @@
def f():
if True:
a = 1
else:
a = 2
#comment <caret>
@@ -0,0 +1,6 @@
def f():
if True:
a = 1
else:
a = 2
#comment <caret>
@@ -0,0 +1,6 @@
def f():
if True:
a = 1
else:
#comment <caret>
a = 2
+3
View File
@@ -0,0 +1,3 @@
for item in range(1,
3):
b = 2<caret>
@@ -0,0 +1,4 @@
for item in range(1,
3):
pass
b = 2<caret>
@@ -0,0 +1,4 @@
b = 2<caret>
for item in range(1,
3):
pass
@@ -4,4 +4,5 @@ def test(a, b=1):
else:
for n in range(10):
print(b)
pass
print(b)
@@ -2,5 +2,6 @@ if condition:
pass
elif other_condition:
if another_one:
if T<caret>rue: a = 1 # <- move statement up here
else: b = 2
pass
if True: a = 1 # <- move statement up here
else: b = 2
@@ -1,4 +1,4 @@
c = 1
if True:
a = 1
b = 2
b = 2
@@ -1,4 +1,5 @@
test2()
def foo():
test()
pass
test()
test1()
@@ -1,4 +1,5 @@
test2()
test()
def foo():
test()
pass
test1()
@@ -1,3 +1,4 @@
if True:
a = 1 # <- move statement down here
else:
+6
View File
@@ -0,0 +1,6 @@
if True:
a = 1
else:
#comment
a =<caret> 3
a = 2
@@ -0,0 +1,6 @@
if True:
a = 1
else:
#comment
a = 2
a =<caret> 3
@@ -0,0 +1,6 @@
if True:
a = 1
else:
a =<caret> 3
#comment
a = 2
@@ -4,4 +4,5 @@ try:
except:
print(zoo(0).foo(2))
except:
zoo<caret>(3)
pass
zoo(3)
@@ -6,7 +6,7 @@ import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.LanguageLevel;
/**
* @author Alexey.Ivanov
* User : ktisha
*/
public class PyStatementMoverTest extends PyTestCase {
private void doTest() {
@@ -177,6 +177,21 @@ public class PyStatementMoverTest extends PyTestCase {
doTest();
}
public void testSameLevelInIf() {
doTest();
}
public void testLastComment() { // PY-6408
doTest();
}
public void testLastComment1() { //PY-6408
doTest();
}
public void testMultiCompound() { //PY-7658
doTest();
}
public void testWith() { // PY-5202
try {
setLanguageLevel(LanguageLevel.PYTHON27);