diff --git a/idea/idea-jps-common/src/org/jetbrains/kotlin/config/KotlinSourceRootType.kt b/idea/idea-jps-common/src/org/jetbrains/kotlin/config/KotlinSourceRootType.kt index 680ffeaa67c..bd1826dfac7 100644 --- a/idea/idea-jps-common/src/org/jetbrains/kotlin/config/KotlinSourceRootType.kt +++ b/idea/idea-jps-common/src/org/jetbrains/kotlin/config/KotlinSourceRootType.kt @@ -15,5 +15,9 @@ sealed class KotlinSourceRootType : JpsElementTypeBase object TestSource : KotlinSourceRootType() override fun createDefaultProperties() = JpsJavaExtensionService.getInstance().createSourceRootProperties("") + + companion object { + val ALL_SOURCES = setOf(Source, TestSource) + } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/CreateKotlinSubClassIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/CreateKotlinSubClassIntention.kt index 599c4ebb8b8..9854d5d2ba5 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/CreateKotlinSubClassIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/CreateKotlinSubClassIntention.kt @@ -18,7 +18,6 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.codeInsight.CodeInsightUtil import com.intellij.codeInsight.daemon.impl.quickfix.CreateClassKind -import com.intellij.codeInsight.intention.impl.CreateClassDialog import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.module.ModuleUtilCore @@ -32,14 +31,13 @@ import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.overrideImplement.ImplementMembersHandler import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile +import org.jetbrains.kotlin.idea.refactoring.ui.CreateKotlinClassDialog import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.KtPsiFactory.ClassHeaderBuilder import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.resolve.ModifiersChecker -import java.lang.IllegalArgumentException -import java.lang.IllegalStateException private const val IMPL_SUFFIX = "Impl" @@ -163,11 +161,11 @@ class CreateKotlinSubClassIntention : SelfTargetingRangeIntention(KtCla PsiElementRenameHandler.rename(klass, project, container, editor) } - private fun chooseSubclassToCreate(baseClass: KtClass, baseName: String): CreateClassDialog? { + private fun chooseSubclassToCreate(baseClass: KtClass, baseName: String): CreateKotlinClassDialog? { val sourceDir = baseClass.containingFile.containingDirectory val aPackage = JavaDirectoryService.getInstance().getPackage(sourceDir) - val dialog = object : CreateClassDialog( + val dialog = object : CreateKotlinClassDialog( baseClass.project, text, targetNameWithoutConflicts(baseName, baseClass.containingClassOrObject), aPackage?.qualifiedName ?: "", diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromUsageFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromUsageFix.kt index 8d902d538e3..5308411f8bf 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromUsageFix.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/createFromUsage/createClass/CreateClassFromUsageFix.kt @@ -38,6 +38,7 @@ import org.jetbrains.kotlin.idea.refactoring.SeparateFileWrapper import org.jetbrains.kotlin.idea.refactoring.canRefactor import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile +import org.jetbrains.kotlin.idea.refactoring.ui.CreateKotlinClassDialog import org.jetbrains.kotlin.idea.references.KtSimpleNameReference import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.util.application.executeCommand @@ -184,7 +185,7 @@ open class CreateClassFromUsageFix protected constructor ( val ideaClassKind = classInfo.kind.toIdeaClassKind() val defaultPackageFqName = file.packageFqName - val dialog = object : CreateClassDialog( + val dialog = object : CreateKotlinClassDialog( file.project, "Create ${ideaClassKind.description.capitalize()}", className, diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt index 89310e492f5..32ffd7e01f4 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt @@ -28,7 +28,6 @@ import com.intellij.refactoring.MoveDestination import com.intellij.refactoring.PackageWrapper import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog -import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo import com.intellij.ui.EditorTextField import com.intellij.ui.RecentsManager @@ -41,6 +40,7 @@ import org.jetbrains.annotations.NonNls import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.Pass import org.jetbrains.kotlin.idea.refactoring.hasIdentifiersOnly +import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox import org.jetbrains.kotlin.idea.util.sourceRoot import org.jetbrains.kotlin.name.FqNameUnsafe import org.jetbrains.kotlin.psi.KtNamedDeclaration @@ -61,7 +61,7 @@ class CopyKotlinDeclarationDialog( private val packageLabel = JLabel() private lateinit var packageNameField: ReferenceEditorComboWithBrowseButton private val openInEditorCheckBox = CopyFilesOrDirectoriesDialog.createOpenInEditorCB() - private val destinationComboBox = object : DestinationFolderComboBox() { + private val destinationComboBox = object : KotlinDestinationFolderComboBox() { override fun getTargetPackage() = packageNameField.text.trim() override fun reportBaseInTestSelectionInSource() = true } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt.173 b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt.173 index 87f5eb105ef..89ae37cbb27 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt.173 +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationDialog.kt.173 @@ -28,7 +28,6 @@ import com.intellij.refactoring.MoveDestination import com.intellij.refactoring.PackageWrapper import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog -import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo import com.intellij.ui.EditorTextField import com.intellij.ui.RecentsManager @@ -41,6 +40,7 @@ import org.jetbrains.annotations.NonNls import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.Pass import org.jetbrains.kotlin.idea.refactoring.hasIdentifiersOnly +import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox import org.jetbrains.kotlin.idea.util.sourceRoot import org.jetbrains.kotlin.name.FqNameUnsafe import org.jetbrains.kotlin.psi.KtNamedDeclaration @@ -61,7 +61,7 @@ class CopyKotlinDeclarationDialog( private val packageLabel = JLabel() private lateinit var packageNameField: ReferenceEditorComboWithBrowseButton private val openInEditorCheckBox = CopyFilesOrDirectoriesDialog.createOpenInEditorCB() - private val destinationComboBox = object : DestinationFolderComboBox() { + private val destinationComboBox = object : KotlinDestinationFolderComboBox() { override fun getTargetPackage() = packageNameField.text.trim() override fun reportBaseInTestSelectionInSource() = true } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java index c8cf2abd187..eed38549b01 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java @@ -38,7 +38,6 @@ import com.intellij.refactoring.classMembers.MemberInfoChangeListener; import com.intellij.refactoring.move.MoveCallback; import com.intellij.refactoring.move.MoveHandler; import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination; -import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox; import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination; import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo; import com.intellij.refactoring.ui.RefactoringDialog; @@ -63,7 +62,7 @@ import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPan import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTable; import org.jetbrains.kotlin.idea.refactoring.move.MoveUtilsKt; import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*; -import static org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsProcessorKt.MoveSource; +import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinFileChooserDialog; import org.jetbrains.kotlin.idea.util.application.ApplicationUtilsKt; import org.jetbrains.kotlin.name.FqName; @@ -80,6 +79,7 @@ import java.util.*; import java.util.List; import static java.util.Collections.emptyList; +import static org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsProcessorKt.MoveSource; public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private static final String RECENTS_KEY = "MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY"; @@ -300,7 +300,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { classPackageChooser.prependItem(targetPackageName); } - ((DestinationFolderComboBox) destinationFolderCB).setData( + ((KotlinDestinationFolderComboBox) destinationFolderCB).setData( myProject, targetDirectory, new Pass() { @@ -406,7 +406,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private void createUIComponents() { classPackageChooser = createPackageChooser(); - destinationFolderCB = new DestinationFolderComboBox() { + destinationFolderCB = new KotlinDestinationFolderComboBox() { @Override public String getTargetPackage() { return MoveKotlinTopLevelDeclarationsDialog.this.getTargetPackage(); diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/CreateKotlinClassDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/CreateKotlinClassDialog.java new file mode 100644 index 00000000000..5950400b960 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/CreateKotlinClassDialog.java @@ -0,0 +1,279 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.ui; + +import com.intellij.CommonBundle; +import com.intellij.codeInsight.CodeInsightBundle; +import com.intellij.codeInsight.daemon.impl.quickfix.ClassKind; +import com.intellij.ide.util.PackageUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CustomShortcutSet; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.JavaProjectRootsUtil; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Pass; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiNameHelper; +import com.intellij.refactoring.MoveDestination; +import com.intellij.refactoring.PackageWrapper; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination; +import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo; +import com.intellij.refactoring.util.RefactoringMessageUtil; +import com.intellij.ui.DocumentAdapter; +import com.intellij.ui.RecentsManager; +import com.intellij.ui.ReferenceEditorComboWithBrowseButton; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.ui.JBUI; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import java.awt.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +// Based on com.intellij.codeInsight.intention.impl.CreateClassDialog +public class CreateKotlinClassDialog extends DialogWrapper { + private final JLabel myInformationLabel = new JLabel("#"); + private final JLabel myPackageLabel = new JLabel(CodeInsightBundle.message("dialog.create.class.destination.package.label")); + private final ReferenceEditorComboWithBrowseButton myPackageComponent; + private final JTextField myTfClassName = new MyTextField(); + private final Project myProject; + private PsiDirectory myTargetDirectory; + private final String myClassName; + private final boolean myClassNameEditable; + private final Module myModule; + private final KotlinDestinationFolderComboBox myDestinationCB = new KotlinDestinationFolderComboBox() { + @Override + public String getTargetPackage() { + return myPackageComponent.getText().trim(); + } + + @Override + protected boolean reportBaseInTestSelectionInSource() { + return CreateKotlinClassDialog.this.reportBaseInTestSelectionInSource(); + } + + @Override + protected boolean reportBaseInSourceSelectionInTest() { + return CreateKotlinClassDialog.this.reportBaseInSourceSelectionInTest(); + } + }; + @NonNls private static final String RECENTS_KEY = "CreateKotlinClassDialog.RecentsKey"; + + public CreateKotlinClassDialog(@NotNull Project project, + @NotNull String title, + @NotNull String targetClassName, + @NotNull String targetPackageName, + @NotNull ClassKind kind, + boolean classNameEditable, + @Nullable Module defaultModule) { + super(project, true); + myClassNameEditable = classNameEditable; + myModule = defaultModule; + myClassName = targetClassName; + myProject = project; + myPackageComponent = new PackageNameReferenceEditorCombo(targetPackageName, myProject, RECENTS_KEY, CodeInsightBundle.message("dialog.create.class.package.chooser.title")); + myPackageComponent.setTextFieldPreferredWidth(40); + + init(); + + if (!myClassNameEditable) { + setTitle(CodeInsightBundle.message("dialog.create.class.name", StringUtil.capitalize(kind.getDescription()), targetClassName)); + } + else { + myInformationLabel.setText(CodeInsightBundle.message("dialog.create.class.label", kind.getDescription())); + setTitle(title); + } + + myTfClassName.setText(myClassName); + myDestinationCB.setData(myProject, getBaseDir(targetPackageName), new Pass() { + @Override + public void pass(String s) { + setErrorText(s, myDestinationCB); + } + }, myPackageComponent.getChildComponent()); + } + + protected boolean reportBaseInTestSelectionInSource() { + return false; + } + + protected boolean reportBaseInSourceSelectionInTest() { + return false; + } + + @NotNull + @Override + protected Action[] createActions() { + return new Action[]{getOKAction(), getCancelAction()}; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myClassNameEditable ? myTfClassName : myPackageComponent.getChildComponent(); + } + + @Override + protected JComponent createCenterPanel() { + return new JPanel(new BorderLayout()); + } + + @Override + protected JComponent createNorthPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbConstraints = new GridBagConstraints(); + + gbConstraints.insets = JBUI.insets(4, 8); + gbConstraints.fill = GridBagConstraints.HORIZONTAL; + gbConstraints.anchor = GridBagConstraints.WEST; + + if (myClassNameEditable) { + gbConstraints.weightx = 0; + gbConstraints.gridwidth = 1; + panel.add(myInformationLabel, gbConstraints); + gbConstraints.insets = JBUI.insets(4, 8); + gbConstraints.gridx = 1; + gbConstraints.weightx = 1; + gbConstraints.gridwidth = 1; + gbConstraints.fill = GridBagConstraints.HORIZONTAL; + gbConstraints.anchor = GridBagConstraints.WEST; + panel.add(myTfClassName, gbConstraints); + + myTfClassName.getDocument().addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(DocumentEvent e) { + getOKAction().setEnabled(PsiNameHelper.getInstance(myProject).isIdentifier(myTfClassName.getText())); + } + }); + getOKAction().setEnabled(StringUtil.isNotEmpty(myClassName)); + } + + gbConstraints.gridx = 0; + gbConstraints.gridy = 2; + gbConstraints.weightx = 0; + gbConstraints.gridwidth = 1; + panel.add(myPackageLabel, gbConstraints); + + gbConstraints.gridx = 1; + gbConstraints.weightx = 1; + + new AnAction() { + @Override + public void actionPerformed(AnActionEvent e) { + myPackageComponent.getButton().doClick(); + } + }.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK)), myPackageComponent.getChildComponent()); + + JPanel _panel = new JPanel(new BorderLayout()); + _panel.add(myPackageComponent, BorderLayout.CENTER); + panel.add(_panel, gbConstraints); + + gbConstraints.gridy = 3; + gbConstraints.gridx = 0; + gbConstraints.gridwidth = 2; + gbConstraints.insets.top = 12; + gbConstraints.anchor = GridBagConstraints.WEST; + gbConstraints.fill = GridBagConstraints.NONE; + JBLabel label = new JBLabel(RefactoringBundle.message("target.destination.folder")); + panel.add(label, gbConstraints); + + gbConstraints.gridy = 4; + gbConstraints.gridx = 0; + gbConstraints.fill = GridBagConstraints.HORIZONTAL; + gbConstraints.insets.top = 4; + panel.add(myDestinationCB, gbConstraints); + + boolean isMultipleSourceRoots = JavaProjectRootsUtil.getSuitableDestinationSourceRoots(myProject).size() > 1; + myDestinationCB.setVisible(isMultipleSourceRoots); + label.setVisible(isMultipleSourceRoots); + label.setLabelFor(myDestinationCB); + return panel; + } + + public PsiDirectory getTargetDirectory() { + return myTargetDirectory; + } + + private String getPackageName() { + String name = myPackageComponent.getText(); + return name != null ? name.trim() : ""; + } + + private static class MyTextField extends JTextField { + @Override + public Dimension getPreferredSize() { + Dimension size = super.getPreferredSize(); + FontMetrics fontMetrics = getFontMetrics(getFont()); + size.width = fontMetrics.charWidth('a') * 40; + return size; + } + } + + @Override + protected void doOKAction() { + RecentsManager.getInstance(myProject).registerRecentEntry(RECENTS_KEY, myPackageComponent.getText()); + String packageName = getPackageName(); + + String[] errorString = new String[1]; + CommandProcessor.getInstance().executeCommand(myProject, () -> { + try { + PackageWrapper targetPackage = new PackageWrapper(PsiManager.getInstance(myProject), packageName); + MoveDestination destination = myDestinationCB.selectDirectory(targetPackage, false); + if (destination == null) return; + myTargetDirectory = WriteAction.compute(() -> { + PsiDirectory baseDir = getBaseDir(packageName); + if (baseDir == null && destination instanceof MultipleRootsMoveDestination) { + errorString[0] = "Destination not found for package '" + packageName + "'"; + return null; + } + return destination.getTargetDirectory(baseDir); + }); + if (myTargetDirectory == null) { + return; + } + errorString[0] = RefactoringMessageUtil.checkCanCreateClass(myTargetDirectory, getClassName()); + } + catch (IncorrectOperationException e) { + errorString[0] = e.getMessage(); + } + }, CodeInsightBundle.message("create.directory.command"), null); + + if (errorString[0] != null) { + if (errorString[0].length() > 0) { + Messages.showMessageDialog(myProject, errorString[0], CommonBundle.getErrorTitle(), Messages.getErrorIcon()); + } + return; + } + super.doOKAction(); + } + + @Nullable + protected PsiDirectory getBaseDir(String packageName) { + return myModule == null? null : PackageUtil.findPossiblePackageDirectoryInModule(myModule, packageName); + } + + @NotNull + public String getClassName() { + if (myClassNameEditable) { + return myTfClassName.getText(); + } + else { + return myClassName; + } + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinDestinationFolderComboBox.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinDestinationFolderComboBox.java new file mode 100644 index 00000000000..29201f25c5e --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinDestinationFolderComboBox.java @@ -0,0 +1,328 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.ui; + +import com.intellij.ide.util.DirectoryChooser; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.event.DocumentListener; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ProjectFileIndex; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.ui.ComboBoxWithWidePopup; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Pass; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiManager; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.refactoring.MoveDestination; +import com.intellij.refactoring.PackageWrapper; +import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination; +import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil; +import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination; +import com.intellij.ui.*; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; + +import static org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt.getKotlinAwareDestinationSourceRoots; + +// Based on com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox +public abstract class KotlinDestinationFolderComboBox extends ComboboxWithBrowseButton { + private static final String LEAVE_IN_SAME_SOURCE_ROOT = "Leave in same source root"; + private static final DirectoryChooser.ItemWrapper NULL_WRAPPER = new DirectoryChooser.ItemWrapper(null, null); + private PsiDirectory myInitialTargetDirectory; + private List mySourceRoots; + + public KotlinDestinationFolderComboBox() { + super(new ComboBoxWithWidePopup()); + } + + public abstract String getTargetPackage(); + + protected boolean reportBaseInTestSelectionInSource() { + return false; + } + + protected boolean reportBaseInSourceSelectionInTest() { + return false; + } + + public void setData( + Project project, + PsiDirectory initialTargetDirectory, + EditorComboBox editorComboBox + ) { + setData(project, initialTargetDirectory, new Pass() { + @Override + public void pass(String s) { + } + }, editorComboBox); + } + + public void setData( + Project project, + PsiDirectory initialTargetDirectory, + Pass errorMessageUpdater, EditorComboBox editorComboBox + ) { + myInitialTargetDirectory = initialTargetDirectory; + mySourceRoots = getKotlinAwareDestinationSourceRoots(project); + new ComboboxSpeedSearch(getComboBox()) { + @Override + protected String getElementText(Object element) { + if (element == NULL_WRAPPER) return LEAVE_IN_SAME_SOURCE_ROOT; + if (element instanceof DirectoryChooser.ItemWrapper) { + VirtualFile virtualFile = ((DirectoryChooser.ItemWrapper) element).getDirectory().getVirtualFile(); + Module module = ModuleUtilCore.findModuleForFile(virtualFile, project); + if (module != null) { + return module.getName(); + } + } + return super.getElementText(element); + } + }; + ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex(); + getComboBox().setRenderer(new ListCellRendererWrapper() { + @Override + public void customize( + JList list, + DirectoryChooser.ItemWrapper itemWrapper, + int index, + boolean selected, + boolean hasFocus + ) { + if (itemWrapper != NULL_WRAPPER && itemWrapper != null) { + setIcon(itemWrapper.getIcon(fileIndex)); + + setText(itemWrapper.getRelativeToProjectPath()); + } + else { + setText(LEAVE_IN_SAME_SOURCE_ROOT); + } + } + }); + VirtualFile initialSourceRoot = + initialTargetDirectory != null ? fileIndex.getSourceRootForFile(initialTargetDirectory.getVirtualFile()) : null; + VirtualFile[] selection = new VirtualFile[] {initialSourceRoot}; + addActionListener(e -> { + VirtualFile root = MoveClassesOrPackagesUtil + .chooseSourceRoot(new PackageWrapper(PsiManager.getInstance(project), getTargetPackage()), mySourceRoots, + initialTargetDirectory); + if (root == null) return; + ComboBoxModel model = getComboBox().getModel(); + for (int i = 0; i < model.getSize(); i++) { + DirectoryChooser.ItemWrapper item = (DirectoryChooser.ItemWrapper) model.getElementAt(i); + if (item != NULL_WRAPPER && + Comparing.equal(fileIndex.getSourceRootForFile(item.getDirectory().getVirtualFile()), root)) { + getComboBox().setSelectedItem(item); + getComboBox().repaint(); + return; + } + } + setComboboxModel(getComboBox(), root, root, fileIndex, mySourceRoots, project, true, errorMessageUpdater); + }); + + editorComboBox.addDocumentListener(new DocumentListener() { + @Override + public void documentChanged(DocumentEvent e) { + JComboBox comboBox = getComboBox(); + DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) comboBox.getSelectedItem(); + setComboboxModel(comboBox, selectedItem != null && selectedItem != NULL_WRAPPER ? fileIndex + .getSourceRootForFile(selectedItem.getDirectory().getVirtualFile()) : initialSourceRoot, selection[0], fileIndex, + mySourceRoots, project, false, errorMessageUpdater); + } + }); + setComboboxModel(getComboBox(), initialSourceRoot, selection[0], fileIndex, mySourceRoots, project, false, errorMessageUpdater); + getComboBox().addActionListener(e -> { + Object selectedItem = getComboBox().getSelectedItem(); + updateErrorMessage(errorMessageUpdater, fileIndex, selectedItem); + if (selectedItem instanceof DirectoryChooser.ItemWrapper && selectedItem != NULL_WRAPPER) { + PsiDirectory directory = ((DirectoryChooser.ItemWrapper) selectedItem).getDirectory(); + if (directory != null) { + selection[0] = fileIndex.getSourceRootForFile(directory.getVirtualFile()); + } + } + }); + } + + @Nullable + public MoveDestination selectDirectory(PackageWrapper targetPackage, boolean showChooserWhenDefault) { + DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) getComboBox().getSelectedItem(); + if (selectedItem == null || selectedItem == NULL_WRAPPER) { + return new MultipleRootsMoveDestination(targetPackage); + } + PsiDirectory selectedPsiDirectory = selectedItem.getDirectory(); + VirtualFile selectedDestination = selectedPsiDirectory.getVirtualFile(); + if (showChooserWhenDefault && + myInitialTargetDirectory != null && Comparing.equal(selectedDestination, myInitialTargetDirectory.getVirtualFile()) && + mySourceRoots.size() > 1) { + selectedDestination = MoveClassesOrPackagesUtil.chooseSourceRoot(targetPackage, mySourceRoots, myInitialTargetDirectory); + } + if (selectedDestination == null) return null; + return new AutocreatingSingleSourceRootMoveDestination(targetPackage, selectedDestination); + } + + private void updateErrorMessage(Pass updateErrorMessage, ProjectFileIndex fileIndex, Object selectedItem) { + updateErrorMessage.pass(null); + if (myInitialTargetDirectory != null && selectedItem instanceof DirectoryChooser.ItemWrapper && selectedItem != NULL_WRAPPER) { + PsiDirectory directory = ((DirectoryChooser.ItemWrapper) selectedItem).getDirectory(); + boolean isSelectionInTestSourceContent = fileIndex.isInTestSourceContent(directory.getVirtualFile()); + boolean inTestSourceContent = fileIndex.isInTestSourceContent(myInitialTargetDirectory.getVirtualFile()); + if (isSelectionInTestSourceContent != inTestSourceContent) { + if (inTestSourceContent && reportBaseInTestSelectionInSource()) { + updateErrorMessage.pass("Source root is selected while the test root is expected"); + } + + if (isSelectionInTestSourceContent && reportBaseInSourceSelectionInTest()) { + updateErrorMessage.pass("Test root is selected while the source root is expected"); + } + } + } + } + + private void setComboboxModel( + JComboBox comboBox, + VirtualFile initialTargetDirectorySourceRoot, + VirtualFile oldSelection, + ProjectFileIndex fileIndex, + List sourceRoots, + Project project, + boolean forceIncludeAll, + Pass updateErrorMessage + ) { + LinkedHashSet targetDirectories = new LinkedHashSet<>(); + HashMap pathsToCreate = new HashMap<>(); + MoveClassesOrPackagesUtil + .buildDirectoryList(new PackageWrapper(PsiManager.getInstance(project), getTargetPackage()), sourceRoots, targetDirectories, + pathsToCreate); + if (!forceIncludeAll && targetDirectories.size() > pathsToCreate.size()) { + targetDirectories.removeAll(pathsToCreate.keySet()); + } + ArrayList items = new ArrayList<>(); + DirectoryChooser.ItemWrapper initial = null; + DirectoryChooser.ItemWrapper oldOne = null; + for (PsiDirectory targetDirectory : targetDirectories) { + DirectoryChooser.ItemWrapper itemWrapper = + new DirectoryChooser.ItemWrapper(targetDirectory, pathsToCreate.get(targetDirectory)); + items.add(itemWrapper); + VirtualFile sourceRootForFile = fileIndex.getSourceRootForFile(targetDirectory.getVirtualFile()); + if (Comparing.equal(sourceRootForFile, initialTargetDirectorySourceRoot)) { + initial = itemWrapper; + } + else if (Comparing.equal(sourceRootForFile, oldSelection)) { + oldOne = itemWrapper; + } + } + if (oldSelection == null || !fileIndex.isInLibrarySource(oldSelection)) { + items.add(NULL_WRAPPER); + } + DirectoryChooser.ItemWrapper selection = chooseSelection(initialTargetDirectorySourceRoot, fileIndex, items, initial, oldOne); + ComboBoxModel model = comboBox.getModel(); + if (model instanceof CollectionComboBoxModel) { + boolean sameModel = model.getSize() == items.size(); + if (sameModel) { + for (int i = 0; i < items.size(); i++) { + DirectoryChooser.ItemWrapper oldItem = (DirectoryChooser.ItemWrapper) model.getElementAt(i); + DirectoryChooser.ItemWrapper itemWrapper = items.get(i); + if (!areItemsEquivalent(oldItem, itemWrapper)) { + sameModel = false; + break; + } + } + } + if (sameModel) { + if (areItemsEquivalent((DirectoryChooser.ItemWrapper) comboBox.getSelectedItem(), selection)) { + return; + } + } + } + updateErrorMessage(updateErrorMessage, fileIndex, selection); + items.sort((o1, o2) -> { + if (o1 == NULL_WRAPPER) return -1; + if (o2 == NULL_WRAPPER) return 1; + return o1.getRelativeToProjectPath().compareToIgnoreCase(o2.getRelativeToProjectPath()); + }); + comboBox.setModel(new CollectionComboBoxModel(items, selection)); + + Component root = SwingUtilities.getRoot(comboBox); + if (root instanceof Window) { + Dimension preferredSize = root.getPreferredSize(); + if (preferredSize.getWidth() > root.getSize().getWidth()) { + root.setSize(preferredSize); + } + } + } + + @Nullable + private static DirectoryChooser.ItemWrapper chooseSelection( + VirtualFile initialTargetDirectorySourceRoot, + ProjectFileIndex fileIndex, + ArrayList items, + DirectoryChooser.ItemWrapper initial, + DirectoryChooser.ItemWrapper oldOne + ) { + if (initial != null || + ((initialTargetDirectorySourceRoot == null || items.size() > 2) && items.contains(NULL_WRAPPER)) || + items.isEmpty()) { + return initial; + } + else { + if (oldOne != null) { + return oldOne; + } + else if (initialTargetDirectorySourceRoot != null) { + boolean inTest = fileIndex.isInTestSourceContent(initialTargetDirectorySourceRoot); + for (DirectoryChooser.ItemWrapper item : items) { + PsiDirectory directory = item.getDirectory(); + if (directory != null) { + VirtualFile virtualFile = directory.getVirtualFile(); + if (fileIndex.isInTestSourceContent(virtualFile) == inTest) { + return item; + } + } + } + } + } + return items.get(0); + } + + private static boolean areItemsEquivalent(DirectoryChooser.ItemWrapper oItem, DirectoryChooser.ItemWrapper itemWrapper) { + if (oItem == NULL_WRAPPER || itemWrapper == NULL_WRAPPER) { + if (oItem != itemWrapper) { + return false; + } + return true; + } + if (oItem == null) return itemWrapper == null; + if (itemWrapper == null) return false; + if (oItem.getDirectory() != itemWrapper.getDirectory()) { + return false; + } + return true; + } + + public static boolean isAccessible( + Project project, + VirtualFile virtualFile, + VirtualFile targetVirtualFile + ) { + boolean inTestSourceContent = ProjectRootManager.getInstance(project).getFileIndex().isInTestSourceContent(virtualFile); + Module module = ModuleUtilCore.findModuleForFile(virtualFile, project); + if (targetVirtualFile != null && + module != null && + !GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, inTestSourceContent).contains(targetVirtualFile)) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/roots/projectRootUtils.kt b/idea/src/org/jetbrains/kotlin/idea/roots/projectRootUtils.kt index 20fd0c1b658..137b3e5d2e6 100644 --- a/idea/src/org/jetbrains/kotlin/idea/roots/projectRootUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/roots/projectRootUtils.kt @@ -5,10 +5,18 @@ package org.jetbrains.kotlin.idea.roots +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.rootManager +import com.intellij.openapi.roots.JavaProjectRootsUtil.isForGeneratedSources import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.jps.model.JpsElement import org.jetbrains.jps.model.ex.JpsElementBase +import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes import org.jetbrains.jps.model.java.JavaResourceRootType +import org.jetbrains.jps.model.java.JavaSourceRootProperties import org.jetbrains.jps.model.java.JavaSourceRootType import org.jetbrains.jps.model.module.JpsModuleSourceRoot import org.jetbrains.jps.model.module.JpsModuleSourceRootType @@ -41,3 +49,20 @@ fun migrateNonJvmSourceFolders(modifiableRootModel: ModifiableRootModel) { } } } + +fun getKotlinAwareDestinationSourceRoots(project: Project): List { + return ModuleManager.getInstance(project).modules.flatMap { it.collectKotlinAwareDestinationSourceRoots() } +} + +private val KOTLIN_AWARE_SOURCE_ROOT_TYPES: Set> = + JavaModuleSourceRootTypes.SOURCES + KotlinSourceRootType.ALL_SOURCES + +private fun Module.collectKotlinAwareDestinationSourceRoots(): List { + return rootManager + .contentEntries + .asSequence() + .flatMap { it.getSourceFolders(KOTLIN_AWARE_SOURCE_ROOT_TYPES).asSequence() } + .filterNot { isForGeneratedSources(it) } + .mapNotNull { it.file } + .toList() +} \ No newline at end of file