diff --git a/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java index b73f5ff924911..44677564896eb 100644 --- a/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinCodeStyleSettings.java @@ -13,8 +13,6 @@ import com.intellij.openapi.util.WriteExternalException; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CustomCodeStyleSettings; -import com.intellij.psi.codeStyle.PackageEntry; -import com.intellij.psi.codeStyle.PackageEntryTable; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +24,8 @@ import static com.intellij.util.ReflectionUtil.copyFields; public class KotlinCodeStyleSettings extends CustomCodeStyleSettings { - public final PackageEntryTable PACKAGES_TO_USE_STAR_IMPORTS = new PackageEntryTable(); + public final KotlinPackageEntryTable PACKAGES_TO_USE_STAR_IMPORTS = new KotlinPackageEntryTable(); + public final KotlinPackageEntryTable PACKAGES_IMPORT_LAYOUT = new KotlinPackageEntryTable(); public boolean SPACE_AROUND_RANGE = false; public boolean SPACE_BEFORE_TYPE_COLON = false; public boolean SPACE_AFTER_TYPE_COLON = true; @@ -78,8 +77,14 @@ private KotlinCodeStyleSettings(CodeStyleSettings container, boolean isTempForDe // defaults in IDE but not in tests if (!ApplicationManager.getApplication().isUnitTestMode()) { - PACKAGES_TO_USE_STAR_IMPORTS.addEntry(new PackageEntry(false, "java.util", false)); - PACKAGES_TO_USE_STAR_IMPORTS.addEntry(new PackageEntry(false, "kotlinx.android.synthetic", true)); + PACKAGES_TO_USE_STAR_IMPORTS.addEntry(new KotlinPackageEntry("java.util", false)); + PACKAGES_TO_USE_STAR_IMPORTS.addEntry(new KotlinPackageEntry("kotlinx.android.synthetic", true)); + + PACKAGES_IMPORT_LAYOUT.addEntry(KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY); + PACKAGES_IMPORT_LAYOUT.addEntry(new KotlinPackageEntry("java", true)); + PACKAGES_IMPORT_LAYOUT.addEntry(new KotlinPackageEntry("javax", true)); + PACKAGES_IMPORT_LAYOUT.addEntry(new KotlinPackageEntry("kotlin", true)); + PACKAGES_IMPORT_LAYOUT.addEntry(KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY); } } @@ -104,6 +109,7 @@ public KotlinCodeStyleSettings cloneSettings() { private void copyFrom(@NotNull KotlinCodeStyleSettings from) { copyFields(getClass().getFields(), from, this); PACKAGES_TO_USE_STAR_IMPORTS.copyFrom(from.PACKAGES_TO_USE_STAR_IMPORTS); + PACKAGES_IMPORT_LAYOUT.copyFrom(from.PACKAGES_IMPORT_LAYOUT); } @Override diff --git a/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntry.kt b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntry.kt new file mode 100644 index 0000000000000..b46aedc101a66 --- /dev/null +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntry.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.core.formatter + +import org.jetbrains.kotlin.resolve.ImportPath + +data class KotlinPackageEntry( + val packageName: String, + val withSubpackages: Boolean +) { + companion object { + @JvmField + val ALL_OTHER_IMPORTS_ENTRY = KotlinPackageEntry("", withSubpackages = true) + + @JvmField + val ALL_OTHER_ALIAS_IMPORTS_ENTRY = KotlinPackageEntry("", withSubpackages = true) + } + + fun matchesPackageName(otherPackageName: String): Boolean { + if (this == ALL_OTHER_IMPORTS_ENTRY || this == ALL_OTHER_ALIAS_IMPORTS_ENTRY) return true + + if (otherPackageName.startsWith(packageName)) { + if (otherPackageName.length == packageName.length) return true + if (withSubpackages) { + if (otherPackageName[packageName.length] == '.') return true + } + } + return false + } + + fun isBetterMatchForPackageThan(entry: KotlinPackageEntry?, path: ImportPath): Boolean { + if (!matchesPackageName(path.pathStr)) return false + if (entry == null) return true + + if (this == ALL_OTHER_ALIAS_IMPORTS_ENTRY && path.hasAlias()) return true + if (entry == ALL_OTHER_ALIAS_IMPORTS_ENTRY && path.hasAlias()) return false + + if (entry == ALL_OTHER_IMPORTS_ENTRY && this != ALL_OTHER_ALIAS_IMPORTS_ENTRY) return true + if (this == ALL_OTHER_IMPORTS_ENTRY && entry != ALL_OTHER_ALIAS_IMPORTS_ENTRY) return false + if (entry.withSubpackages != withSubpackages) return !withSubpackages + + return entry.packageName.count { it == '.' } < packageName.count { it == '.' } + } + + val isSpecial: Boolean get() { + return (this == ALL_OTHER_IMPORTS_ENTRY || this == ALL_OTHER_ALIAS_IMPORTS_ENTRY) + } + + override fun toString(): String { + return packageName + } +} \ No newline at end of file diff --git a/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntryTable.kt b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntryTable.kt new file mode 100644 index 0000000000000..d0cfc4c6bde70 --- /dev/null +++ b/idea/formatter/src/org/jetbrains/kotlin/idea/core/formatter/KotlinPackageEntryTable.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.core.formatter + +import com.intellij.openapi.util.InvalidDataException +import com.intellij.openapi.util.JDOMExternalizable +import org.jdom.Element + +class KotlinPackageEntryTable: JDOMExternalizable, Cloneable { + private val entries = mutableListOf() + + override fun equals(other: Any?): Boolean { + if (other !is KotlinPackageEntryTable) return false + if (other.entries.size != entries.size) return false + + return entries.zip(other.entries).any { (entry, otherEntry) -> entry != otherEntry } + } + + public override fun clone(): KotlinPackageEntryTable { + val clone = KotlinPackageEntryTable() + clone.copyFrom(this) + return clone + } + + override fun hashCode(): Int { + return entries.firstOrNull()?.hashCode() ?: 0 + } + + fun copyFrom(packageTable: KotlinPackageEntryTable) { + entries.clear() + entries.addAll(packageTable.entries) + } + + fun getEntries(): Array { + return entries.toTypedArray() + } + + fun insertEntryAt(entry: KotlinPackageEntry, index: Int) { + entries.add(index, entry) + } + + fun removeEntryAt(index: Int) { + entries.removeAt(index) + } + + fun getEntryAt(index: Int): KotlinPackageEntry { + return entries[index] + } + + fun getEntryCount(): Int { + return entries.size + } + + fun setEntryAt(entry: KotlinPackageEntry, index: Int) { + entries[index] = entry + } + + operator fun contains(packageName: String): Boolean { + for (entry in entries) { + if (packageName.startsWith(entry.packageName)) { + if (packageName.length == entry.packageName.length) return true + if (entry.withSubpackages) { + if (packageName[entry.packageName.length] == '.') return true + } + } + } + return false + } + + fun removeEmptyPackages() { + entries.removeAll { it.packageName.isBlank() } + } + + fun addEntry(entry: KotlinPackageEntry) { + entries.add(entry) + } + + override fun readExternal(element: Element) { + entries.clear() + + element.children.forEach { + if (it.name == "package") { + val packageName = it.getAttributeValue("name") ?: throw InvalidDataException() + val alias = it.getAttributeValue("alias")?.toBoolean() ?: false + val withSubpackages = it.getAttributeValue("withSubpackages")?.toBoolean() ?: false + + val entry = when { + packageName.isEmpty() && !alias -> KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY + packageName.isEmpty() && alias -> KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY + else -> KotlinPackageEntry(packageName, withSubpackages) + } + + entries.add(entry) + } + } + } + + override fun writeExternal(parentNode: Element) { + for (entry in entries) { + val element = Element("package") + parentNode.addContent(element) + val name = if (entry == KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY || entry == KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY) "" else entry.packageName + val alias = (entry == KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY) + + element.setAttribute("name", name) + element.setAttribute("alias", alias.toString()) + element.setAttribute("withSubpackages", entry.withSubpackages.toString()) + } + } +} \ No newline at end of file diff --git a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/ImportsUtils.kt b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/ImportsUtils.kt index afc3d770f0ec5..26aea01d9ed89 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/ImportsUtils.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/ImportsUtils.kt @@ -27,36 +27,12 @@ import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.DescriptorUtils -import org.jetbrains.kotlin.resolve.ImportPath import org.jetbrains.kotlin.resolve.bindingContextUtil.getReferenceTargets import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.resolve.descriptorUtil.getImportableDescriptor import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.types.KotlinType -object ImportPathComparator : Comparator { - override fun compare(import1: ImportPath, import2: ImportPath): Int { - // alias imports placed last - if (import1.hasAlias() != import2.hasAlias()) { - return if (import1.hasAlias()) +1 else -1 - } - - // standard library imports last - val stdlib1 = isJavaOrKotlinStdlibImport(import1) - val stdlib2 = isJavaOrKotlinStdlibImport(import2) - if (stdlib1 != stdlib2) { - return if (stdlib1) +1 else -1 - } - - return import1.toString().compareTo(import2.toString()) - } - - private fun isJavaOrKotlinStdlibImport(path: ImportPath): Boolean { - val s = path.pathStr - return s.startsWith("java.") || s.startsWith("javax.") || s.startsWith("kotlin.") - } -} - val DeclarationDescriptor.importableFqName: FqName? get() { if (!canBeReferencedViaImport()) return null diff --git a/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/AbstractPerformanceImportTest.kt b/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/AbstractPerformanceImportTest.kt index 481fd6b45fef9..bc12e2d54c0be 100644 --- a/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/AbstractPerformanceImportTest.kt +++ b/idea/performanceTests/test/org/jetbrains/kotlin/idea/perf/AbstractPerformanceImportTest.kt @@ -14,6 +14,7 @@ import junit.framework.TestCase import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DeclarationDescriptor import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntry import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor import org.jetbrains.kotlin.idea.util.ImportInsertHelper @@ -61,10 +62,10 @@ abstract class AbstractPerformanceImportTest : KotlinLightCodeInsightFixtureTest InTextDirectivesUtils.getPrefixedBoolean(fileText, "// IMPORT_NESTED_CLASSES:") ?: false InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PACKAGE_TO_USE_STAR_IMPORTS:").forEach { - codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(PackageEntry(false, it.trim(), false)) + codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(KotlinPackageEntry(it.trim(), false)) } InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PACKAGES_TO_USE_STAR_IMPORTS:").forEach { - codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(PackageEntry(false, it.trim(), true)) + codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(KotlinPackageEntry(it.trim(), true)) } var descriptorName = InTextDirectivesUtils.findStringWithPrefixes(file.text, "// IMPORT:") diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/BaseKotlinImportLayoutPanel.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/BaseKotlinImportLayoutPanel.kt new file mode 100644 index 0000000000000..790d1a2a88da6 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/BaseKotlinImportLayoutPanel.kt @@ -0,0 +1,322 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.formatter + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonShortcuts +import com.intellij.openapi.actionSystem.ShortcutSet +import com.intellij.openapi.application.ApplicationBundle +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.ui.* +import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.layout.selected +import com.intellij.ui.table.JBTable +import com.intellij.util.IconUtil +import com.intellij.util.ui.JBUI +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntry +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntryTable +import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingColors +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.DefaultCellEditor +import javax.swing.JPanel +import javax.swing.JTable +import javax.swing.ListSelectionModel +import javax.swing.table.AbstractTableModel + +open class BaseKotlinImportLayoutPanel(title: String): JPanel(BorderLayout()) { + val packageTable = KotlinPackageEntryTable() + val layoutTable = createTableForPackageEntries(packageTable) + + init { + border = IdeBorderFactory.createTitledBorder( + title, + false, + JBUI.emptyInsets() + ) + } + + protected fun addPackage() { + var row = layoutTable.selectedRow + 1 + if (row < 0) { + row = packageTable.getEntryCount() + } + val entry = KotlinPackageEntry("", true) + packageTable.insertEntryAt(entry, row) + refreshTableModel(row) + } + + protected fun removePackage() { + var row = layoutTable.selectedRow + if (row < 0) return + + val entry = packageTable.getEntryAt(row) + if (entry == KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY || entry == KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY) { + return + } + + TableUtil.stopEditing(layoutTable) + packageTable.removeEntryAt(row) + + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsDeleted(row, row) + + if (row >= packageTable.getEntryCount()) { + row-- + } + + if (row >= 0) { + layoutTable.setRowSelectionInterval(row, row) + } + } + + protected fun movePackageUp() { + val row = layoutTable.selectedRow + if (row < 1) return + + TableUtil.stopEditing(layoutTable) + val entry = packageTable.getEntryAt(row) + val previousEntry = packageTable.getEntryAt(row - 1) + packageTable.setEntryAt(entry, row - 1) + packageTable.setEntryAt(previousEntry, row) + + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsUpdated(row - 1, row) + layoutTable.setRowSelectionInterval(row - 1, row - 1) + } + + protected fun movePackageDown() { + val row = layoutTable.selectedRow + if (row >= packageTable.getEntryCount() - 1) return + + TableUtil.stopEditing(layoutTable) + val entry = packageTable.getEntryAt(row) + val nextEntry = packageTable.getEntryAt(row + 1) + packageTable.setEntryAt(entry, row + 1) + packageTable.setEntryAt(nextEntry, row) + + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsUpdated(row, row + 1) + layoutTable.setRowSelectionInterval(row + 1, row + 1) + } + + private fun refreshTableModel(row: Int) { + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsInserted(row, row) + layoutTable.setRowSelectionInterval(row, row) + TableUtil.editCellAt(layoutTable, row, 0) + val editorComp = layoutTable.editorComponent + if (editorComp != null) { + IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown { IdeFocusManager.getGlobalInstance().requestFocus(editorComp, true) } + } + } + + protected fun resizeColumns() { + val packageRenderer: ColoredTableCellRenderer = object : ColoredTableCellRenderer() { + override fun customizeCellRenderer( + table: JTable, + value: Any?, + selected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int + ) { + val entry = packageTable.getEntryAt(row) + val attributes = KotlinHighlightingColors.KEYWORD.defaultAttributes + append("import", SimpleTextAttributes.fromTextAttributes(attributes)) + append(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES) + + when (entry) { + KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY -> append( + "all other imports", + SimpleTextAttributes.REGULAR_ATTRIBUTES + ) + + KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY -> append( + "all alias imports", + SimpleTextAttributes.REGULAR_ATTRIBUTES + ) + + else -> append( + "${entry.packageName}.*", + SimpleTextAttributes.REGULAR_ATTRIBUTES + ) + } + } + } + + layoutTable.columnModel.apply { + getColumn(0).cellRenderer = packageRenderer + getColumn(1).cellRenderer = BooleanTableCellRenderer() + + fixColumnWidthToHeader(1) + } + } + + private fun fixColumnWidthToHeader(columnIndex: Int) { + with (layoutTable) { + val column = columnModel.getColumn(columnIndex) + val width = 15 + tableHeader.getFontMetrics(tableHeader.font).stringWidth(getColumnName(columnIndex)) + + column.minWidth = width + column.maxWidth = width + } + } +} + +class KotlinStarImportLayoutPanel: BaseKotlinImportLayoutPanel(ApplicationBundle.message("title.packages.to.use.import.with")) { + init { + val importLayoutPanel = ToolbarDecorator.createDecorator(layoutTable) + .setAddAction { addPackage() } + .setRemoveAction { removePackage() } + .setButtonComparator( + "Add", + "Remove" + ).setPreferredSize(Dimension(-1, 100)) + .createPanel() + + add(importLayoutPanel, BorderLayout.CENTER) + resizeColumns() + } +} + +class KotlinImportOrderLayoutPanel: BaseKotlinImportLayoutPanel(ApplicationBundle.message("title.import.layout")) { + private val cbImportAliasesSeparately = JBCheckBox("Import aliases separately") + + init { + add(cbImportAliasesSeparately, BorderLayout.NORTH) + + val importLayoutPanel = ToolbarDecorator.createDecorator(layoutTable) + .addExtraAction( + object: DumbAwareActionButton(ApplicationBundle.message("button.add.package"), IconUtil.getAddPackageIcon()) { + override fun actionPerformed(event: AnActionEvent) { + addPackage() + } + + override fun getShortcut(): ShortcutSet { + return CommonShortcuts.getNewForDialogs() + } + } + ) + .setRemoveAction { removePackage() } + .setMoveUpAction { movePackageUp() } + .setMoveDownAction { movePackageDown() } + .setRemoveActionUpdater { + val selectedRow = layoutTable.selectedRow + val entry = if (selectedRow in 0 until packageTable.getEntryCount()) packageTable.getEntryAt(selectedRow) else null + + entry != null && entry != KotlinPackageEntry.ALL_OTHER_IMPORTS_ENTRY && entry != KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY + }.setButtonComparator( + ApplicationBundle.message("button.add.package"), + "Remove", + "Up", + "Down" + ).setPreferredSize(Dimension(-1, 100)) + .createPanel() + + add(importLayoutPanel, BorderLayout.CENTER) + resizeColumns() + + cbImportAliasesSeparately.addItemListener { _ -> + if (areImportAliasesEnabled()) { + if (packageTable.getEntries().none { it == KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY }) { + packageTable.addEntry(KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY) + val row = packageTable.getEntryCount() - 1 + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsInserted(row, row) + layoutTable.setRowSelectionInterval(row, row) + } + } else { + val entryIndex = packageTable.getEntries().indexOfFirst { it == KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY } + + if (entryIndex != -1) { + val currentIndex = layoutTable.selectedRow + packageTable.removeEntryAt(entryIndex) + val model = layoutTable.model as AbstractTableModel + model.fireTableRowsDeleted(entryIndex, entryIndex) + + if (currentIndex < entryIndex) { + layoutTable.setRowSelectionInterval(currentIndex, currentIndex) + } else if (entryIndex > 0) { + layoutTable.setRowSelectionInterval(entryIndex - 1, entryIndex - 1) + } + } + } + } + } + + fun recomputeAliasesCheckbox() { + cbImportAliasesSeparately.isSelected = packageTable.getEntries().any { it == KotlinPackageEntry.ALL_OTHER_ALIAS_IMPORTS_ENTRY } + } + + private fun areImportAliasesEnabled(): Boolean { + return cbImportAliasesSeparately.isSelected + } +} + +fun createTableForPackageEntries(packageTable: KotlinPackageEntryTable): JBTable { + val names = arrayOf(ApplicationBundle.message("listbox.import.package"), ApplicationBundle.message("listbox.import.with.subpackages")) + + val dataModel = object : AbstractTableModel() { + override fun getColumnCount(): Int { + return names.size + } + + override fun getRowCount(): Int { + return packageTable.getEntryCount() + } + + override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? { + val entry = packageTable.getEntryAt(rowIndex) + if (!isCellEditable(rowIndex, columnIndex)) return null + + return when (columnIndex) { + 0 -> entry.packageName + 1 -> entry.withSubpackages + else -> throw IllegalArgumentException(columnIndex.toString()) + } + } + + override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean { + val entry = packageTable.getEntryAt(rowIndex) + return !entry.isSpecial + } + + override fun getColumnName(column: Int): String { + return names[column] + } + + override fun getColumnClass(columnIndex: Int): Class<*> { + return when (columnIndex) { + 0 -> String::class.java + 1 -> Boolean::class.java + else -> throw IllegalArgumentException(columnIndex.toString()) + } + } + + override fun setValueAt(value: Any, rowIndex: Int, columnIndex: Int) { + val entry = packageTable.getEntryAt(rowIndex) + + val newEntry = when (columnIndex) { + 0 -> KotlinPackageEntry((value as String).trim(), entry.withSubpackages) + 1 -> KotlinPackageEntry(entry.packageName, (value as Boolean)) + else -> throw IllegalArgumentException(columnIndex.toString()) + } + + packageTable.setEntryAt(newEntry, rowIndex) + } + } + + // Create the table + val result = JBTable(dataModel) + result.selectionModel.selectionMode = ListSelectionModel.SINGLE_SELECTION + + val editor = result.getDefaultEditor(String::class.java) + if (editor is DefaultCellEditor) editor.clickCountToStart = 1 + + return result +} + diff --git a/idea/src/org/jetbrains/kotlin/idea/formatter/ImportSettingsPanel.kt b/idea/src/org/jetbrains/kotlin/idea/formatter/ImportSettingsPanel.kt index 79d732c571c81..6e0b60f83ed8b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/formatter/ImportSettingsPanel.kt +++ b/idea/src/org/jetbrains/kotlin/idea/formatter/ImportSettingsPanel.kt @@ -11,12 +11,12 @@ import com.intellij.application.options.PackagePanel import com.intellij.openapi.application.ApplicationBundle import com.intellij.openapi.editor.colors.EditorColorsScheme import com.intellij.psi.codeStyle.CodeStyleSettings -import com.intellij.psi.codeStyle.PackageEntryTable import com.intellij.ui.OptionGroup import com.intellij.ui.components.JBScrollPane import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntryTable import java.awt.BorderLayout import java.awt.GridBagConstraints import java.awt.GridBagLayout @@ -53,12 +53,8 @@ class ImportSettingsPanelWrapper(settings: CodeStyleSettings) : CodeStyleAbstrac class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPanel() { private val cbImportNestedClasses = JCheckBox(KotlinBundle.message("formatter.checkbox.text.insert.imports.for.nested.classes")) - private val starImportPackageEntryTable = PackageEntryTable() - private val dummyImportLayoutPanel = object : ImportLayoutPanel() { - override fun areStaticImportsEnabled() = false - override fun refresh() {} - } - private val starImportPackageTable = ImportLayoutPanel.createTableForPackageEntries(starImportPackageEntryTable, dummyImportLayoutPanel) + private val starImportLayoutPanel = KotlinStarImportLayoutPanel() + private val importOrderLayoutPanel = KotlinImportOrderLayoutPanel() private val nameCountToUseStarImportSelector = NameCountToUseStarImportSelector( KotlinBundle.message("formatter.title.top.level.symbols"), KotlinCodeStyleSettings.defaultSettings().NAME_COUNT_TO_USE_STAR_IMPORT @@ -88,7 +84,13 @@ class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPane KotlinBundle.message("formatter.title.other")).apply { add(cbImportNestedClasses) }.createPanel(), constraints.apply { gridy++ }) - add(PackagePanel.createPackagesPanel(starImportPackageTable, starImportPackageEntryTable), constraints.apply { + add(starImportLayoutPanel, constraints.apply { + gridy++ + fill = GridBagConstraints.BOTH + weighty = 1.0 + }) + + add(importOrderLayoutPanel, constraints.apply { gridy++ fill = GridBagConstraints.BOTH weighty = 1.0 @@ -102,18 +104,27 @@ class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPane cbImportNestedClasses.isSelected = settings.IMPORT_NESTED_CLASSES - starImportPackageEntryTable.copyFrom(settings.PACKAGES_TO_USE_STAR_IMPORTS) - (starImportPackageTable.model as AbstractTableModel).fireTableDataChanged() - if (starImportPackageTable.rowCount > 0) { - starImportPackageTable.selectionModel.setSelectionInterval(0, 0) + starImportLayoutPanel.packageTable.copyFrom(settings.PACKAGES_TO_USE_STAR_IMPORTS) + (starImportLayoutPanel.layoutTable.model as AbstractTableModel).fireTableDataChanged() + if (starImportLayoutPanel.layoutTable.rowCount > 0) { + starImportLayoutPanel.layoutTable.selectionModel.setSelectionInterval(0, 0) + } + + importOrderLayoutPanel.packageTable.copyFrom(settings.PACKAGES_IMPORT_LAYOUT) + (importOrderLayoutPanel.layoutTable.model as AbstractTableModel).fireTableDataChanged() + if (importOrderLayoutPanel.layoutTable.rowCount > 0) { + importOrderLayoutPanel.layoutTable.selectionModel.setSelectionInterval(0, 0) } + + importOrderLayoutPanel.recomputeAliasesCheckbox() } fun apply(settings: KotlinCodeStyleSettings) { settings.NAME_COUNT_TO_USE_STAR_IMPORT = nameCountToUseStarImportSelector.value settings.NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS = nameCountToUseStarImportForMembersSelector.value settings.IMPORT_NESTED_CLASSES = cbImportNestedClasses.isSelected - settings.PACKAGES_TO_USE_STAR_IMPORTS.copyFrom(getCopyWithoutEmptyPackages(starImportPackageEntryTable)) + settings.PACKAGES_TO_USE_STAR_IMPORTS.copyFrom(getCopyWithoutEmptyPackages(starImportLayoutPanel.packageTable)) + settings.PACKAGES_IMPORT_LAYOUT.copyFrom(importOrderLayoutPanel.packageTable) } fun isModified(settings: KotlinCodeStyleSettings): Boolean { @@ -122,7 +133,8 @@ class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPane isModified = isModified || nameCountToUseStarImportSelector.value != NAME_COUNT_TO_USE_STAR_IMPORT isModified = isModified || nameCountToUseStarImportForMembersSelector.value != NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS isModified = isModified || isModified(cbImportNestedClasses, IMPORT_NESTED_CLASSES) - isModified = isModified || isModified(getCopyWithoutEmptyPackages(starImportPackageEntryTable), PACKAGES_TO_USE_STAR_IMPORTS) + isModified = isModified || isModified(getCopyWithoutEmptyPackages(starImportLayoutPanel.packageTable), PACKAGES_TO_USE_STAR_IMPORTS) + isModified = isModified || isModified(importOrderLayoutPanel.packageTable, PACKAGES_IMPORT_LAYOUT) isModified } @@ -133,12 +145,12 @@ class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPane return checkBox.isSelected != value } - private fun isModified(list: PackageEntryTable, table: PackageEntryTable): Boolean { - if (list.entryCount != table.entryCount) { + private fun isModified(list: KotlinPackageEntryTable, table: KotlinPackageEntryTable): Boolean { + if (list.getEntryCount() != table.getEntryCount()) { return true } - for (i in 0 until list.entryCount) { + for (i in 0 until list.getEntryCount()) { val entry1 = list.getEntryAt(i) val entry2 = table.getEntryAt(i) if (entry1 != entry2) { @@ -149,9 +161,9 @@ class ImportSettingsPanel(private val commonSettings: CodeStyleSettings) : JPane return false } - private fun getCopyWithoutEmptyPackages(table: PackageEntryTable): PackageEntryTable { + private fun getCopyWithoutEmptyPackages(table: KotlinPackageEntryTable): KotlinPackageEntryTable { try { - val copy = table.clone() as PackageEntryTable + val copy = table.clone() copy.removeEmptyPackages() return copy } catch (ignored: CloneNotSupportedException) { diff --git a/idea/src/org/jetbrains/kotlin/idea/util/ImportInsertHelperImpl.kt b/idea/src/org/jetbrains/kotlin/idea/util/ImportInsertHelperImpl.kt index d5e21cdb91066..dfc6500ec50ac 100644 --- a/idea/src/org/jetbrains/kotlin/idea/util/ImportInsertHelperImpl.kt +++ b/idea/src/org/jetbrains/kotlin/idea/util/ImportInsertHelperImpl.kt @@ -12,7 +12,6 @@ import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings import org.jetbrains.kotlin.idea.core.targetDescriptors -import org.jetbrains.kotlin.idea.imports.ImportPathComparator import org.jetbrains.kotlin.idea.imports.getImportableTargets import org.jetbrains.kotlin.idea.imports.importableFqName import org.jetbrains.kotlin.idea.project.TargetPlatformDetector @@ -45,7 +44,7 @@ class ImportInsertHelperImpl(private val project: Project) : ImportInsertHelper( get() = KotlinCodeStyleSettings.getInstance(project) override val importSortComparator: Comparator - get() = ImportPathComparator + get() = ImportPathComparator(codeStyleSettings.PACKAGES_IMPORT_LAYOUT) override fun isImportedWithDefault(importPath: ImportPath, contextFile: KtFile): Boolean { val languageVersionSettings = contextFile.getResolutionFacade().frontendService() @@ -409,10 +408,11 @@ class ImportInsertHelperImpl(private val project: Project) : ImportInsertHelper( importList.add(psiFactory.createNewLine()) importList.add(newDirective) as KtImportDirective } else { + val importPathComparator = ImportInsertHelperImpl(project).importSortComparator val insertAfter = imports .lastOrNull { val directivePath = it.importPath - directivePath != null && ImportPathComparator.compare(directivePath, importPath) <= 0 + directivePath != null && importPathComparator.compare(directivePath, importPath) <= 0 } importList.addAfter(newDirective, insertAfter) as KtImportDirective } diff --git a/idea/src/org/jetbrains/kotlin/idea/util/ImportPathComparator.kt b/idea/src/org/jetbrains/kotlin/idea/util/ImportPathComparator.kt new file mode 100644 index 0000000000000..65d73090741b0 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/util/ImportPathComparator.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * 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.util + +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntry +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntryTable +import org.jetbrains.kotlin.resolve.ImportPath +import java.util.Comparator + +class ImportPathComparator( + private val packageTable: KotlinPackageEntryTable +) : Comparator { + override fun compare(import1: ImportPath, import2: ImportPath): Int { + val index1 = bestMatchIndex(import1) + val index2 = bestMatchIndex(import2) + + return when { + index1 == index2 -> import1.toString().compareTo(import2.toString()) + index1 < index2 -> -1 + else -> +1 + } + } + + private fun bestMatchIndex(path: ImportPath): Int { + var bestIndex: Int? = null + var bestEntryMatch: KotlinPackageEntry? = null + + for ((index, entry) in packageTable.getEntries().withIndex()) { + if (entry.isBetterMatchForPackageThan(bestEntryMatch, path)) { + bestEntryMatch = entry + bestIndex = index + } + } + + return bestIndex!! + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/AbstractImportsTest.kt b/idea/tests/org/jetbrains/kotlin/AbstractImportsTest.kt index c95afe57b93b5..fa09e14be5204 100644 --- a/idea/tests/org/jetbrains/kotlin/AbstractImportsTest.kt +++ b/idea/tests/org/jetbrains/kotlin/AbstractImportsTest.kt @@ -6,10 +6,10 @@ package org.jetbrains.kotlin import com.intellij.application.options.CodeStyle -import com.intellij.psi.codeStyle.PackageEntry import com.intellij.testFramework.LightProjectDescriptor import junit.framework.TestCase import org.jetbrains.kotlin.idea.core.formatter.KotlinCodeStyleSettings +import org.jetbrains.kotlin.idea.core.formatter.KotlinPackageEntry import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor @@ -56,10 +56,10 @@ abstract class AbstractImportsTest : KotlinLightCodeInsightFixtureTestCase() { InTextDirectivesUtils.getPrefixedBoolean(fileText, "// IMPORT_NESTED_CLASSES:") ?: false InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PACKAGE_TO_USE_STAR_IMPORTS:").forEach { - codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(PackageEntry(false, it.trim(), false)) + codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(KotlinPackageEntry(it.trim(), false)) } InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, "// PACKAGES_TO_USE_STAR_IMPORTS:").forEach { - codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(PackageEntry(false, it.trim(), true)) + codeStyleSettings.PACKAGES_TO_USE_STAR_IMPORTS.addEntry(KotlinPackageEntry(it.trim(), true)) } val log = project.executeWriteCommand("") { doTest(file) }