2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.jetbrains.python.refactoring.move;
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;
53 import java.util.List;
55 import static com.jetbrains.python.psi.PyUtil.as;
60 public class PyMoveSymbolDelegate extends MoveHandlerDelegate {
62 public boolean canMove(PsiElement[] elements, @Nullable PsiElement targetContainer) {
63 if (!super.canMove(elements, targetContainer)) {
66 // Local function or method
67 if (isMovableLocalFunctionOrMethod(elements[0])) {
71 // Top-level module member
72 for (PsiElement element : elements) {
73 if (!PyMoveModuleMembersHelper.isMovableModuleMember(element)) {
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()) {
90 if (function.getContainingClass() != null) {
91 processor = new PyMakeMethodTopLevelProcessor(function, dialog.getTargetPath());
94 processor = new PyMakeLocalFunctionTopLevelProcessor(function, dialog.getTargetPath());
96 processor.setPreviewUsages(dialog.isPreviewUsages());
99 final List<PsiNamedElement> initialElements = Lists.newArrayList();
100 for (PsiElement element : elements) {
101 final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
105 initialElements.add(e);
107 final PyMoveModuleMembersDialog dialog = new PyMoveModuleMembersDialog(project, initialElements, initialPath, initialPath);
108 if (!dialog.showAndGet()) {
111 final PsiNamedElement[] selectedElements = ContainerUtil.findAllAsArray(dialog.getSelectedTopLevelSymbols(), PsiNamedElement.class);
112 processor = new PyMoveModuleMembersProcessor(selectedElements, dialog.getTargetPath());
113 processor.setPreviewUsages(dialog.isPreviewUsages());
119 catch (IncorrectOperationException e) {
120 if (ApplicationManager.getApplication().isUnitTestMode()) {
123 CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, project);
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);
140 doMove(project, ContainerUtil.findAllAsArray(moduleMembers, PsiNamedElement.class), null, null);
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);
152 showBadSelectionErrorHint(project, editor);
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);
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());
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());
183 public static boolean isMovableLocalFunctionOrMethod(@NotNull PsiElement element) {
184 return isLocalFunction(element) || isSuitableInstanceMethod(element);
187 private static boolean isSuitableInstanceMethod(@Nullable PsiElement element) {
188 final PyFunction function = as(element, PyFunction.class);
189 if (function == null || function.getContainingClass() == null) {
192 final String funcName = function.getName();
193 if (funcName == null || PyUtil.isSpecialName(funcName)) {
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;
204 private static boolean isLocalFunction(@Nullable PsiElement resolved) {
205 return resolved instanceof PyFunction && PsiTreeUtil.getParentOfType(resolved, ScopeOwner.class, true) instanceof PyFunction;