diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 2efc75dce8b6..95685817062c 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -1019,6 +1019,7 @@ formatter.imports.panel.sort.imports=Sort import statements formatter.imports.panel.sort.names.in.from.imports=Sort imported names in "from" imports formatter.imports.panel.join.from.imports.with.same.source=Join "from" imports with the same source formatter.imports.panel.sort.by.type=Sort plain and "from" imports separately within a group +formatter.imports.panel.sort.case.insensitively=Sort case-insensitively formatter.panel.dict.alignment.do.not.align=Do not align formatter.panel.dict.alignment.align.on.colon=Align on colon diff --git a/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java b/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java index 5bc8a40e55af..397aa95a2397 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/AddImportHelper.java @@ -53,8 +53,14 @@ public class AddImportHelper { return firstIsFromImport - secondIsFromImport; }; - private static final Comparator IMPORT_NAMES_COMPARATOR = - (import1, import2) -> ContainerUtil.compareLexicographically(getSortNames(import1), getSortNames(import2)); + @NotNull + private static Comparator getImportNamesComparator(@NotNull PyCodeStyleSettings settings) { + return (import1, import2) -> { + final Comparator stringComparator = + settings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER ? String.CASE_INSENSITIVE_ORDER : Comparator.naturalOrder(); + return ContainerUtil.compareLexicographically(getSortNames(import1), getSortNames(import2), Comparator.nullsFirst(stringComparator)); + }; + } @NotNull private static List getSortNames(@NotNull PyImportStatementBase importStatement) { @@ -93,10 +99,10 @@ public class AddImportHelper { public static Comparator getSameGroupImportsComparator(@NotNull PsiFile settingsAnchor) { final PyCodeStyleSettings settings = CodeStyle.getCustomSettings(settingsAnchor, PyCodeStyleSettings.class); if (settings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST) { - return IMPORT_TYPE_COMPARATOR.thenComparing(IMPORT_NAMES_COMPARATOR); + return IMPORT_TYPE_COMPARATOR.thenComparing(getImportNamesComparator(settings)); } else { - return IMPORT_NAMES_COMPARATOR.thenComparing(IMPORT_TYPE_COMPARATOR); + return getImportNamesComparator(settings).thenComparing(IMPORT_TYPE_COMPARATOR); } } diff --git a/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java b/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java index bc6559f8a519..10c14278bce7 100644 --- a/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java +++ b/python/src/com/jetbrains/python/codeInsight/imports/PyImportOptimizer.java @@ -30,6 +30,7 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.QualifiedName; import com.intellij.util.ObjectUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; @@ -90,14 +91,6 @@ public class PyImportOptimizer implements ImportOptimizer { } private static class ImportSorter { - private static final Comparator IMPORT_ELEMENT_COMPARATOR = (o1, o2) -> { - final int byImportedName = Comparing.compare(o1.getImportedQName(), o2.getImportedQName()); - if (byImportedName != 0) { - return byImportedName; - } - return Comparing.compare(o1.getAsName(), o2.getAsName()); - }; - private final PyFile myFile; private final PyCodeStyleSettings myPySettings; private final List myImportBlock; @@ -122,6 +115,16 @@ public class PyImportOptimizer implements ImportOptimizer { } } + @NotNull + private Comparator getFromNamesComparator() { + final Comparator stringComparator = + myPySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER ? String.CASE_INSENSITIVE_ORDER : Comparator.naturalOrder(); + final Comparator qNamesComparator = Comparator.comparing(QualifiedName::toString, stringComparator); + return Comparator + .comparing(PyImportElement::getImportedQName, Comparator.nullsFirst(qNamesComparator)) + .thenComparing(PyImportElement::getAsName, Comparator.nullsFirst(stringComparator)); + } + public void run() { if (myImportBlock.isEmpty()) { return; @@ -201,6 +204,7 @@ public class PyImportOptimizer implements ImportOptimizer { final List newStatementElements = new ArrayList<>(); boolean forceParentheses = false; + final Comparator fromNamesComparator = getFromNamesComparator(); // We can neither sort, nor combine star imports if (!fromImport.isStarImport()) { final Collection sameSourceImports = myOldFromImportBySources.get(source); @@ -220,7 +224,7 @@ public class PyImportOptimizer implements ImportOptimizer { } else if (myPySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS) { final List originalElements = Arrays.asList(fromImport.getImportElements()); - if (!Ordering.from(IMPORT_ELEMENT_COMPARATOR).isOrdered(originalElements)) { + if (!Ordering.from(fromNamesComparator).isOrdered(originalElements)) { ContainerUtil.addAll(newStatementElements, originalElements); } } @@ -228,7 +232,7 @@ public class PyImportOptimizer implements ImportOptimizer { if (!newStatementElements.isEmpty()) { if (myPySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS) { - Collections.sort(newStatementElements, IMPORT_ELEMENT_COMPARATOR); + Collections.sort(newStatementElements, fromNamesComparator); } String importedNames = StringUtil.join(newStatementElements, ImportSorter::getNormalizedImportElementText, ", "); if (forceParentheses) { diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index d844e5f448c7..475d8f4580f9 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -101,6 +101,7 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { public boolean OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = false; public boolean OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST = true; public boolean OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE = false; + public boolean OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = false; /** * Affects wrapping of multiple imported names in a single "from" import. diff --git a/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.form b/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.form index cef42eb07ca8..27c459864814 100644 --- a/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.form +++ b/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.form @@ -3,7 +3,7 @@ - + @@ -13,7 +13,7 @@ - + @@ -42,7 +42,7 @@ - + @@ -56,6 +56,15 @@ + + + + + + + + + diff --git a/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.java b/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.java index d16a9b2dc2c7..dde893f59f05 100644 --- a/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.java +++ b/python/src/com/jetbrains/python/formatter/PyImportsCodeStylePanel.java @@ -39,6 +39,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel { private JBCheckBox mySortImportsByTypeFirst; private JBCheckBox myJoinFromImportsWithSameSource; private JPanel myRootPanel; + private JBCheckBox mySortCaseInsensitively; public PyImportsCodeStylePanel(@NotNull CodeStyleSettings settings) { super(PythonLanguage.getInstance(), null, settings); @@ -48,6 +49,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel { final boolean sortingEnabled = mySortImports.isSelected(); mySortNamesInFromImports.setEnabled(sortingEnabled); mySortImportsByTypeFirst.setEnabled(sortingEnabled); + mySortCaseInsensitively.setEnabled(sortingEnabled); }); } @@ -87,6 +89,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel { pySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = mySortNamesInFromImports.isSelected(); pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST = mySortImportsByTypeFirst.isSelected(); pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE = myJoinFromImportsWithSameSource.isSelected(); + pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = mySortCaseInsensitively.isSelected(); } @Override @@ -96,7 +99,8 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel { return mySortImports.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_IMPORTS || mySortNamesInFromImports.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS || mySortImportsByTypeFirst.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST || - myJoinFromImportsWithSameSource.isSelected() != pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE; + myJoinFromImportsWithSameSource.isSelected() != pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE || + mySortCaseInsensitively.isSelected() != pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER; } @Nullable @@ -115,5 +119,6 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel { mySortImportsByTypeFirst.setSelected(pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST); mySortImportsByTypeFirst.setEnabled(mySortImports.isSelected()); myJoinFromImportsWithSameSource.setSelected(pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE); + mySortCaseInsensitively.setSelected(pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER); } } diff --git a/python/testData/optimizeImports/caseInsensitiveOrderOfImports.after.py b/python/testData/optimizeImports/caseInsensitiveOrderOfImports.after.py new file mode 100644 index 000000000000..fe543d935828 --- /dev/null +++ b/python/testData/optimizeImports/caseInsensitiveOrderOfImports.after.py @@ -0,0 +1,12 @@ +import mod_a +import mod_Z +# noinspection PyUnresolvedReferences +import pkg.pkg_a +# noinspection PyUnresolvedReferences +import pkg.pkg_Z +from mod import var_a +from mod import var_Z +from pkg.pkg_a import mod1 +from pkg.pkg_Z import mod2 + +print(mod_a, mod_Z, pkg.pkg_a, pkg.pkg_Z, mod1, mod2, var_a, var_Z) \ No newline at end of file diff --git a/python/testData/optimizeImports/caseInsensitiveOrderOfImports.py b/python/testData/optimizeImports/caseInsensitiveOrderOfImports.py new file mode 100644 index 000000000000..fe543d935828 --- /dev/null +++ b/python/testData/optimizeImports/caseInsensitiveOrderOfImports.py @@ -0,0 +1,12 @@ +import mod_a +import mod_Z +# noinspection PyUnresolvedReferences +import pkg.pkg_a +# noinspection PyUnresolvedReferences +import pkg.pkg_Z +from mod import var_a +from mod import var_Z +from pkg.pkg_a import mod1 +from pkg.pkg_Z import mod2 + +print(mod_a, mod_Z, pkg.pkg_a, pkg.pkg_Z, mod1, mod2, var_a, var_Z) \ No newline at end of file diff --git a/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.after.py b/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.after.py new file mode 100644 index 000000000000..ab4d22794930 --- /dev/null +++ b/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.after.py @@ -0,0 +1,3 @@ +from mod import var_a, var_Z + +print(var_a, var_Z) \ No newline at end of file diff --git a/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.py b/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.py new file mode 100644 index 000000000000..ab4d22794930 --- /dev/null +++ b/python/testData/optimizeImports/caseInsensitiveOrderOfNamesInsideFromImports.py @@ -0,0 +1,3 @@ +from mod import var_a, var_Z + +print(var_a, var_Z) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyOptimizeImportsTest.java b/python/testSrc/com/jetbrains/python/PyOptimizeImportsTest.java index 381ce754fc7a..bc716262c3c8 100644 --- a/python/testSrc/com/jetbrains/python/PyOptimizeImportsTest.java +++ b/python/testSrc/com/jetbrains/python/PyOptimizeImportsTest.java @@ -173,6 +173,19 @@ public class PyOptimizeImportsTest extends PyTestCase { doTest(); } + // PY-20159 + public void testCaseInsensitiveOrderOfImports() { + getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = true; + doTest(); + } + + // PY-20159 + public void testCaseInsensitiveOrderOfNamesInsideFromImports() { + getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = true; + getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = true; + doTest(); + } + // PY-19674 public void testUnresolvedRelativeImportsShouldBeInProjectGroup() { final String testName = getTestName(true);