package com.jetbrains.lang.dart.ide.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.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.IntroduceTargetChooser;
import com.intellij.refactoring.RefactoringActionHandler;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer;
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.jetbrains.lang.dart.DartBundle;
import com.jetbrains.lang.dart.DartComponentType;
import com.jetbrains.lang.dart.DartTokenTypes;
import com.jetbrains.lang.dart.psi.*;
import com.jetbrains.lang.dart.util.DartElementGenerator;
import com.jetbrains.lang.dart.util.DartNameSuggesterUtil;
import com.jetbrains.lang.dart.util.DartRefactoringUtil;
import com.jetbrains.lang.dart.util.UsefulPsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@SuppressWarnings("MethodMayBeStatic")
public abstract class DartIntroduceHandler implements RefactoringActionHandler {
@Nullable
protected static PsiElement findAnchor(PsiElement occurrence) {
return findAnchor(Collections.singletonList(occurrence));
}
@Nullable
protected static PsiElement findAnchor(List<PsiElement> occurrences) {
int minOffset = Integer.MAX_VALUE;
for (PsiElement element : occurrences) {
minOffset = Math.min(minOffset, element.getTextRange().getStartOffset());
}
DartStatements statements = findContainingStatements(occurrences);
if (statements == null) {
return null;
}
PsiElement child = null;
PsiElement[] children = statements.getChildren();
for (PsiElement aChildren : children) {
child = aChildren;
if (child.getTextRange().contains(minOffset)) {
break;
}
}
return child;
}
@Nullable
private static DartStatements findContainingStatements(List<PsiElement> occurrences) {
DartStatements result = PsiTreeUtil.getParentOfType(occurrences.get(0), DartStatements.class, true);
while (result != null && !UsefulPsiTreeUtil.isAncestor(result, occurrences, true)) {
result = PsiTreeUtil.getParentOfType(result, DartStatements.class, true);
}
return result;
}
protected final String myDialogTitle;
public DartIntroduceHandler(@NotNull final String dialogTitle) {
myDialogTitle = dialogTitle;
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file, DataContext dataContext) {
performAction(new DartIntroduceOperation(project, editor, file, null));
}
@Override
public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) {
}
public void performAction(DartIntroduceOperation 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();
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);
}
}
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 = DartRefactoringUtil.getSelectedExpression(project, file, element1, element2);
if (element1 == null) {
showCannotPerformError(project, editor);
return;
}
if (!checkIntroduceContext(file, editor, element1)) {
return;
}
operation.setElement(element1);
performActionOnElement(operation);
}
protected boolean checkIntroduceContext(PsiFile file, Editor editor, PsiElement element) {
if (!isValidIntroduceContext(element)) {
showCannotPerformError(file.getProject(), editor);
return false;
}
return true;
}
private void showCannotPerformError(Project project, Editor editor) {
CommonRefactoringUtil.showErrorHint(
project,
editor,
DartBundle.message("refactoring.introduce.selection.error"),
myDialogTitle,
"refactoring.extractMethod"
);
}
protected boolean isValidIntroduceContext(PsiElement element) {
return PsiTreeUtil.getParentOfType(element, DartFormalParameterList.class) == null;
}
private boolean smartIntroduce(final DartIntroduceOperation 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<DartExpression> expressions = new ArrayList<>();
while (elementAtCaret != null) {
if (elementAtCaret instanceof DartFile) {
break;
}
if (elementAtCaret instanceof DartExpression) {
expressions.add((DartExpression)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<DartExpression>() {
@Override
public void pass(DartExpression expression) {
operation.setElement(expression);
performActionOnElement(operation);
}
}, PsiElement::getText
);
return true;
}
return false;
}
private void performActionOnElement(DartIntroduceOperation operation) {
if (!checkEnabled(operation)) {
return;
}
final PsiElement element = operation.getElement();
final DartExpression initializer = (DartExpression)element;
operation.setInitializer(initializer);
operation.setOccurrences(getOccurrences(element, initializer));
operation.setSuggestedNames(DartNameSuggesterUtil.getSuggestedNames(initializer));
if (operation.getOccurrences().size() == 0) {
operation.setReplaceAll(false);
}
performActionOnElementOccurrences(operation);
}
protected void performActionOnElementOccurrences(final DartIntroduceOperation operation) {
final Editor editor = operation.getEditor();
if (editor.getSettings().isVariableInplaceRenameEnabled()) {
ensureName(operation);
if (operation.isReplaceAll()) {
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 boolean checkEnabled(DartIntroduceOperation operation) {
return true;
}
protected static void ensureName(DartIntroduceOperation operation) {
if (operation.getName() == null) {
final Collection<String> suggestedNames = operation.getSuggestedNames();
if (suggestedNames.size() > 0) {
operation.setName(suggestedNames.iterator().next());
}
else {
operation.setName("x");
}
}
}
protected List<PsiElement> getOccurrences(PsiElement element, @NotNull final DartExpression expression) {
PsiElement context = element;
DartComponentType type;
do {
context = PsiTreeUtil.getParentOfType(context, DartComponent.class, true);
type = DartComponentType.typeOf(context);
}
while (type != null && notFunctionMethodClass(type));
if (context == null) {
context = expression.getContainingFile();
}
return DartRefactoringUtil.getOccurrences(expression, context);
}
private static boolean notFunctionMethodClass(DartComponentType type) {
final boolean isFunctionMethodClass = type == DartComponentType.METHOD ||
type == DartComponentType.FUNCTION ||
type == DartComponentType.CLASS;
return !isFunctionMethodClass;
}
protected void performIntroduceWithDialog(DartIntroduceOperation operation) {
final Project project = operation.getProject();
if (operation.getName() == null) {
DartIntroduceDialog dialog = new DartIntroduceDialog(project, myDialogTitle, operation);
if (!dialog.showAndGet()) {
return;
}
operation.setName(dialog.getName());
operation.setReplaceAll(dialog.doReplaceAllOccurrences());
}
PsiElement declaration = performRefactoring(operation);
if (declaration == null) {
return;
}
final Editor editor = operation.getEditor();
editor.getCaretModel().moveToOffset(declaration.getTextRange().getEndOffset());
editor.getSelectionModel().removeSelection();
}
protected void performInplaceIntroduce(DartIntroduceOperation operation) {
final List<PsiElement> occurrences = operation.getOccurrences();
final List<SmartPsiElementPointer<PsiElement>> pointers = new ArrayList<>(occurrences.size());
for (PsiElement occurrence : occurrences) {
pointers.add(SmartPointerManager.getInstance(operation.getProject()).createSmartPsiElementPointer(occurrence));
}
final PsiElement statement = performRefactoring(operation);
final DartComponent target = PsiTreeUtil.findChildOfType(statement, DartComponent.class);
final DartComponentName componentName = target != null ? target.getComponentName() : null;
if (componentName == null) {
return;
}
operation.getEditor().getCaretModel().moveToOffset(componentName.getTextOffset());
occurrences.clear();
for (SmartPsiElementPointer<PsiElement> pointer : pointers) {
final PsiElement element = pointer.getElement();
if (element != null) {
occurrences.add(element);
}
}
final InplaceVariableIntroducer<PsiElement> introducer =
new DartInplaceVariableIntroducer(componentName, operation, occurrences);
introducer.performInplaceRefactoring(new LinkedHashSet<>(operation.getSuggestedNames()));
}
@Nullable
protected PsiElement performRefactoring(@NotNull DartIntroduceOperation operation) {
PsiElement anchor = operation.isReplaceAll()
? findAnchor(operation.getOccurrences())
: findAnchor(operation.getInitializer());
if (anchor == null) {
CommonRefactoringUtil.showErrorHint(
operation.getProject(),
operation.getEditor(),
RefactoringBundle.getCannotRefactorMessage(DartBundle.message("dart.refactoring.introduce.anchor.error")),
DartBundle.message("dart.refactoring.introduce.error"),
null
);
return null;
}
PsiElement declaration = createDeclaration(operation);
if (declaration == null) {
showCannotPerformError(operation.getProject(), operation.getEditor());
return null;
}
final SmartPsiElementPointer<PsiElement> pointer = performReplace(declaration, operation);
final PsiElement element = pointer == null ? null : pointer.getElement();
if (element != null) {
declaration = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(element);
}
return declaration;
}
@Nullable
public PsiElement createDeclaration(DartIntroduceOperation operation) {
final Project project = operation.getProject();
final DartExpression initializer = operation.getInitializer();
InitializerTextBuilder builder = new InitializerTextBuilder();
initializer.accept(builder);
String assignmentText = getDeclarationString(operation, builder.result());
return DartElementGenerator.createStatementFromText(project, assignmentText);
}
abstract protected String getDeclarationString(DartIntroduceOperation operation, String initExpression);
@Nullable
private SmartPsiElementPointer<PsiElement> performReplace(@NotNull final PsiElement declaration, final DartIntroduceOperation operation) {
final DartExpression expression = operation.getInitializer();
final Project project = operation.getProject();
return new WriteCommandAction<SmartPsiElementPointer<PsiElement>>(project, expression.getContainingFile()) {
protected void run(@NotNull final Result<SmartPsiElementPointer<PsiElement>> result) throws Throwable {
final PsiElement createdDeclaration = addDeclaration(operation, declaration);
if (createdDeclaration != null) {
result.setResult(SmartPointerManager.getInstance(project).createSmartPsiElementPointer(createdDeclaration));
modifyDeclaration(createdDeclaration);
}
PsiElement newExpression = createExpression(project, operation.getName());
if (operation.isReplaceAll()) {
List<PsiElement> newOccurrences = new ArrayList<>();
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();
}
protected void modifyDeclaration(@NotNull PsiElement declaration) {
final PsiElement parent = declaration.getParent();
PsiElement newLineNode = PsiParserFacade.SERVICE.getInstance(declaration.getProject()).createWhiteSpaceFromText("\n");
parent.addAfter(newLineNode, declaration);
final ASTNode nextChild = declaration.getNode().getTreeNext();
parent.getNode().addLeaf(DartTokenTypes.SEMICOLON, ";", nextChild);
}
@Nullable
protected DartReference createExpression(Project project, String name) {
return DartElementGenerator.createReferenceFromText(project, name);
}
@Nullable
protected PsiElement replaceExpression(PsiElement expression, PsiElement newExpression, DartIntroduceOperation operation) {
return expression.replace(newExpression);
}
protected void postRefactoring(PsiElement element) {
}
@Nullable
public PsiElement addDeclaration(DartIntroduceOperation operation, PsiElement declaration) {
PsiElement anchor = operation.isReplaceAll() ? findAnchor(operation.getOccurrences()) : findAnchor(operation.getInitializer());
if (anchor == null) {
CommonRefactoringUtil.showErrorHint(
operation.getProject(),
operation.getEditor(),
RefactoringBundle.getCannotRefactorMessage(DartBundle.message("dart.refactoring.introduce.anchor.error")),
DartBundle.message("dart.refactoring.introduce.error"),
null
);
return null;
}
final PsiElement parent = anchor.getParent();
return parent.addBefore(declaration, anchor);
}
private static class DartInplaceVariableIntroducer extends InplaceVariableIntroducer<PsiElement> {
private final DartComponentName myTarget;
public DartInplaceVariableIntroducer(DartComponentName target,
DartIntroduceOperation 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();
}
@Override
protected void collectAdditionalElementsToRename(List<Pair<PsiElement, TextRange>> stringUsages) {
for (PsiElement expression : getOccurrences()) {
LOG.assertTrue(expression.isValid(), expression.getText());
stringUsages.add(Pair.create(expression, new TextRange(0, expression.getTextLength())));
}
}
}
private static class InitializerTextBuilder extends PsiRecursiveElementVisitor {
private final StringBuilder myResult = new StringBuilder();
@Override
public void visitWhiteSpace(PsiWhiteSpace space) {
myResult.append(space.getText().replace('\n', ' '));
}
@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();
}
}
}