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.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;
52 import java.util.List;
54 import static com.jetbrains.python.psi.PyUtil.as;
59 public class PyMoveSymbolDelegate extends MoveHandlerDelegate {
61 public boolean canMove(PsiElement[] elements, @Nullable PsiElement targetContainer) {
62 if (!super.canMove(elements, targetContainer)) {
65 // Local function or method
66 if (findTargetFunction(elements[0]) != null) {
70 // Top-level module member
71 for (PsiElement element : elements) {
72 if (!PyMoveModuleMembersHelper.isMovableModuleMember(element)) {
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));
85 if (initialPath == null) {
86 initialPath = StringUtil.notNullize(PyPsiUtils.getContainingFilePath(elements[0]));
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()) {
96 if (function.getContainingClass() != null) {
97 processor = new PyMakeMethodTopLevelProcessor(function, dialog.getTargetPath());
100 processor = new PyMakeLocalFunctionTopLevelProcessor(function, dialog.getTargetPath());
102 processor.setPreviewUsages(dialog.isPreviewUsages());
105 final List<PsiNamedElement> initialElements = Lists.newArrayList();
106 for (PsiElement element : elements) {
107 final PsiNamedElement e = PyMoveModuleMembersHelper.extractNamedElement(element);
111 initialElements.add(e);
113 final PyMoveModuleMembersDialog dialog = new PyMoveModuleMembersDialog(project, initialElements, initialPath, initialPath);
114 if (!dialog.showAndGet()) {
117 final PsiNamedElement[] selectedElements = ContainerUtil.findAllAsArray(dialog.getSelectedTopLevelSymbols(), PsiNamedElement.class);
118 processor = new PyMoveModuleMembersProcessor(selectedElements, dialog.getTargetPath());
119 processor.setPreviewUsages(dialog.isPreviewUsages());
125 catch (IncorrectOperationException e) {
126 if (ApplicationManager.getApplication().isUnitTestMode()) {
129 CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, project);
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);
149 doMove(project, ContainerUtil.findAllAsArray(moduleMembers, PsiNamedElement.class), targetContainer, null);
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);
162 showBadSelectionErrorHint(project, editor);
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);
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());
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());
193 public static PyFunction findTargetFunction(@NotNull PsiElement element) {
194 if (isLocalFunction(element) || isSuitableInstanceMethod(element)) {
195 return (PyFunction)element;
197 // e.g. caret is on "def" keyword
198 if (isLocalFunction(element.getParent()) || isSuitableInstanceMethod(element.getParent())) {
199 return (PyFunction)element.getParent();
201 final PyReferenceExpression refExpr = PsiTreeUtil.getParentOfType(element, PyReferenceExpression.class);
202 if (refExpr == null) {
205 final PsiElement resolved = refExpr.getReference().resolve();
206 if (isLocalFunction(resolved) || isSuitableInstanceMethod(resolved)) {
207 return (PyFunction)resolved;
212 public static boolean isSuitableInstanceMethod(@Nullable PsiElement element) {
213 final PyFunction function = as(element, PyFunction.class);
214 if (function == null || function.getContainingClass() == null) {
217 final String funcName = function.getName();
218 if (funcName == null || PyUtil.isSpecialName(funcName)) {
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;
229 private static boolean isLocalFunction(@Nullable PsiElement resolved) {
230 return resolved instanceof PyFunction && PsiTreeUtil.getParentOfType(resolved, ScopeOwner.class, true) instanceof PyFunction;