blob: 00448c8b69bdb2cdc874d4fe3d7dc85fb441c891 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.refactoring.introduce;
import com.intellij.codeInsight.CodeInsightUtilCore;
import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
import com.intellij.codeInsight.template.impl.TemplateState;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Pass;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.TokenType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.IntroduceTargetChooser;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer;
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.Function;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonStringUtil;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.types.PyNoneType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.refactoring.NameSuggesterUtil;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.jetbrains.python.inspections.PyStringFormatParser.*;
/**
* @author Alexey.Ivanov
* @author vlan
*/
abstract public class IntroduceHandler implements RefactoringActionHandler {
protected static PsiElement findAnchor(List<PsiElement> occurrences) {
PsiElement anchor = occurrences.get(0);
final Pair<PsiElement, TextRange> data = anchor.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
// Search anchor in the origin file, not in dummy.py, if selection breaks statement and thus element was generated
if (data != null && occurrences.size() == 1) {
return PsiTreeUtil.getParentOfType(data.getFirst(), PyStatement.class);
}
next:
do {
final PyStatement statement = PsiTreeUtil.getParentOfType(anchor, PyStatement.class);
if (statement != null) {
final PsiElement parent = statement.getParent();
for (PsiElement element : occurrences) {
if (!PsiTreeUtil.isAncestor(parent, element, true)) {
anchor = statement;
continue next;
}
}
}
return statement;
}
while (true);
}
protected static void ensureName(IntroduceOperation operation) {
if (operation.getName() == null) {
final Collection<String> suggestedNames = operation.getSuggestedNames();
if (suggestedNames.size() > 0) {
operation.setName(suggestedNames.iterator().next());
}
else {
operation.setName("x");
}
}
}
@Nullable
protected static PsiElement findOccurrenceUnderCaret(List<PsiElement> occurrences, Editor editor) {
if (occurrences.isEmpty()) {
return null;
}
int offset = editor.getCaretModel().getOffset();
for (PsiElement occurrence : occurrences) {
if (occurrence != null && occurrence.getTextRange().contains(offset)) {
return occurrence;
}
}
int line = editor.getDocument().getLineNumber(offset);
for (PsiElement occurrence : occurrences) {
if (occurrence.isValid() && editor.getDocument().getLineNumber(occurrence.getTextRange().getStartOffset()) == line) {
return occurrence;
}
}
for (PsiElement occurrence : occurrences) {
if (occurrence.isValid()) {
return occurrence;
}
}
return null;
}
public enum InitPlace {
SAME_METHOD,
CONSTRUCTOR,
SET_UP
}
@Nullable
protected PsiElement replaceExpression(PsiElement expression, PyExpression newExpression, IntroduceOperation operation) {
PyExpressionStatement statement = PsiTreeUtil.getParentOfType(expression, PyExpressionStatement.class);
if (statement != null) {
if (statement.getExpression() == expression && expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE) == null) {
statement.delete();
return null;
}
}
return PyReplaceExpressionUtil.replaceExpression(expression, newExpression);
}
private final IntroduceValidator myValidator;
protected final String myDialogTitle;
protected IntroduceHandler(@NotNull final IntroduceValidator validator, @NotNull final String dialogTitle) {
myValidator = validator;
myDialogTitle = dialogTitle;
}
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
performAction(new IntroduceOperation(project, editor, file, null));
}
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
}
public Collection<String> getSuggestedNames(@NotNull final PyExpression expression) {
Collection<String> candidates = generateSuggestedNames(expression);
Collection<String> res = new ArrayList<String>();
for (String name : candidates) {
if (myValidator.checkPossibleName(name, expression)) {
res.add(name);
}
}
if (res.isEmpty()) { // no available names found, generate disambiguated suggestions
for (String name : candidates) {
int index = 1;
while (!myValidator.checkPossibleName(name + index, expression)) {
index++;
}
res.add(name + index);
}
}
return res;
}
protected Collection<String> generateSuggestedNames(PyExpression expression) {
Collection<String> candidates = new LinkedHashSet<String>() {
@Override
public boolean add(String s) {
if (PyNames.isReserved(s)) {
return false;
}
return super.add(s);
}
};
String text = expression.getText();
final Pair<PsiElement, TextRange> selection = expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (selection != null) {
text = selection.getSecond().substring(selection.getFirst().getText());
}
if (expression instanceof PyCallExpression) {
final PyExpression callee = ((PyCallExpression)expression).getCallee();
if (callee != null) {
text = callee.getText();
}
}
if (text != null) {
candidates.addAll(NameSuggesterUtil.generateNames(text));
}
final TypeEvalContext context = TypeEvalContext.userInitiated(expression.getContainingFile());
PyType type = context.getType(expression);
if (type != null && type != PyNoneType.INSTANCE) {
String typeName = type.getName();
if (typeName != null) {
if (type.isBuiltin()) {
typeName = typeName.substring(0, 1);
}
candidates.addAll(NameSuggesterUtil.generateNamesByType(typeName));
}
}
final PyKeywordArgument kwArg = PsiTreeUtil.getParentOfType(expression, PyKeywordArgument.class);
if (kwArg != null && kwArg.getValueExpression() == expression) {
candidates.add(kwArg.getKeyword());
}
final PyArgumentList argList = PsiTreeUtil.getParentOfType(expression, PyArgumentList.class);
if (argList != null) {
final CallArgumentsMapping result = argList.analyzeCall(PyResolveContext.noImplicits());
if (result.getMarkedCallee() != null) {
final PyNamedParameter namedParameter = result.getPlainMappedParams().get(expression);
if (namedParameter != null) {
candidates.add(namedParameter.getName());
}
}
}
return candidates;
}
public void performAction(IntroduceOperation operation) {
final PsiFile file = operation.getFile();
if (!CommonRefactoringUtil.checkReadOnlyStatus(file)) {
return;
}
final Editor editor = operation.getEditor();
if (editor.getSettings().isVariableInplaceRenameEnabled()) {
final TemplateState templateState = TemplateManagerImpl.getTemplateState(operation.getEditor());
if (templateState != null && !templateState.isFinished()) {
return;
}
}
PsiElement element1 = null;
PsiElement element2 = null;
final SelectionModel selectionModel = editor.getSelectionModel();
boolean singleElementSelection = false;
if (selectionModel.hasSelection()) {
element1 = file.findElementAt(selectionModel.getSelectionStart());
element2 = file.findElementAt(selectionModel.getSelectionEnd() - 1);
if (element1 instanceof PsiWhiteSpace) {
int startOffset = element1.getTextRange().getEndOffset();
element1 = file.findElementAt(startOffset);
}
if (element2 instanceof PsiWhiteSpace) {
int endOffset = element2.getTextRange().getStartOffset();
element2 = file.findElementAt(endOffset - 1);
}
if (element1 == element2) {
singleElementSelection = true;
}
}
else {
if (smartIntroduce(operation)) {
return;
}
final CaretModel caretModel = editor.getCaretModel();
final Document document = editor.getDocument();
int lineNumber = document.getLineNumber(caretModel.getOffset());
if ((lineNumber >= 0) && (lineNumber < document.getLineCount())) {
element1 = file.findElementAt(document.getLineStartOffset(lineNumber));
element2 = file.findElementAt(document.getLineEndOffset(lineNumber) - 1);
}
}
final Project project = operation.getProject();
if (element1 == null || element2 == null) {
showCannotPerformError(project, editor);
return;
}
element1 = PyRefactoringUtil.getSelectedExpression(project, file, element1, element2);
final PyComprehensionElement comprehension = PsiTreeUtil.getParentOfType(element1, PyComprehensionElement.class, true);
if (element1 == null || comprehension != null) {
showCannotPerformError(project, editor);
return;
}
if (singleElementSelection && element1 instanceof PyStringLiteralExpression) {
final PyStringLiteralExpression literal = (PyStringLiteralExpression)element1;
// Currently introduce for substrings of a multi-part string literals is not supported
if (literal.getStringNodes().size() > 1) {
showCannotPerformError(project, editor);
return;
}
final int offset = element1.getTextOffset();
final TextRange selectionRange = TextRange.create(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
final TextRange elementRange = element1.getTextRange();
if (!elementRange.equals(selectionRange) && elementRange.contains(selectionRange)) {
final TextRange innerRange = literal.getStringValueTextRange();
final TextRange intersection = selectionRange.shiftRight(-offset).intersection(innerRange);
final TextRange finalRange = intersection != null ? intersection : selectionRange;
final String text = literal.getText();
if (getFormatValueExpression(literal) != null && breaksStringFormatting(text, finalRange) ||
getNewStyleFormatValueExpression(literal) != null && breaksNewStyleStringFormatting(text, finalRange) ||
breaksStringEscaping(text, finalRange)) {
showCannotPerformError(project, editor);
return;
}
element1.putUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE, Pair.create(element1, finalRange));
}
}
if (!checkIntroduceContext(file, editor, element1)) {
return;
}
operation.setElement(element1);
performActionOnElement(operation);
}
private static boolean breaksStringFormatting(@NotNull String s, @NotNull TextRange range) {
return breaksRanges(substitutionsToRanges(filterSubstitutions(parsePercentFormat(s))), range);
}
private static boolean breaksNewStyleStringFormatting(@NotNull String s, @NotNull TextRange range) {
return breaksRanges(substitutionsToRanges(filterSubstitutions(parseNewStyleFormat(s))), range);
}
private static boolean breaksStringEscaping(@NotNull String s, @NotNull TextRange range) {
return breaksRanges(getEscapeRanges(s), range);
}
private static boolean breaksRanges(@NotNull List<TextRange> ranges, @NotNull TextRange range) {
for (TextRange r : ranges) {
if (range.contains(r)) {
continue;
}
if (range.intersectsStrict(r)) {
return true;
}
}
return false;
}
private void showCannotPerformError(Project project, Editor editor) {
CommonRefactoringUtil.showErrorHint(project, editor, PyBundle.message("refactoring.introduce.selection.error"), myDialogTitle,
"refactoring.extractMethod");
}
private boolean smartIntroduce(final IntroduceOperation operation) {
final Editor editor = operation.getEditor();
final PsiFile file = operation.getFile();
int offset = editor.getCaretModel().getOffset();
PsiElement elementAtCaret = file.findElementAt(offset);
if (!checkIntroduceContext(file, editor, elementAtCaret)) return true;
final List<PyExpression> expressions = new ArrayList<PyExpression>();
while (elementAtCaret != null) {
if (elementAtCaret instanceof PyStatement || elementAtCaret instanceof PyFile) {
break;
}
if (elementAtCaret instanceof PyExpression && isValidIntroduceVariant(elementAtCaret)) {
expressions.add((PyExpression)elementAtCaret);
}
elementAtCaret = elementAtCaret.getParent();
}
if (expressions.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) {
operation.setElement(expressions.get(0));
performActionOnElement(operation);
return true;
}
else if (expressions.size() > 1) {
IntroduceTargetChooser.showChooser(editor, expressions, new Pass<PyExpression>() {
@Override
public void pass(PyExpression pyExpression) {
operation.setElement(pyExpression);
performActionOnElement(operation);
}
}, new Function<PyExpression, String>() {
public String fun(PyExpression pyExpression) {
return pyExpression.getText();
}
});
return true;
}
return false;
}
protected boolean checkIntroduceContext(PsiFile file, Editor editor, PsiElement element) {
if (!isValidIntroduceContext(element)) {
CommonRefactoringUtil.showErrorHint(file.getProject(), editor, PyBundle.message("refactoring.introduce.selection.error"),
myDialogTitle, "refactoring.extractMethod");
return false;
}
return true;
}
protected boolean isValidIntroduceContext(PsiElement element) {
final PyDecorator decorator = PsiTreeUtil.getParentOfType(element, PyDecorator.class);
if (decorator != null && PsiTreeUtil.isAncestor(decorator.getCallee(), element, false)) {
return false;
}
return PsiTreeUtil.getParentOfType(element, PyParameterList.class) == null;
}
private static boolean isValidIntroduceVariant(PsiElement element) {
final PyCallExpression call = PsiTreeUtil.getParentOfType(element, PyCallExpression.class);
if (call != null && PsiTreeUtil.isAncestor(call.getCallee(), element, false)) {
return false;
}
final PyComprehensionElement comprehension = PsiTreeUtil.getParentOfType(element, PyComprehensionElement.class, true);
if (comprehension != null) {
return false;
}
return true;
}
private void performActionOnElement(IntroduceOperation operation) {
if (!checkEnabled(operation)) {
return;
}
final PsiElement element = operation.getElement();
final PsiElement parent = element.getParent();
final PyExpression initializer = parent instanceof PyAssignmentStatement ?
((PyAssignmentStatement)parent).getAssignedValue() :
(PyExpression)element;
operation.setInitializer(initializer);
if (initializer != null) {
operation.setOccurrences(getOccurrences(element, initializer));
operation.setSuggestedNames(getSuggestedNames(initializer));
}
if (operation.getOccurrences().size() == 0) {
operation.setReplaceAll(false);
}
performActionOnElementOccurrences(operation);
}
protected void performActionOnElementOccurrences(final IntroduceOperation operation) {
final Editor editor = operation.getEditor();
if (editor.getSettings().isVariableInplaceRenameEnabled()) {
ensureName(operation);
if (operation.isReplaceAll() != null) {
performInplaceIntroduce(operation);
}
else {
OccurrencesChooser.simpleChooser(editor).showChooser(operation.getElement(), operation.getOccurrences(), new Pass<OccurrencesChooser.ReplaceChoice>() {
@Override
public void pass(OccurrencesChooser.ReplaceChoice replaceChoice) {
operation.setReplaceAll(replaceChoice == OccurrencesChooser.ReplaceChoice.ALL);
performInplaceIntroduce(operation);
}
});
}
}
else {
performIntroduceWithDialog(operation);
}
}
protected void performInplaceIntroduce(IntroduceOperation operation) {
final PsiElement statement = performRefactoring(operation);
if (statement instanceof PyAssignmentStatement) {
PyTargetExpression target = (PyTargetExpression) ((PyAssignmentStatement)statement).getTargets() [0];
final List<PsiElement> occurrences = operation.getOccurrences();
final PsiElement occurrence = findOccurrenceUnderCaret(occurrences, operation.getEditor());
PsiElement elementForCaret = occurrence != null ? occurrence : target;
operation.getEditor().getCaretModel().moveToOffset(elementForCaret.getTextRange().getStartOffset());
final InplaceVariableIntroducer<PsiElement> introducer =
new PyInplaceVariableIntroducer(target, operation, occurrences);
introducer.performInplaceRefactoring(new LinkedHashSet<String>(operation.getSuggestedNames()));
}
}
protected void performIntroduceWithDialog(IntroduceOperation operation) {
final Project project = operation.getProject();
if (operation.getName() == null) {
PyIntroduceDialog dialog = new PyIntroduceDialog(project, myDialogTitle, myValidator, getHelpId(), operation);
dialog.show();
if (!dialog.isOK()) {
return;
}
operation.setName(dialog.getName());
operation.setReplaceAll(dialog.doReplaceAllOccurrences());
operation.setInitPlace(dialog.getInitPlace());
}
PsiElement declaration = performRefactoring(operation);
final Editor editor = operation.getEditor();
editor.getCaretModel().moveToOffset(declaration.getTextRange().getEndOffset());
editor.getSelectionModel().removeSelection();
}
protected PsiElement performRefactoring(IntroduceOperation operation) {
PsiElement declaration = createDeclaration(operation);
declaration = performReplace(declaration, operation);
declaration = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(declaration);
return declaration;
}
public PyAssignmentStatement createDeclaration(IntroduceOperation operation) {
final Project project = operation.getProject();
final PyExpression initializer = operation.getInitializer();
InitializerTextBuilder builder = new InitializerTextBuilder();
initializer.accept(builder);
String assignmentText = operation.getName() + " = " + builder.result();
PsiElement anchor = operation.isReplaceAll()
? findAnchor(operation.getOccurrences())
: PsiTreeUtil.getParentOfType(initializer, PyStatement.class);
return createDeclaration(project, assignmentText, anchor);
}
private static class InitializerTextBuilder extends PyRecursiveElementVisitor {
private final StringBuilder myResult = new StringBuilder();
@Override
public void visitWhiteSpace(PsiWhiteSpace space) {
myResult.append(space.getText().replace('\n', ' ').replace("\\", ""));
}
@Override
public void visitPyStringLiteralExpression(PyStringLiteralExpression node) {
final Pair<PsiElement, TextRange> data = node.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (data != null) {
final PsiElement parent = data.getFirst();
final String text = parent.getText();
final Pair<String, String> detectedQuotes = PythonStringUtil.getQuotes(text);
final Pair<String, String> quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'");
final TextRange range = data.getSecond();
final String substring = range.substring(text);
myResult.append(quotes.getFirst()).append(substring).append(quotes.getSecond());
}
else {
ASTNode child = node.getNode().getFirstChildNode();
while (child != null) {
String text = child.getText();
if (child.getElementType() == TokenType.WHITE_SPACE) {
if (text.contains("\n")) {
if (!text.contains("\\")) {
myResult.append("\\");
}
myResult.append(text);
}
}
else {
myResult.append(text);
}
child = child.getTreeNext();
}
}
}
@Override
public void visitPyGeneratorExpression(PyGeneratorExpression node) {
final PsiElement firstChild = node.getFirstChild();
if (firstChild != null && firstChild.getNode().getElementType() != PyTokenTypes.LPAR) {
myResult.append("(").append(node.getText()).append(")");
}
else {
super.visitPyGeneratorExpression(node);
}
}
@Override
public void visitElement(PsiElement element) {
if (element.getChildren().length == 0) {
myResult.append(element.getText());
}
else {
super.visitElement(element);
}
}
public String result() {
return myResult.toString();
}
}
protected abstract String getHelpId();
protected PyAssignmentStatement createDeclaration(Project project, String assignmentText, PsiElement anchor) {
LanguageLevel langLevel = ((PyFile) anchor.getContainingFile()).getLanguageLevel();
return PyElementGenerator.getInstance(project).createFromText(langLevel, PyAssignmentStatement.class, assignmentText);
}
protected boolean checkEnabled(IntroduceOperation operation) {
return true;
}
protected List<PsiElement> getOccurrences(PsiElement element, @NotNull final PyExpression expression) {
return PyRefactoringUtil.getOccurrences(expression, ScopeUtil.getScopeOwner(expression));
}
private PsiElement performReplace(@NotNull final PsiElement declaration,
final IntroduceOperation operation) {
final PyExpression expression = operation.getInitializer();
final Project project = operation.getProject();
return new WriteCommandAction<PsiElement>(project, expression.getContainingFile()) {
protected void run(@NotNull final Result<PsiElement> result) throws Throwable {
result.setResult(addDeclaration(operation, declaration));
PyExpression newExpression = createExpression(project, operation.getName(), declaration);
if (operation.isReplaceAll()) {
List<PsiElement> newOccurrences = new ArrayList<PsiElement>();
for (PsiElement occurrence : operation.getOccurrences()) {
final PsiElement replaced = replaceExpression(occurrence, newExpression, operation);
if (replaced != null) {
newOccurrences.add(replaced);
}
}
operation.setOccurrences(newOccurrences);
}
else {
final PsiElement replaced = replaceExpression(expression, newExpression, operation);
operation.setOccurrences(Collections.singletonList(replaced));
}
postRefactoring(operation.getElement());
}
}.execute().getResultObject();
}
@Nullable
public PsiElement addDeclaration(IntroduceOperation operation, PsiElement declaration) {
final PsiElement expression = operation.getInitializer();
final Pair<PsiElement, TextRange> data = expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (data == null) {
return addDeclaration(expression, declaration, operation);
}
else {
return addDeclaration(data.first, declaration, operation);
}
}
protected PyExpression createExpression(Project project, String name, PsiElement declaration) {
return PyElementGenerator.getInstance(project).createExpressionFromText(LanguageLevel.forElement(declaration), name);
}
@Nullable
protected abstract PsiElement addDeclaration(@NotNull final PsiElement expression,
@NotNull final PsiElement declaration,
@NotNull IntroduceOperation operation);
protected void postRefactoring(PsiElement element) {
}
private static class PyInplaceVariableIntroducer extends InplaceVariableIntroducer<PsiElement> {
private final PyTargetExpression myTarget;
public PyInplaceVariableIntroducer(PyTargetExpression target,
IntroduceOperation operation,
List<PsiElement> occurrences) {
super(target, operation.getEditor(), operation.getProject(), "Introduce Variable",
occurrences.toArray(new PsiElement[occurrences.size()]), null);
myTarget = target;
}
@Override
protected PsiElement checkLocalScope() {
return myTarget.getContainingFile();
}
}
}