/*
* Copyright 2010-2016 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 org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.JavaProjectRootsUtil;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.PackageWrapper;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.move.MoveDialogBase;
import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil;
import com.intellij.refactoring.move.moveInner.MoveInnerImpl;
import com.intellij.refactoring.ui.NameSuggestionsField;
import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.refactoring.util.RefactoringMessageUtil;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.ui.EditorTextField;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.descriptors.ClassDescriptor;
import org.jetbrains.kotlin.descriptors.ClassifierDescriptor;
import org.jetbrains.kotlin.idea.KotlinFileType;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.idea.core.CollectingNameValidator;
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester;
import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator;
import org.jetbrains.kotlin.idea.core.PackageUtilsKt;
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings;
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt;
import org.jetbrains.kotlin.idea.refactoring.move.MoveUtilsKt;
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*;
import org.jetbrains.kotlin.incremental.components.NoLookupLocation;
import org.jetbrains.kotlin.name.FqName;
import org.jetbrains.kotlin.name.Name;
import org.jetbrains.kotlin.psi.*;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode;
import org.jetbrains.kotlin.types.KotlinType;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.Collections;
import java.util.List;
public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase {
@NonNls private static final String RECENTS_KEY = MoveKotlinNestedClassesToUpperLevelDialog.class.getName() + ".RECENTS_KEY";
private final Project project;
private final KtClassOrObject innerClass;
private final ClassDescriptor innerClassDescriptor;
private final PsiElement targetContainer;
private EditorTextField classNameField;
private NameSuggestionsField parameterField;
private JCheckBox passOuterClassCheckBox;
private JPanel panel;
private JCheckBox searchInCommentsCheckBox;
private JCheckBox searchForTextOccurrencesCheckBox;
private PackageNameReferenceEditorCombo packageNameField;
private JLabel packageNameLabel;
private JLabel classNameLabel;
private JLabel parameterNameLabel;
private JPanel openInEditorPanel;
public MoveKotlinNestedClassesToUpperLevelDialog(
@NotNull Project project,
@NotNull KtClassOrObject innerClass,
@NotNull PsiElement targetContainer
) {
super(project, true);
this.project = project;
this.innerClass = innerClass;
this.targetContainer = targetContainer;
this.innerClassDescriptor = (ClassDescriptor) ResolutionUtils.resolveToDescriptor(innerClass, BodyResolveMode.FULL);
setTitle("Move Nested Classes to Upper Level");
init();
packageNameLabel.setLabelFor(packageNameField.getChildComponent());
classNameLabel.setLabelFor(classNameField);
parameterNameLabel.setLabelFor(parameterField);
openInEditorPanel.add(initOpenInEditorCb(), BorderLayout.EAST);
}
@Nullable
private static FqName getTargetPackageFqName(PsiElement targetContainer) {
if (targetContainer instanceof PsiDirectory) {
PsiPackage targetPackage = PackageUtilsKt.getPackage((PsiDirectory) targetContainer);
return targetPackage != null ? new FqName(targetPackage.getQualifiedName()) : null;
}
if (targetContainer instanceof KtFile) return ((KtFile) targetContainer).getPackageFqName();
return null;
}
private void createUIComponents() {
parameterField = new NameSuggestionsField(project);
packageNameField = new PackageNameReferenceEditorCombo("", project, RECENTS_KEY,
RefactoringBundle.message("choose.destination.package"));
}
@Override
protected String getMovePropertySuffix() {
return "Nested Classes to Upper Level";
}
@Override
protected String getHelpId() {
return HelpID.MOVE_INNER_UPPER;
}
@Override
protected String getCbTitle() {
return "Open moved member in editor";
}
public boolean isSearchInComments() {
return searchInCommentsCheckBox.isSelected();
}
public boolean isSearchInNonJavaFiles() {
return searchForTextOccurrencesCheckBox.isSelected();
}
public String getClassName() {
return classNameField.getText().trim();
}
@Nullable
public String getParameterName() {
return parameterField != null ? parameterField.getEnteredName() : null;
}
private boolean isThisNeeded() {
return innerClass instanceof KtClass && MoveUtilsKt.traverseOuterInstanceReferences((KtClass) innerClass, true);
}
@Nullable
private FqName getTargetPackageFqName() {
return getTargetPackageFqName(targetContainer);
}
@NotNull
private KotlinType getOuterInstanceType() {
return ((ClassDescriptor) innerClassDescriptor.getContainingDeclaration()).getDefaultType();
}
@Override
protected void init() {
classNameField.setText(innerClass.getName());
classNameField.selectAll();
if (innerClass instanceof KtClass && ((KtClass) innerClass).isInner()) {
passOuterClassCheckBox.setSelected(true);
passOuterClassCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
parameterField.setEnabled(passOuterClassCheckBox.isSelected());
}
});
}
else {
passOuterClassCheckBox.setSelected(false);
passOuterClassCheckBox.setEnabled(false);
parameterField.setEnabled(false);
}
if (passOuterClassCheckBox.isEnabled()) {
boolean thisNeeded = isThisNeeded();
passOuterClassCheckBox.setSelected(thisNeeded);
parameterField.setEnabled(thisNeeded);
}
passOuterClassCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
boolean selected = passOuterClassCheckBox.isSelected();
parameterField.getComponent().setEnabled(selected);
}
});
if (!(targetContainer instanceof PsiDirectory)) {
packageNameField.setVisible(false);
packageNameLabel.setVisible(false);
}
if (innerClass instanceof KtClass && ((KtClass) innerClass).isInner()) {
KtClassBody innerClassBody = innerClass.getBody();
Function1<String, Boolean> validator =
innerClassBody != null
? new NewDeclarationNameValidator(innerClassBody, (PsiElement) null,
NewDeclarationNameValidator.Target.VARIABLES,
Collections.<KtDeclaration>emptyList())
: new CollectingNameValidator();
List<String> suggestions = KotlinNameSuggester.INSTANCE.suggestNamesByType(getOuterInstanceType(), validator, "outer");
parameterField.setSuggestions(ArrayUtil.toStringArray(suggestions));
}
else {
parameterField.getComponent().setEnabled(false);
}
FqName packageFqName = getTargetPackageFqName();
if (packageFqName != null) {
packageNameField.prependItem(packageFqName.asString());
}
KotlinRefactoringSettings settings = KotlinRefactoringSettings.getInstance();
searchForTextOccurrencesCheckBox.setSelected(settings.MOVE_TO_UPPER_LEVEL_SEARCH_FOR_TEXT);
searchInCommentsCheckBox.setSelected(settings.MOVE_TO_UPPER_LEVEL_SEARCH_IN_COMMENTS);
super.init();
}
@Override
public JComponent getPreferredFocusedComponent() {
return classNameField;
}
@Override
protected String getDimensionServiceKey() {
return "#com.intellij.refactoring.move.moveInner.MoveInnerDialog";
}
@Override
protected JComponent createNorthPanel() {
return panel;
}
@Override
protected JComponent createCenterPanel() {
return null;
}
@Nullable
private PsiElement getTargetContainer() {
if (targetContainer instanceof PsiDirectory) {
PsiDirectory psiDirectory = (PsiDirectory) targetContainer;
FqName oldPackageFqName = getTargetPackageFqName();
String targetName = packageNameField.getText();
if (!Comparing.equal(oldPackageFqName != null ? oldPackageFqName.asString() : null, targetName)) {
ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project);
List<VirtualFile> contentSourceRoots = JavaProjectRootsUtil.getSuitableDestinationSourceRoots(project);
final PackageWrapper newPackage = new PackageWrapper(PsiManager.getInstance(project), targetName);
final VirtualFile targetSourceRoot;
if (contentSourceRoots.size() > 1) {
PsiDirectory initialDir = null;
PsiPackage oldPackage = oldPackageFqName != null
? JavaPsiFacade.getInstance(project).findPackage(oldPackageFqName.asString())
: null;
if (oldPackage != null) {
PsiDirectory[] directories = oldPackage.getDirectories();
VirtualFile root = projectRootManager.getFileIndex().getContentRootForFile(psiDirectory.getVirtualFile());
for (PsiDirectory dir : directories) {
if (Comparing.equal(projectRootManager.getFileIndex().getContentRootForFile(dir.getVirtualFile()), root)) {
initialDir = dir;
}
}
}
VirtualFile sourceRoot = MoveClassesOrPackagesUtil.chooseSourceRoot(newPackage, contentSourceRoots, initialDir);
if (sourceRoot == null) return null;
targetSourceRoot = sourceRoot;
}
else {
targetSourceRoot = contentSourceRoots.get(0);
}
PsiDirectory dir = RefactoringUtil.findPackageDirectoryInSourceRoot(newPackage, targetSourceRoot);
if (dir == null) {
dir = ApplicationManager.getApplication().runWriteAction(new NullableComputable<PsiDirectory>() {
@Override
public PsiDirectory compute() {
try {
return RefactoringUtil.createPackageDirectoryInSourceRoot(newPackage, targetSourceRoot);
}
catch (IncorrectOperationException e) {
return null;
}
}
});
}
return dir;
}
return targetContainer;
}
if (targetContainer instanceof KtFile || targetContainer instanceof KtClassOrObject) return targetContainer;
return null;
}
@Nullable
private PsiElement getTargetContainerWithValidation() throws ConfigurationException {
String className = getClassName();
String parameterName = getParameterName();
if (className != null && className.isEmpty()) throw new ConfigurationException(RefactoringBundle.message("no.class.name.specified"));
if (!KotlinNameSuggester.INSTANCE.isIdentifier(className)) throw new ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(className));
if (passOuterClassCheckBox.isSelected()) {
if (parameterName != null && parameterName.isEmpty()) throw new ConfigurationException(RefactoringBundle.message("no.parameter.name.specified"));
if (!KotlinNameSuggester.INSTANCE.isIdentifier(parameterName)) throw new ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(parameterName));
}
PsiElement targetContainer = getTargetContainer();
if (targetContainer instanceof KtClassOrObject) {
KtClassOrObject targetClass = (KtClassOrObject) targetContainer;
for (KtDeclaration member : targetClass.getDeclarations()) {
if (member instanceof KtClassOrObject && className != null && className.equals(member.getName())) {
throw new ConfigurationException(RefactoringBundle.message("inner.class.exists", className, targetClass.getName()));
}
}
}
if (targetContainer instanceof PsiDirectory || targetContainer instanceof KtFile) {
FqName targetPackageFqName = getTargetPackageFqName();
if (targetPackageFqName == null) throw new ConfigurationException("No package corresponds to this directory");
//noinspection ConstantConditions
ClassifierDescriptor existingClass = DescriptorUtils
.getContainingModule(innerClassDescriptor)
.getPackage(targetPackageFqName)
.getMemberScope()
.getContributedClassifier(Name.identifier(className), NoLookupLocation.FROM_IDE);
if (existingClass != null) throw new ConfigurationException("Class " + className + " already exists in package " + targetPackageFqName);
PsiDirectory targetDir = targetContainer instanceof PsiDirectory
? (PsiDirectory) targetContainer
: targetContainer.getContainingFile().getContainingDirectory();
String message = RefactoringMessageUtil.checkCanCreateFile(targetDir, className + ".kt");
if (message != null) throw new ConfigurationException(message);
}
return targetContainer;
}
@Override
protected void doAction() {
PsiElement target;
try {
target = getTargetContainerWithValidation();
if (target == null) return;
}
catch (ConfigurationException e) {
CommonRefactoringUtil.showErrorMessage(MoveInnerImpl.REFACTORING_NAME, e.getMessage(), HelpID.MOVE_INNER_UPPER, project);
return;
}
KotlinRefactoringSettings settings = KotlinRefactoringSettings.getInstance();
settings.MOVE_TO_UPPER_LEVEL_SEARCH_FOR_TEXT = searchForTextOccurrencesCheckBox.isSelected();
settings.MOVE_TO_UPPER_LEVEL_SEARCH_IN_COMMENTS = searchInCommentsCheckBox.isSelected();
KotlinMoveTarget moveTarget;
if (target instanceof PsiDirectory) {
final PsiDirectory targetDir = (PsiDirectory) target;
final FqName targetPackageFqName = getTargetPackageFqName(target);
if (targetPackageFqName == null) return;
String innerClassName = innerClass.getName();
if (innerClassName == null) return;
final String targetFileName = KotlinNameSuggester.INSTANCE.suggestNameByName(
innerClassName,
new Function1<String, Boolean>() {
@Override
public Boolean invoke(String s) {
return targetDir.findFile(s + "." + KotlinFileType.EXTENSION) == null;
}
}
) + "." + KotlinFileType.EXTENSION;
moveTarget = new KotlinMoveTargetForDeferredFile(
targetPackageFqName,
targetDir,
null,
new Function1<KtFile, KtFile>() {
@Override
public KtFile invoke(@NotNull KtFile originalFile) {
return KotlinRefactoringUtilKt.createKotlinFile(targetFileName, targetDir, targetPackageFqName.asString());
}
}
);
}
else {
//noinspection ConstantConditions
moveTarget = new KotlinMoveTargetForExistingElement((KtElement) target);
}
String outerInstanceParameterName = passOuterClassCheckBox.isSelected() ? getParameterName() : null;
String newClassName = getClassName();
MoveDeclarationsDelegate delegate = new MoveDeclarationsDelegate.NestedClass(newClassName, outerInstanceParameterName);
MoveDeclarationsDescriptor moveDescriptor = new MoveDeclarationsDescriptor(
project,
CollectionsKt.listOf(innerClass),
moveTarget,
delegate,
isSearchInComments(),
isSearchInNonJavaFiles(),
false,
false,
null,
isOpenInEditor()
);
saveOpenInEditorOption();
invokeRefactoring(new MoveKotlinDeclarationsProcessor(moveDescriptor, Mover.Default.INSTANCE));
}
}