diff --git a/python/src/com/jetbrains/python/actions/AddFieldQuickFix.java b/python/src/com/jetbrains/python/actions/AddFieldQuickFix.java index d39eacb76df9..e98dc658b4f7 100644 --- a/python/src/com/jetbrains/python/actions/AddFieldQuickFix.java +++ b/python/src/com/jetbrains/python/actions/AddFieldQuickFix.java @@ -45,7 +45,7 @@ public class AddFieldQuickFix implements LocalQuickFix { } @Nullable - public static PsiElement appendToInit(PyFunction init, Function callback) { + public static PsiElement appendToMethod(PyFunction init, Function callback) { // add this field as the last stmt of the constructor final PyStatementList stmt_list = init.getStatementList(); PyStatement[] stmts = stmt_list.getStatements(); // NOTE: rather wasteful, consider iterable stmt list @@ -93,7 +93,7 @@ public class AddFieldQuickFix implements LocalQuickFix { if (cls != null && item_name != null) { PyFunction init = cls.findMethodByName(PyNames.INIT, false); if (init != null) { - return appendToInit(init, callback); + return appendToMethod(init, callback); } else { // no init! boldly copy ancestor's. for (PyClass ancestor : cls.iterateAncestorClasses()) { @@ -105,7 +105,7 @@ public class AddFieldQuickFix implements LocalQuickFix { return null; } - appendToInit(new_init, callback); + appendToMethod(new_init, callback); PsiElement add_anchor = null; PyFunction[] meths = cls.getMethods(); diff --git a/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java b/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java index 1d96a3126d58..d95f7665863f 100644 --- a/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java +++ b/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java @@ -11,8 +11,8 @@ import com.jetbrains.python.refactoring.classes.extractSuperclass.PyExtractSuper import com.jetbrains.python.refactoring.classes.pullUp.PyPullUpHandler; import com.jetbrains.python.refactoring.classes.pushDown.PyPushDownHandler; import com.jetbrains.python.refactoring.extractmethod.PyExtractMethodHandler; -import com.jetbrains.python.refactoring.introduce.constant.ConstantIntroduceHandler; -import com.jetbrains.python.refactoring.introduce.field.FieldIntroduceHandler; +import com.jetbrains.python.refactoring.introduce.constant.PyIntroduceConstantHandler; +import com.jetbrains.python.refactoring.introduce.field.PyIntroduceFieldHandler; import com.jetbrains.python.refactoring.introduce.variable.PyIntroduceVariableHandler; /** @@ -26,12 +26,12 @@ public class RefactoringProvider extends RefactoringSupportProvider { @Override public RefactoringActionHandler getIntroduceConstantHandler() { - return new ConstantIntroduceHandler(); + return new PyIntroduceConstantHandler(); } @Override public RefactoringActionHandler getIntroduceFieldHandler() { - return new FieldIntroduceHandler(); + return new PyIntroduceFieldHandler(); } @Override diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java index 392a446d551a..0b24cfee6754 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java @@ -19,6 +19,8 @@ import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.IntroduceTargetChooser; import com.intellij.refactoring.RefactoringActionHandler; +import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; +import com.intellij.refactoring.introduce.inplace.OccurrencesChooser; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.Function; import com.jetbrains.python.PyBundle; @@ -57,21 +59,34 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { while (true); } + protected static void ensureName(IntroduceOperation operation) { + if (operation.getName() == null) { + final Collection suggestedNames = operation.getSuggestedNames(); + if (suggestedNames.size() > 0) { + operation.setName(suggestedNames.iterator().next()); + } + else { + operation.setName("x"); + } + } + } + public enum InitPlace { SAME_METHOD, CONSTRUCTOR, SET_UP } - private static void replaceExpression(PyExpression newExpression, Project project, PsiElement expression) { + @Nullable + private static PsiElement replaceExpression(PyExpression newExpression, Project project, PsiElement expression) { PyExpressionStatement statement = PsiTreeUtil.getParentOfType(expression, PyExpressionStatement.class); if (statement != null) { if (statement.getExpression() == expression) { statement.delete(); - return; + return null; } } - PyPsiUtils.replaceExpression(project, expression, newExpression); + return PyPsiUtils.replaceExpression(project, expression, newExpression); } private final IntroduceValidator myValidator; @@ -83,7 +98,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) { - performAction(project, editor, file, null, InitPlace.SAME_METHOD, false, false, false); + performAction(new IntroduceOperation(project, editor, file, null, false, false, false)); } public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) { @@ -134,16 +149,18 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } public void performAction(@NotNull final Project project, Editor editor, PsiFile file, String name, boolean replaceAll, boolean hasConstructor, boolean isTestClass) { - performAction(project, editor, file, name, InitPlace.SAME_METHOD, replaceAll, hasConstructor, isTestClass); + performAction(new IntroduceOperation(project, editor, file, name, replaceAll, hasConstructor, isTestClass)); } - public void performAction(@NotNull final Project project, Editor editor, PsiFile file, String name, InitPlace initInConstructor, boolean replaceAll, boolean hasConstructor, boolean isTestClass) { + public void performAction(IntroduceOperation operation) { + final PsiFile file = operation.getFile(); if (!CommonRefactoringUtil.checkReadOnlyStatus(file)) { return; } PsiElement element1 = null; PsiElement element2 = null; + final Editor editor = operation.getEditor(); final SelectionModel selectionModel = editor.getSelectionModel(); boolean singleElementSelection = false; if (selectionModel.hasSelection()) { @@ -162,7 +179,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { } } else { - if (smartIntroduce(file, editor, name, initInConstructor, replaceAll, hasConstructor, isTestClass)) { + if (smartIntroduce(operation)) { return; } final CaretModel caretModel = editor.getCaretModel(); @@ -173,6 +190,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { element2 = file.findElementAt(document.getLineEndOffset(lineNumber) - 1); } } + final Project project = operation.getProject(); if (element1 == null || element2 == null) { showCannotPerformError(project, editor); return; @@ -194,7 +212,8 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { if (!checkIntroduceContext(file, editor, element1)) { return; } - performActionOnElement(editor, element1, name, initInConstructor, replaceAll, hasConstructor, isTestClass); + operation.setElement(element1); + performActionOnElement(operation); } private void showCannotPerformError(Project project, Editor editor) { @@ -202,7 +221,9 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { "refactoring.extractMethod"); } - private boolean smartIntroduce(final PsiFile file, final Editor editor, final String name, final InitPlace initInConstructor, final boolean replaceAll, final boolean hasConstructor, final boolean isTestClass) { + private boolean smartIntroduce(final IntroduceOperation operation) { + final Editor editor = operation.getEditor(); + final PsiFile file = operation.getFile(); int offset = editor.getCaretModel().getOffset(); PsiElement elementAtCaret = file.findElementAt(offset); if (!checkIntroduceContext(file, editor, elementAtCaret)) return true; @@ -217,14 +238,16 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { elementAtCaret = elementAtCaret.getParent(); } if (expressions.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) { - performActionOnElement(editor, expressions.get(0), name, initInConstructor, replaceAll, hasConstructor, isTestClass); + operation.setElement(expressions.get(0)); + performActionOnElement(operation); return true; } else if (expressions.size() > 1) { IntroduceTargetChooser.showChooser(editor, expressions, new Pass() { @Override public void pass(PyExpression pyExpression) { - performActionOnElement(editor, pyExpression, name, initInConstructor, replaceAll, hasConstructor, isTestClass); + operation.setElement(pyExpression); + performActionOnElement(operation); } }, new Function() { public String fun(PyExpression pyExpression) { @@ -261,56 +284,94 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { return true; } - private void performActionOnElement(Editor editor, - @NotNull PsiElement element, - String name, - InitPlace initInConstructor, - boolean replaceAll, - boolean hasConstructor, - boolean isTestClass) { - final Project project = element.getProject(); - if (!checkEnabled(project, editor, element, myDialogTitle)) { + private void performActionOnElement(IntroduceOperation operation) { + final Project project = operation.getProject(); + if (!checkEnabled(project, operation.getEditor(), operation.getElement(), myDialogTitle)) { return; } + final PsiElement element = operation.getElement(); final PsiElement parent = element.getParent(); final PyExpression initializer = parent instanceof PyAssignmentStatement ? ((PyAssignmentStatement)parent).getAssignedValue() : (PyExpression)element; + operation.setInitializer(initializer); - final List occurrences; if (initializer.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE) == null) { - occurrences = getOccurrences(element, initializer); + operation.setOccurrences(getOccurrences(element, initializer)); + } + operation.setSuggestedNames(getSuggestedNames(initializer)); + if (operation.getOccurrences().size() == 0) { + operation.setReplaceAll(false); + } + + performActionOnElementOccurrences(operation); + } + + protected void performActionOnElementOccurrences(final IntroduceOperation operation) { + final Editor editor = operation.getEditor(); + if (editor.getSettings().isVariableInplaceRenameEnabled() && !ApplicationManager.getApplication().isUnitTestMode()) { + ensureName(operation); + new OccurrencesChooser(editor) + .showChooser(operation.getElement(), operation.getOccurrences(), new Pass() { + @Override + public void pass(OccurrencesChooser.ReplaceChoice replaceChoice) { + operation.setReplaceAll(replaceChoice == OccurrencesChooser.ReplaceChoice.ALL); + performInplaceIntroduce(operation); + } + }); } else { - occurrences = Collections.emptyList(); + performIntroduceWithDialog(operation); } - Collection possibleNames = getSuggestedNames(initializer); - replaceAll &= occurrences.size() > 0; + } + + protected void performInplaceIntroduce(IntroduceOperation operation) { + final PyAssignmentStatement statement = performRefactoring(operation); + PyTargetExpression target = (PyTargetExpression) statement.getTargets() [0]; + final List occurrences = operation.getOccurrences(); + final InplaceVariableIntroducer introducer = + new PyInplaceVariableIntroducer(target, operation, occurrences); + introducer.performInplaceRename(false, new LinkedHashSet(operation.getSuggestedNames())); + } - if (name == null) { - PyIntroduceDialog dialog = new PyIntroduceDialog(project, initializer, myDialogTitle, myValidator, occurrences.size(), possibleNames, getHelpId(), hasConstructor, isTestClass); + protected void performIntroduceWithDialog(IntroduceOperation operation) { + final Project project = operation.getProject(); + if (operation.getName() == null) { + PyIntroduceDialog dialog = new PyIntroduceDialog(project, myDialogTitle, myValidator, getHelpId(), operation); dialog.show(); if (!dialog.isOK()) { return; } - name = dialog.getName(); - replaceAll = dialog.doReplaceAllOccurrences(); - initInConstructor = dialog.getInitPlace(); + operation.setName(dialog.getName()); + operation.setReplaceAll(dialog.doReplaceAllOccurrences()); + operation.setInitPlace(dialog.getInitPlace()); } - initInConstructor = initInConstructor != null ? initInConstructor : InitPlace.SAME_METHOD; - String assignmentText = name + " = " + initializer.getText().replace("\n", " "); - PsiElement anchor = replaceAll ? findAnchor(occurrences) : PsiTreeUtil.getParentOfType(initializer, PyStatement.class); - PyAssignmentStatement declaration = createDeclaration(project, assignmentText, anchor); - - assert name != null; - declaration = performReplace(project, element, declaration, initializer, occurrences, name, replaceAll, initInConstructor); - declaration = CodeInsightUtilBase.forcePsiPostprocessAndRestoreElement(declaration); + PyAssignmentStatement declaration = performRefactoring(operation); + final Editor editor = operation.getEditor(); editor.getCaretModel().moveToOffset(declaration.getTextRange().getEndOffset()); editor.getSelectionModel().removeSelection(); } + protected PyAssignmentStatement performRefactoring(IntroduceOperation operation) { + PyAssignmentStatement declaration = createDeclaration(operation); + + declaration = performReplace(declaration, operation); + declaration = CodeInsightUtilBase.forcePsiPostprocessAndRestoreElement(declaration); + return declaration; + } + + public PyAssignmentStatement createDeclaration(IntroduceOperation operation) { + final Project project = operation.getProject(); + final PyExpression initializer = operation.getInitializer(); + String assignmentText = operation.getName() + " = " + initializer.getText().replace("\n", " "); + PsiElement anchor = operation.isReplaceAll() + ? findAnchor(operation.getOccurrences()) + : PsiTreeUtil.getParentOfType(initializer, PyStatement.class); + return createDeclaration(project, assignmentText, anchor); + } + protected abstract String getHelpId(); protected PyAssignmentStatement createDeclaration(Project project, String assignmentText, PsiElement anchor) { @@ -333,40 +394,47 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { return PyRefactoringUtil.getOccurrences(expression, context); } - private PyAssignmentStatement performReplace(@NotNull final Project project, - final PsiElement element, - @NotNull final PyAssignmentStatement declaration, - @NotNull final PsiElement expression, - @NotNull final List occurrences, - @NotNull final String name, - final boolean replaceAll, - final InitPlace initInConstructor) { + private PyAssignmentStatement performReplace(@NotNull final PyAssignmentStatement declaration, + final IntroduceOperation operation) { + final PyExpression expression = operation.getInitializer(); + final Project project = operation.getProject(); return new WriteCommandAction(project, expression.getContainingFile()) { protected void run(final Result result) throws Throwable { - final Pair data = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); - if (data == null) { - result.setResult((PyAssignmentStatement)addDeclaration(expression, declaration, occurrences, replaceAll, initInConstructor)); - } - else { - result.setResult((PyAssignmentStatement)addDeclaration(data.first, declaration, occurrences, replaceAll, initInConstructor)); - } + result.setResult(addDeclaration(operation, declaration)); - PyExpression newExpression = createExpression(project, name, declaration); + PyExpression newExpression = createExpression(project, operation.getName(), declaration); - if (replaceAll) { - for (PsiElement occurrence : occurrences) { - replaceExpression(newExpression, project, occurrence); + if (operation.isReplaceAll()) { + List newOccurrences = new ArrayList(); + for (PsiElement occurrence : operation.getOccurrences()) { + final PsiElement replaced = replaceExpression(newExpression, project, occurrence); + if (replaced != null) { + newOccurrences.add(replaced); + } } + operation.setOccurrences(newOccurrences); } else { replaceExpression(newExpression, project, expression); } - postRefactoring(element); + postRefactoring(operation.getElement()); } }.execute().getResultObject(); } + @Nullable + public PyAssignmentStatement addDeclaration(IntroduceOperation operation, PyAssignmentStatement declaration) { + final PsiElement expression = operation.getInitializer(); + final Pair data = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + if (data == null) { + return (PyAssignmentStatement)addDeclaration(expression, declaration, operation); + } + else { + return (PyAssignmentStatement)addDeclaration(data.first, declaration, operation); + } + } + protected PyExpression createExpression(Project project, String name, PyAssignmentStatement declaration) { return PyElementGenerator.getInstance(project).createExpressionFromText(name); } @@ -374,10 +442,25 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { @Nullable protected abstract PsiElement addDeclaration(@NotNull final PsiElement expression, @NotNull final PsiElement declaration, - @NotNull final List occurrences, - final boolean replaceAll, - final InitPlace initInConstructor); + @NotNull IntroduceOperation operation); protected void postRefactoring(PsiElement element) { } + + private static class PyInplaceVariableIntroducer extends InplaceVariableIntroducer { + private final PyTargetExpression myTarget; + + public PyInplaceVariableIntroducer(PyTargetExpression target, + IntroduceOperation operation, + List occurrences) { + super(target, operation.getEditor(), operation.getProject(), "Introduce Variable", + occurrences.toArray(new PsiElement[occurrences.size()]), null); + myTarget = target; + } + + @Override + protected PsiElement checkLocalScope() { + return myTarget.getContainingFile(); + } + } } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceOperation.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceOperation.java new file mode 100644 index 000000000000..06b203f8d655 --- /dev/null +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceOperation.java @@ -0,0 +1,122 @@ +package com.jetbrains.python.refactoring.introduce; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.psi.PyExpression; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author yole + */ +public class IntroduceOperation { + private final Project myProject; + private final Editor myEditor; + private final PsiFile myFile; + private String myName; + private IntroduceHandler.InitPlace myInitPlace = IntroduceHandler.InitPlace.SAME_METHOD; + private boolean myReplaceAll; + private final boolean myHasConstructor; + private final boolean myTestClass; + private PsiElement myElement; + private PyExpression myInitializer; + private List myOccurrences = Collections.emptyList(); + private Collection mySuggestedNames; + + public IntroduceOperation(Project project, + Editor editor, + PsiFile file, + String name, + boolean replaceAll, + boolean hasConstructor, + boolean testClass) { + myProject = project; + myEditor = editor; + myFile = file; + myName = name; + myReplaceAll = replaceAll; + myHasConstructor = hasConstructor; + myTestClass = testClass; + } + + public String getName() { + return myName; + } + + public void setName(@Nullable String name) { + myName = name; + } + + public Project getProject() { + return myProject; + } + + public Editor getEditor() { + return myEditor; + } + + public PsiFile getFile() { + return myFile; + } + + public PsiElement getElement() { + return myElement; + } + + public void setElement(PsiElement element) { + myElement = element; + } + + public IntroduceHandler.InitPlace getInitPlace() { + return myInitPlace; + } + + public void setInitPlace(IntroduceHandler.InitPlace initPlace) { + myInitPlace = initPlace; + } + + public boolean isReplaceAll() { + return myReplaceAll; + } + + public void setReplaceAll(boolean replaceAll) { + myReplaceAll = replaceAll; + } + + public PyExpression getInitializer() { + return myInitializer; + } + + public void setInitializer(PyExpression initializer) { + myInitializer = initializer; + } + + public List getOccurrences() { + return myOccurrences; + } + + public void setOccurrences(List occurrences) { + myOccurrences = occurrences; + } + + public Collection getSuggestedNames() { + return mySuggestedNames; + } + + public void setSuggestedNames(Collection suggestedNames) { + mySuggestedNames = suggestedNames; + } + + public boolean hasConstructor() { + return myHasConstructor; + } + + public boolean isTestClass() { + return myTestClass; + } +} diff --git a/python/src/com/jetbrains/python/refactoring/introduce/PyIntroduceDialog.java b/python/src/com/jetbrains/python/refactoring/introduce/PyIntroduceDialog.java index f9276fd47f6c..77875e714474 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/PyIntroduceDialog.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/PyIntroduceDialog.java @@ -42,26 +42,22 @@ public class PyIntroduceDialog extends DialogWrapper implements PyIntroduceSetti private final String myHelpId; public PyIntroduceDialog(@NotNull final Project project, - @NotNull PyExpression expression, @NotNull final String caption, @NotNull final IntroduceValidator validator, - final int occurrencesCount, - final Collection possibleNames, final String helpId, - boolean hasConstructor, - boolean isTestClass) { + final IntroduceOperation operation) { super(project, true); - myOccurrencesCount = occurrencesCount; + myOccurrencesCount = operation.getOccurrences().size(); myValidator = validator; myProject = project; - myExpression = expression; + myExpression = operation.getInitializer(); myHelpId = helpId; - setUpNameComboBox(possibleNames); + setUpNameComboBox(operation.getSuggestedNames()); setModal(true); setTitle(caption); init(); - setupDialog(hasConstructor, isTestClass); + setupDialog(operation.hasConstructor(), operation.isTestClass()); updateControls(); } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/constant/ConstantIntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java similarity index 79% rename from python/src/com/jetbrains/python/refactoring/introduce/constant/ConstantIntroduceHandler.java rename to python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java index 1b1e0d99debf..8a3a4d04a677 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/constant/ConstantIntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java @@ -7,25 +7,23 @@ import com.jetbrains.python.codeInsight.imports.AddImportHelper; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.refactoring.introduce.IntroduceHandler; +import com.jetbrains.python.refactoring.introduce.IntroduceOperation; import org.jetbrains.annotations.NotNull; import java.util.Collection; -import java.util.List; /** * @author Alexey.Ivanov */ -public class ConstantIntroduceHandler extends IntroduceHandler { - public ConstantIntroduceHandler() { +public class PyIntroduceConstantHandler extends IntroduceHandler { + public PyIntroduceConstantHandler() { super(new ConstantValidator(), PyBundle.message("refactoring.introduce.constant.dialog.title")); } @Override protected PsiElement addDeclaration(@NotNull final PsiElement expression, @NotNull final PsiElement declaration, - @NotNull final List occurrences, - final boolean replaceAll, - InitPlace initInConstructor) { + @NotNull final IntroduceOperation operation) { final PsiElement anchor = expression.getContainingFile(); assert anchor instanceof PyFile; return anchor.addBefore(declaration, AddImportHelper.getFileInsertPosition((PyFile)anchor)); diff --git a/python/src/com/jetbrains/python/refactoring/introduce/field/FieldIntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java similarity index 62% rename from python/src/com/jetbrains/python/refactoring/introduce/field/FieldIntroduceHandler.java rename to python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java index 652f2bdfd1d5..75c694706baf 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/field/FieldIntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java @@ -1,6 +1,9 @@ package com.jetbrains.python.refactoring.introduce.field; +import com.intellij.lang.ASTNode; import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.AccessToken; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.CaretModel; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; @@ -13,6 +16,7 @@ import com.intellij.psi.search.LocalSearchScope; import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; import com.intellij.refactoring.util.CommonRefactoringUtil; import com.intellij.util.Function; import com.jetbrains.python.PyNames; @@ -20,27 +24,26 @@ import com.jetbrains.python.actions.AddFieldQuickFix; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyFunctionBuilder; import com.jetbrains.python.refactoring.introduce.IntroduceHandler; +import com.jetbrains.python.refactoring.introduce.IntroduceOperation; import com.jetbrains.python.refactoring.introduce.variable.PyIntroduceVariableHandler; import com.jetbrains.python.testing.PythonUnitTestUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import javax.swing.*; +import java.util.*; /** * @author Dennis.Ushakov */ -public class FieldIntroduceHandler extends IntroduceHandler { +public class PyIntroduceFieldHandler extends IntroduceHandler { - public FieldIntroduceHandler() { + public PyIntroduceFieldHandler() { super(new IntroduceFieldValidator(), RefactoringBundle.message("introduce.field.title")); } public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) { - performAction(project, editor, file, null, InitPlace.SAME_METHOD, false, true, isTestClass(file, editor)); + performAction(new IntroduceOperation(project, editor, file, null, false, true, isTestClass(file, editor))); } private static boolean isTestClass(PsiFile file, Editor editor) { @@ -76,22 +79,23 @@ public class FieldIntroduceHandler extends IntroduceHandler { @Nullable @Override - protected PsiElement addDeclaration(@NotNull PsiElement expression, @NotNull PsiElement declaration, @NotNull List occurrences, - boolean replaceAll, InitPlace initInConstructor) { + protected PsiElement addDeclaration(@NotNull PsiElement expression, @NotNull PsiElement declaration, @NotNull IntroduceOperation operation) { final PsiElement expr = expression instanceof PyClass ? expression : expression.getParent(); PsiElement anchor = PyUtil.getContainingClassOrSelf(expr); assert anchor instanceof PyClass; final PyClass clazz = (PyClass)anchor; final Project project = anchor.getProject(); - if (initInConstructor == InitPlace.CONSTRUCTOR && !inConstructor(clazz, expression)) { + if (operation.getInitPlace() == InitPlace.CONSTRUCTOR && !inConstructor(expression)) { return AddFieldQuickFix.addFieldToInit(project, clazz, "", new AddFieldDeclaration(declaration)); - } else if (initInConstructor == InitPlace.SET_UP) { - return addFieldToSetUp(project, clazz, declaration); + } else if (operation.getInitPlace() == InitPlace.SET_UP) { + return addFieldToSetUp(clazz, new AddFieldDeclaration(declaration)); } - return PyIntroduceVariableHandler.doIntroduceVariable(expression, declaration, occurrences, replaceAll); + return PyIntroduceVariableHandler.doIntroduceVariable(expression, declaration, operation.getOccurrences(), operation.isReplaceAll()); } - private boolean inConstructor(@Nullable PyClass clazz, @NotNull PsiElement expression) { + private static boolean inConstructor(@NotNull PsiElement expression) { + final PsiElement expr = expression instanceof PyClass ? expression : expression.getParent(); + PyClass clazz = PyUtil.getContainingClassOrSelf(expr); PsiElement current = PyUtil.getConcealingParent(expression); if (clazz != null && current != null && current instanceof PyFunction) { PyFunction init = clazz.findMethodByName(PyNames.INIT, false); @@ -103,18 +107,18 @@ public class FieldIntroduceHandler extends IntroduceHandler { } @Nullable - private static PsiElement addFieldToSetUp(Project project, PyClass clazz, PsiElement declaration) { + private static PsiElement addFieldToSetUp(PyClass clazz, final Function callback) { final PyFunction init = clazz.findMethodByName(PythonUnitTestUtil.TESTCASE_SETUP_NAME, false); if (init != null) { - return AddFieldQuickFix.appendToInit(init, new AddFieldDeclaration(declaration)); + return AddFieldQuickFix.appendToMethod(init, callback); } final PyFunctionBuilder builder = new PyFunctionBuilder(PythonUnitTestUtil.TESTCASE_SETUP_NAME); builder.parameter(PyNames.CANONICAL_SELF); - PyFunction setUp = builder.buildFunction(project, LanguageLevel.getDefault()); + PyFunction setUp = builder.buildFunction(clazz.getProject(), LanguageLevel.getDefault()); final PyStatementList statements = clazz.getStatementList(); final PsiElement anchor = statements.getFirstChild(); setUp = (PyFunction)statements.addBefore(setUp, anchor); - return AddFieldQuickFix.appendToInit(setUp, new AddFieldDeclaration(declaration)); + return AddFieldQuickFix.appendToMethod(setUp, callback); } @Override @@ -217,4 +221,89 @@ public class FieldIntroduceHandler extends IntroduceHandler { text.replaceFirst(PyNames.CANONICAL_SELF + "\\.", self_name + ".")); } } + + @Override + protected void performInplaceIntroduce(IntroduceOperation operation) { + final PyAssignmentStatement statement = performRefactoring(operation); + // put caret on identifier after "self." + putCaretOnFieldName(operation.getEditor(), statement); + PyTargetExpression target = (PyTargetExpression) statement.getTargets() [0]; + final List occurrences = operation.getOccurrences(); + final InplaceVariableIntroducer introducer = + new PyInplaceFieldIntroducer(target, operation, occurrences); + introducer.performInplaceRename(false, new LinkedHashSet(operation.getSuggestedNames())); + } + + private static void putCaretOnFieldName(Editor editor, PyAssignmentStatement statement) { + final PsiElement elementAtOffset = statement.getContainingFile().findElementAt(editor.getCaretModel().getOffset()); + PyQualifiedExpression qExpr = PsiTreeUtil.getParentOfType(elementAtOffset, PyQualifiedExpression.class); + if (qExpr != null && qExpr.getQualifier() == null) { + qExpr = PsiTreeUtil.getParentOfType(qExpr, PyQualifiedExpression.class); + } + if (qExpr != null) { + final ASTNode nameElement = qExpr.getNameElement(); + if (nameElement != null) { + final int offset = nameElement.getTextRange().getStartOffset(); + editor.getCaretModel().moveToOffset(offset); + } + } + } + + private static class PyInplaceFieldIntroducer extends InplaceVariableIntroducer { + private final PyTargetExpression myTarget; + private final PyIntroduceFieldPanel myPanel; + + public PyInplaceFieldIntroducer(PyTargetExpression target, + IntroduceOperation operation, + List occurrences) { + super(target, operation.getEditor(), operation.getProject(), "Introduce Field", + occurrences.toArray(new PsiElement[occurrences.size()]), null); + myTarget = target; + if (!inConstructor(target)) { + myPanel = new PyIntroduceFieldPanel(myProject, operation.isTestClass()); + } + else { + myPanel = null; + } + } + + @Override + protected PsiElement checkLocalScope() { + return myTarget.getContainingFile(); + } + + @Override + protected JComponent getComponent() { + return myPanel == null ? null : myPanel.getRootPanel(); + } + + @Override + public void finish() { + super.finish(); + if (myPanel != null && myPanel.getInitPlace() != InitPlace.SAME_METHOD) { + final AccessToken accessToken = ApplicationManager.getApplication().acquireWriteActionLock(getClass()); + try { + final PyAssignmentStatement initializer = PsiTreeUtil.getParentOfType(myTarget, PyAssignmentStatement.class); + assert initializer != null; + final Function callback = new Function() { + @Override + public PyStatement fun(String s) { + return initializer; + } + }; + final PyClass pyClass = PyUtil.getContainingClassOrSelf(initializer); + if (myPanel.getInitPlace() == InitPlace.CONSTRUCTOR) { + AddFieldQuickFix.addFieldToInit(myProject, pyClass, "", callback); + } + else if (myPanel.getInitPlace() == InitPlace.SET_UP) { + addFieldToSetUp(pyClass, callback); + } + initializer.delete(); + } + finally { + accessToken.finish(); + } + } + } + } } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.form b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.form new file mode 100644 index 000000000000..2f0c34f27925 --- /dev/null +++ b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.form @@ -0,0 +1,40 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.java b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.java new file mode 100644 index 000000000000..3a0fdb340a72 --- /dev/null +++ b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldPanel.java @@ -0,0 +1,38 @@ +package com.jetbrains.python.refactoring.introduce.field; + +import com.intellij.openapi.project.Project; +import com.intellij.refactoring.introduce.inplace.KeyboardComboSwitcher; +import com.jetbrains.python.refactoring.introduce.IntroduceHandler; + +import javax.swing.*; + +/** + * @author yole + */ +public class PyIntroduceFieldPanel { + private JPanel myRootPanel; + private JComboBox myInitializerPlaceCombo; + + public PyIntroduceFieldPanel(Project project, boolean testClass) { + KeyboardComboSwitcher.setupActions(myInitializerPlaceCombo, project); + if (testClass) { + ((DefaultComboBoxModel) myInitializerPlaceCombo.getModel()).addElement("setUp() method"); + } + } + + public JPanel getRootPanel() { + return myRootPanel; + } + + public IntroduceHandler.InitPlace getInitPlace() { + final int index = myInitializerPlaceCombo.getSelectedIndex(); + switch (index) { + case 1: + return IntroduceHandler.InitPlace.CONSTRUCTOR; + case 2: + return IntroduceHandler.InitPlace.SET_UP; + default: + return IntroduceHandler.InitPlace.SAME_METHOD; + } + } +} diff --git a/python/src/com/jetbrains/python/refactoring/introduce/variable/PyIntroduceVariableHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/variable/PyIntroduceVariableHandler.java index 7c0146a86b3e..1e3473c0e3f3 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/variable/PyIntroduceVariableHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/variable/PyIntroduceVariableHandler.java @@ -5,6 +5,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.PyBundle; import com.jetbrains.python.psi.PyStatement; import com.jetbrains.python.refactoring.introduce.IntroduceHandler; +import com.jetbrains.python.refactoring.introduce.IntroduceOperation; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -20,10 +21,8 @@ public class PyIntroduceVariableHandler extends IntroduceHandler { @Override protected PsiElement addDeclaration(@NotNull final PsiElement expression, @NotNull final PsiElement declaration, - @NotNull final List occurrences, - final boolean replaceAll, - InitPlace initInConstructor) { - return doIntroduceVariable(expression, declaration, occurrences, replaceAll); + @NotNull IntroduceOperation operation) { + return doIntroduceVariable(expression, declaration, operation.getOccurrences(), operation.isReplaceAll()); } public static PsiElement doIntroduceVariable(PsiElement expression, diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceConstantTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceConstantTest.java index c0ccdfbdd217..e656d57c9773 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceConstantTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceConstantTest.java @@ -2,7 +2,7 @@ package com.jetbrains.python.refactoring; import com.jetbrains.python.fixtures.PyLightFixtureTestCase; import com.jetbrains.python.psi.LanguageLevel; -import com.jetbrains.python.refactoring.introduce.constant.ConstantIntroduceHandler; +import com.jetbrains.python.refactoring.introduce.constant.PyIntroduceConstantHandler; /** * @author yole @@ -32,7 +32,7 @@ public class PyIntroduceConstantTest extends PyLightFixtureTestCase { private void doTest() { myFixture.configureByFile("/refactoring/introduceConstant/" + getTestName(true) + ".py"); - ConstantIntroduceHandler handler = new ConstantIntroduceHandler(); + PyIntroduceConstantHandler handler = new PyIntroduceConstantHandler(); handler.performAction(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile(), "a", true, false, false); myFixture.checkResultByFile("/refactoring/introduceConstant/" + getTestName(true) + ".after.py"); } diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceFieldTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceFieldTest.java index 35b4cbdbf27e..de405ea2213a 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceFieldTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceFieldTest.java @@ -2,7 +2,8 @@ package com.jetbrains.python.refactoring; import com.jetbrains.python.fixtures.PyLightFixtureTestCase; import com.jetbrains.python.refactoring.introduce.IntroduceHandler; -import com.jetbrains.python.refactoring.introduce.field.FieldIntroduceHandler; +import com.jetbrains.python.refactoring.introduce.IntroduceOperation; +import com.jetbrains.python.refactoring.introduce.field.PyIntroduceFieldHandler; /** * @author yole @@ -22,8 +23,10 @@ public class PyIntroduceFieldTest extends PyLightFixtureTestCase { private void doTest(IntroduceHandler.InitPlace initPlace) { myFixture.configureByFile("/refactoring/introduceField/" + getTestName(true) + ".py"); - FieldIntroduceHandler handler = new FieldIntroduceHandler(); - handler.performAction(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile(), "a", initPlace, true, false, false); + PyIntroduceFieldHandler handler = new PyIntroduceFieldHandler(); + final IntroduceOperation introduceOperation = new IntroduceOperation(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile(), "a", true, false, false); + introduceOperation.setInitPlace(initPlace); + handler.performAction(introduceOperation); myFixture.checkResultByFile("/refactoring/introduceField/" + getTestName(true) + ".after.py"); } }