PY-17265 Cleanup related to transition to Java 8
[idea/community.git] / python / src / com / jetbrains / python / refactoring / move / PyMoveSymbolDelegate.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.refactoring.move;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.openapi.actionSystem.DataContext;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.editor.Document;
22 import com.intellij.openapi.editor.Editor;
23 import com.intellij.openapi.editor.SelectionModel;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.util.TextRange;
26 import com.intellij.openapi.util.text.StringUtil;
27 import com.intellij.psi.*;
28 import com.intellij.psi.util.PsiTreeUtil;
29 import com.intellij.refactoring.BaseRefactoringProcessor;
30 import com.intellij.refactoring.RefactoringBundle;
31 import com.intellij.refactoring.move.MoveCallback;
32 import com.intellij.refactoring.move.MoveHandlerDelegate;
33 import com.intellij.refactoring.util.CommonRefactoringUtil;
34 import com.intellij.util.IncorrectOperationException;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.jetbrains.python.PyBundle;
37 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
38 import com.jetbrains.python.psi.*;
39 import com.jetbrains.python.psi.impl.PyPsiUtils;
40 import com.jetbrains.python.psi.search.PyOverridingMethodsSearch;
41 import com.jetbrains.python.psi.search.PySuperMethodsSearch;
42 import com.jetbrains.python.psi.types.TypeEvalContext;
43 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeFunctionTopLevelDialog;
44 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeLocalFunctionTopLevelProcessor;
45 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeMethodTopLevelProcessor;
46 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersDialog;
47 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersHelper;
48 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersProcessor;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.util.List;
53
54 import static com.jetbrains.python.psi.PyUtil.as;
55
56 /**
57  * @author vlan
58  */
59 public class PyMoveSymbolDelegate extends MoveHandlerDelegate {
60   @Override
61   public boolean canMove(PsiElement[] elements, @Nullable PsiElement targetContainer) {
62     if (!super.canMove(elements, targetContainer)) {
63       return false;
64     }
65     // Local function or method
66     if (findTargetFunction(elements[0]) != null) {
67       return true;
68     }
69     
70     // Top-level module member
71     for (PsiElement element : elements) {
72       if (!PyMoveModuleMembersHelper.isMovableModuleMember(element)) {
73         return false;
74       }
75     }
76     return true;
77   }
78
79   @Override
80   public void doMove(Project project, PsiElement[] elements, @Nullable PsiElement targetContainer, @Nullable MoveCallback callback) {
81     String initialPath = null;
82     if (targetContainer instanceof PsiFile) {
83       initialPath = StringUtil.notNullize(PyPsiUtils.getContainingFilePath(targetContainer));
84     }
85     if (initialPath == null) {
86       initialPath = StringUtil.notNullize(PyPsiUtils.getContainingFilePath(elements[0]));
87     }
88     
89     final BaseRefactoringProcessor processor;
90     final PyFunction function = findTargetFunction(elements[0]);
91     if (function != null) {
92       final PyMakeFunctionTopLevelDialog dialog = new PyMakeFunctionTopLevelDialog(project, function, initialPath, initialPath);
93       if (!dialog.showAndGet()) {
94         return;
95       }
96       if (function.getContainingClass() != null) {
97         processor = new PyMakeMethodTopLevelProcessor(function, dialog.getTargetPath());
98       }
99       else {
100         processor = new PyMakeLocalFunctionTopLevelProcessor(function, dialog.getTargetPath());
101       }
102       processor.setPreviewUsages(dialog.isPreviewUsages());
103     }
104     else {
105       final List<PsiNamedElement> initialElements = Lists.newArrayList();
106       for (PsiElement element : elements) {
107         final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
108         if (e == null) {
109           return;
110         }
111         initialElements.add(e);
112       }
113       final PyMoveModuleMembersDialog dialog = new PyMoveModuleMembersDialog(project, initialElements, initialPath, initialPath);
114       if (!dialog.showAndGet()) {
115         return;
116       }
117       final PsiNamedElement[] selectedElements = ContainerUtil.findAllAsArray(dialog.getSelectedTopLevelSymbols(), PsiNamedElement.class);
118       processor = new PyMoveModuleMembersProcessor(selectedElements, dialog.getTargetPath());
119       processor.setPreviewUsages(dialog.isPreviewUsages());
120     }
121     
122     try {
123       processor.run();
124     }
125     catch (IncorrectOperationException e) {
126       if (ApplicationManager.getApplication().isUnitTestMode()) {
127         throw e;
128       }
129       CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, project);
130     }
131   }
132
133   @Override
134   public boolean tryToMove(@NotNull PsiElement element,
135                            @NotNull Project project,
136                            @Nullable DataContext dataContext,
137                            @Nullable PsiReference reference,
138                            @Nullable Editor editor) {
139     PsiFile targetContainer = null;
140     if (editor != null) {
141       final Document document = editor.getDocument();
142       targetContainer = PsiDocumentManager.getInstance(project).getPsiFile(document);
143       if (targetContainer instanceof PyFile && selectionSpansMultipleLines(editor)) {
144         final List<PyElement> moduleMembers = collectAllMovableElementsInSelection(editor, (PyFile)targetContainer);
145         if (moduleMembers.isEmpty()) {
146           showBadSelectionErrorHint(project, editor);
147         }
148         else {
149           doMove(project, ContainerUtil.findAllAsArray(moduleMembers, PsiNamedElement.class), targetContainer, null);
150         }
151         return true;
152       }
153     }
154
155     // Fallback to the old way to select single element to move
156     final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
157     if (e != null && PyMoveModuleMembersHelper.hasMovableElementType(e)) {
158       if (PyMoveModuleMembersHelper.isMovableModuleMember(e) || findTargetFunction(e) != null) {
159         doMove(project, new PsiElement[]{e}, targetContainer, null);
160       }
161       else {
162         showBadSelectionErrorHint(project, editor);
163       }
164       return true;
165     }
166     return false;
167   }
168
169   private static void showBadSelectionErrorHint(@NotNull Project project, @Nullable Editor editor) {
170     CommonRefactoringUtil.showErrorHint(project, editor,
171                                         PyBundle.message("refactoring.move.module.members.error.selection"),
172                                         RefactoringBundle.message("error.title"), null);
173   }
174
175   private static boolean selectionSpansMultipleLines(@NotNull Editor editor) {
176     final SelectionModel selectionModel = editor.getSelectionModel();
177     final Document document = editor.getDocument();
178     return document.getLineNumber(selectionModel.getSelectionStart()) != document.getLineNumber(selectionModel.getSelectionEnd());
179   }
180
181   @NotNull
182   private static List<PyElement> collectAllMovableElementsInSelection(@NotNull Editor editor, @NotNull PyFile pyFile) {
183     final SelectionModel selectionModel = editor.getSelectionModel();
184     final TextRange selectionRange = new TextRange(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
185     final List<PyElement> members = PyMoveModuleMembersHelper.getTopLevelModuleMembers(pyFile);
186     return ContainerUtil.filter(members, member -> {
187       final PsiElement body = PyMoveModuleMembersHelper.expandNamedElementBody(((PsiNamedElement)member));
188       return body != null && selectionRange.contains(body.getTextRange());
189     });
190   }
191
192   @Nullable
193   public static PyFunction findTargetFunction(@NotNull PsiElement element) {
194     if (isLocalFunction(element) || isSuitableInstanceMethod(element)) {
195       return (PyFunction)element;
196     }
197     // e.g. caret is on "def" keyword
198     if (isLocalFunction(element.getParent()) || isSuitableInstanceMethod(element.getParent())) {
199       return (PyFunction)element.getParent();
200     }
201     final PyReferenceExpression refExpr = PsiTreeUtil.getParentOfType(element, PyReferenceExpression.class);
202     if (refExpr == null) {
203       return null;
204     }
205     final PsiElement resolved = refExpr.getReference().resolve();
206     if (isLocalFunction(resolved) || isSuitableInstanceMethod(resolved)) {
207       return (PyFunction)resolved;
208     }
209     return null;
210   }
211
212   public static boolean isSuitableInstanceMethod(@Nullable PsiElement element) {
213     final PyFunction function = as(element, PyFunction.class);
214     if (function == null || function.getContainingClass() == null) {
215       return false;
216     }
217     final String funcName = function.getName();
218     if (funcName == null || PyUtil.isSpecialName(funcName)) {
219       return false;
220     }
221     final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(function.getProject(), function.getContainingFile());
222     if (PySuperMethodsSearch.search(function, typeEvalContext).findFirst() != null) return false;
223     if (PyOverridingMethodsSearch.search(function, true).findFirst() != null) return false;
224     if (function.getDecoratorList() != null || function.getModifier() != null) return false;
225     if (function.getContainingClass().findPropertyByCallable(function) != null) return false;
226     return true;
227   }
228
229   private static boolean isLocalFunction(@Nullable PsiElement resolved) {
230     return resolved instanceof PyFunction && PsiTreeUtil.getParentOfType(resolved, ScopeOwner.class, true) instanceof PyFunction;
231   }
232 }