PY-21147 Don't override MoveHandlerDelegate#doMove() method in PyMoveSymbolDelegate
[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.annotations.VisibleForTesting;
19 import com.google.common.collect.Lists;
20 import com.intellij.openapi.actionSystem.DataContext;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.editor.Document;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.editor.SelectionModel;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.TextRange;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.psi.*;
29 import com.intellij.psi.util.PsiTreeUtil;
30 import com.intellij.refactoring.BaseRefactoringProcessor;
31 import com.intellij.refactoring.RefactoringBundle;
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.Collections;
53 import java.util.List;
54
55 import static com.jetbrains.python.psi.PyUtil.as;
56
57 /**
58  * @author vlan
59  */
60 public class PyMoveSymbolDelegate extends MoveHandlerDelegate {
61   @Override
62   public boolean canMove(PsiElement[] elements, @Nullable PsiElement targetContainer) {
63     if (!super.canMove(elements, targetContainer)) {
64       return false;
65     }
66     // Local function or method
67     if (isMovableLocalFunctionOrMethod(elements[0])) {
68       return true;
69     }
70     
71     // Top-level module member
72     for (PsiElement element : elements) {
73       if (!PyMoveModuleMembersHelper.isMovableModuleMember(element)) {
74         return false;
75       }
76     }
77     return true;
78   }
79
80   public void doMove(@NotNull Project project, @NotNull List<PyElement> elements) {
81     final PsiElement firstElement = elements.get(0);
82     final String initialPath = StringUtil.notNullize(PyPsiUtils.getContainingFilePath(firstElement));
83
84     final BaseRefactoringProcessor processor;
85     if (isMovableLocalFunctionOrMethod(firstElement)) {
86       final PyFunction function = (PyFunction)firstElement;
87       final PyMakeFunctionTopLevelDialog dialog = new PyMakeFunctionTopLevelDialog(project, function, initialPath, initialPath);
88       if (!dialog.showAndGet()) {
89         return;
90       }
91       if (function.getContainingClass() != null) {
92         processor = new PyMakeMethodTopLevelProcessor(function, dialog.getTargetPath());
93       }
94       else {
95         processor = new PyMakeLocalFunctionTopLevelProcessor(function, dialog.getTargetPath());
96       }
97       processor.setPreviewUsages(dialog.isPreviewUsages());
98     }
99     else {
100       final List<PsiNamedElement> initialElements = Lists.newArrayList();
101       for (PsiElement element : elements) {
102         final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
103         if (e == null) {
104           return;
105         }
106         initialElements.add(e);
107       }
108       final PyMoveModuleMembersDialog dialog = new PyMoveModuleMembersDialog(project, initialElements, initialPath, initialPath);
109       if (!dialog.showAndGet()) {
110         return;
111       }
112       final PsiNamedElement[] selectedElements = ContainerUtil.findAllAsArray(dialog.getSelectedTopLevelSymbols(), PsiNamedElement.class);
113       processor = new PyMoveModuleMembersProcessor(selectedElements, dialog.getTargetPath());
114       processor.setPreviewUsages(dialog.isPreviewUsages());
115     }
116     
117     try {
118       processor.run();
119     }
120     catch (IncorrectOperationException e) {
121       if (ApplicationManager.getApplication().isUnitTestMode()) {
122         throw e;
123       }
124       CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, project);
125     }
126   }
127
128   @Override
129   public boolean tryToMove(@NotNull PsiElement element,
130                            @NotNull Project project,
131                            @Nullable DataContext dataContext,
132                            @Nullable PsiReference reference,
133                            @Nullable Editor editor) {
134     final PsiFile currentFile = element.getContainingFile();
135     if (editor != null && currentFile instanceof PyFile && selectionSpansMultipleLines(editor)) {
136       final List<PyElement> moduleMembers = collectAllMovableElementsInSelection(editor, (PyFile)currentFile);
137       if (moduleMembers.isEmpty()) {
138         showBadSelectionErrorHint(project, editor);
139       }
140       else {
141         doMove(project, moduleMembers);
142       }
143       return true;
144     }
145
146     // Fallback to the old way to select single element to move
147     final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
148     if (e != null && PyMoveModuleMembersHelper.hasMovableElementType(e)) {
149       if (PyMoveModuleMembersHelper.isMovableModuleMember(e) || isMovableLocalFunctionOrMethod(e)) {
150         doMove(project, Collections.singletonList((PyElement)e));
151       }
152       else {
153         showBadSelectionErrorHint(project, editor);
154       }
155       return true;
156     }
157     return false;
158   }
159
160   private static void showBadSelectionErrorHint(@NotNull Project project, @Nullable Editor editor) {
161     CommonRefactoringUtil.showErrorHint(project, editor,
162                                         PyBundle.message("refactoring.move.module.members.error.selection"),
163                                         RefactoringBundle.message("error.title"), null);
164   }
165
166   private static boolean selectionSpansMultipleLines(@NotNull Editor editor) {
167     final SelectionModel selectionModel = editor.getSelectionModel();
168     final Document document = editor.getDocument();
169     return document.getLineNumber(selectionModel.getSelectionStart()) != document.getLineNumber(selectionModel.getSelectionEnd());
170   }
171
172   @NotNull
173   private static List<PyElement> collectAllMovableElementsInSelection(@NotNull Editor editor, @NotNull PyFile pyFile) {
174     final SelectionModel selectionModel = editor.getSelectionModel();
175     final TextRange selectionRange = new TextRange(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
176     final List<PyElement> members = PyMoveModuleMembersHelper.getTopLevelModuleMembers(pyFile);
177     return ContainerUtil.filter(members, member -> {
178       final PsiElement body = PyMoveModuleMembersHelper.expandNamedElementBody((PsiNamedElement)member);
179       return body != null && selectionRange.contains(body.getTextRange());
180     });
181   }
182
183   @VisibleForTesting
184   public static boolean isMovableLocalFunctionOrMethod(@NotNull PsiElement element) {
185     return isLocalFunction(element) || isSuitableInstanceMethod(element);
186   }
187
188   private static boolean isSuitableInstanceMethod(@Nullable PsiElement element) {
189     final PyFunction function = as(element, PyFunction.class);
190     if (function == null || function.getContainingClass() == null) {
191       return false;
192     }
193     final String funcName = function.getName();
194     if (funcName == null || PyUtil.isSpecialName(funcName)) {
195       return false;
196     }
197     final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(function.getProject(), function.getContainingFile());
198     if (PySuperMethodsSearch.search(function, typeEvalContext).findFirst() != null) return false;
199     if (PyOverridingMethodsSearch.search(function, true).findFirst() != null) return false;
200     if (function.getDecoratorList() != null || function.getModifier() != null) return false;
201     if (function.getContainingClass().findPropertyByCallable(function) != null) return false;
202     return true;
203   }
204
205   private static boolean isLocalFunction(@Nullable PsiElement resolved) {
206     return resolved instanceof PyFunction && PsiTreeUtil.getParentOfType(resolved, ScopeOwner.class, true) instanceof PyFunction;
207   }
208 }