e49d4a4d6f278e4d388951884c7f2438ad80303a
[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.MoveCallback;
33 import com.intellij.refactoring.move.MoveHandlerDelegate;
34 import com.intellij.refactoring.util.CommonRefactoringUtil;
35 import com.intellij.util.IncorrectOperationException;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.jetbrains.python.PyBundle;
38 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
39 import com.jetbrains.python.psi.*;
40 import com.jetbrains.python.psi.impl.PyPsiUtils;
41 import com.jetbrains.python.psi.search.PyOverridingMethodsSearch;
42 import com.jetbrains.python.psi.search.PySuperMethodsSearch;
43 import com.jetbrains.python.psi.types.TypeEvalContext;
44 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeFunctionTopLevelDialog;
45 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeLocalFunctionTopLevelProcessor;
46 import com.jetbrains.python.refactoring.move.makeFunctionTopLevel.PyMakeMethodTopLevelProcessor;
47 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersDialog;
48 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersHelper;
49 import com.jetbrains.python.refactoring.move.moduleMembers.PyMoveModuleMembersProcessor;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
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   @Override
81   public void doMove(Project project, PsiElement[] elements, @Nullable PsiElement targetContainer, @Nullable MoveCallback callback) {
82     final String initialPath = StringUtil.notNullize(PyPsiUtils.getContainingFilePath(elements[0]));
83     final BaseRefactoringProcessor processor;
84     if (isMovableLocalFunctionOrMethod(elements[0])) {
85       final PyFunction function = (PyFunction)elements[0];
86       final PyMakeFunctionTopLevelDialog dialog = new PyMakeFunctionTopLevelDialog(project, function, initialPath, initialPath);
87       if (!dialog.showAndGet()) {
88         return;
89       }
90       if (function.getContainingClass() != null) {
91         processor = new PyMakeMethodTopLevelProcessor(function, dialog.getTargetPath());
92       }
93       else {
94         processor = new PyMakeLocalFunctionTopLevelProcessor(function, dialog.getTargetPath());
95       }
96       processor.setPreviewUsages(dialog.isPreviewUsages());
97     }
98     else {
99       final List<PsiNamedElement> initialElements = Lists.newArrayList();
100       for (PsiElement element : elements) {
101         final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
102         if (e == null) {
103           return;
104         }
105         initialElements.add(e);
106       }
107       final PyMoveModuleMembersDialog dialog = new PyMoveModuleMembersDialog(project, initialElements, initialPath, initialPath);
108       if (!dialog.showAndGet()) {
109         return;
110       }
111       final PsiNamedElement[] selectedElements = ContainerUtil.findAllAsArray(dialog.getSelectedTopLevelSymbols(), PsiNamedElement.class);
112       processor = new PyMoveModuleMembersProcessor(selectedElements, dialog.getTargetPath());
113       processor.setPreviewUsages(dialog.isPreviewUsages());
114     }
115     
116     try {
117       processor.run();
118     }
119     catch (IncorrectOperationException e) {
120       if (ApplicationManager.getApplication().isUnitTestMode()) {
121         throw e;
122       }
123       CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, project);
124     }
125   }
126
127   @Override
128   public boolean tryToMove(@NotNull PsiElement element,
129                            @NotNull Project project,
130                            @Nullable DataContext dataContext,
131                            @Nullable PsiReference reference,
132                            @Nullable Editor editor) {
133     final PsiFile currentFile = element.getContainingFile();
134     if (editor != null && currentFile instanceof PyFile && selectionSpansMultipleLines(editor)) {
135       final List<PyElement> moduleMembers = collectAllMovableElementsInSelection(editor, (PyFile)currentFile);
136       if (moduleMembers.isEmpty()) {
137         showBadSelectionErrorHint(project, editor);
138       }
139       else {
140         doMove(project, ContainerUtil.findAllAsArray(moduleMembers, PsiNamedElement.class), null, null);
141       }
142       return true;
143     }
144
145     // Fallback to the old way to select single element to move
146     final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
147     if (e != null && PyMoveModuleMembersHelper.hasMovableElementType(e)) {
148       if (PyMoveModuleMembersHelper.isMovableModuleMember(e) || isMovableLocalFunctionOrMethod(e)) {
149         doMove(project, new PsiElement[]{e}, null, null);
150       }
151       else {
152         showBadSelectionErrorHint(project, editor);
153       }
154       return true;
155     }
156     return false;
157   }
158
159   private static void showBadSelectionErrorHint(@NotNull Project project, @Nullable Editor editor) {
160     CommonRefactoringUtil.showErrorHint(project, editor,
161                                         PyBundle.message("refactoring.move.module.members.error.selection"),
162                                         RefactoringBundle.message("error.title"), null);
163   }
164
165   private static boolean selectionSpansMultipleLines(@NotNull Editor editor) {
166     final SelectionModel selectionModel = editor.getSelectionModel();
167     final Document document = editor.getDocument();
168     return document.getLineNumber(selectionModel.getSelectionStart()) != document.getLineNumber(selectionModel.getSelectionEnd());
169   }
170
171   @NotNull
172   private static List<PyElement> collectAllMovableElementsInSelection(@NotNull Editor editor, @NotNull PyFile pyFile) {
173     final SelectionModel selectionModel = editor.getSelectionModel();
174     final TextRange selectionRange = new TextRange(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
175     final List<PyElement> members = PyMoveModuleMembersHelper.getTopLevelModuleMembers(pyFile);
176     return ContainerUtil.filter(members, member -> {
177       final PsiElement body = PyMoveModuleMembersHelper.expandNamedElementBody(((PsiNamedElement)member));
178       return body != null && selectionRange.contains(body.getTextRange());
179     });
180   }
181
182   @VisibleForTesting
183   public static boolean isMovableLocalFunctionOrMethod(@NotNull PsiElement element) {
184     return isLocalFunction(element) || isSuitableInstanceMethod(element);
185   }
186
187   private static boolean isSuitableInstanceMethod(@Nullable PsiElement element) {
188     final PyFunction function = as(element, PyFunction.class);
189     if (function == null || function.getContainingClass() == null) {
190       return false;
191     }
192     final String funcName = function.getName();
193     if (funcName == null || PyUtil.isSpecialName(funcName)) {
194       return false;
195     }
196     final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(function.getProject(), function.getContainingFile());
197     if (PySuperMethodsSearch.search(function, typeEvalContext).findFirst() != null) return false;
198     if (PyOverridingMethodsSearch.search(function, true).findFirst() != null) return false;
199     if (function.getDecoratorList() != null || function.getModifier() != null) return false;
200     if (function.getContainingClass().findPropertyByCallable(function) != null) return false;
201     return true;
202   }
203
204   private static boolean isLocalFunction(@Nullable PsiElement resolved) {
205     return resolved instanceof PyFunction && PsiTreeUtil.getParentOfType(resolved, ScopeOwner.class, true) instanceof PyFunction;
206   }
207 }