diff --git a/community-main.iml b/community-main.iml
index b22a21650bef..85186028f6d8 100644
--- a/community-main.iml
+++ b/community-main.iml
@@ -124,5 +124,6 @@
+
\ No newline at end of file
diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessEvents.java b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessEvents.java
index 84ce91a7285a..91fca2cb05c8 100644
--- a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessEvents.java
+++ b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessEvents.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2016 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
@@ -355,7 +355,9 @@ public class DebugProcessEvents extends DebugProcessImpl {
private void createStackCapturingBreakpoints() {
if (Registry.is("debugger.capture.points")) {
for (CapturePoint point : DebuggerSettings.getInstance().getCapturePoints()) {
- StackCapturingLineBreakpoint.track(this, point.myClassName, point.myMethodName, null, point.myParamNo);
+ if (point.myEnabled) {
+ StackCapturingLineBreakpoint.track(this, point.myClassName, point.myMethodName, null, point.myParamNo);
+ }
}
}
}
diff --git a/java/java-impl/src/com/intellij/refactoring/makeStatic/MakeStaticJavaCallerChooser.java b/java/java-impl/src/com/intellij/refactoring/makeStatic/MakeStaticJavaCallerChooser.java
index a82e6b2d8623..0ff4714084d8 100644
--- a/java/java-impl/src/com/intellij/refactoring/makeStatic/MakeStaticJavaCallerChooser.java
+++ b/java/java-impl/src/com/intellij/refactoring/makeStatic/MakeStaticJavaCallerChooser.java
@@ -22,7 +22,9 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.search.searches.OverridingMethodsSearch;
+import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.util.PsiUtil;
import com.intellij.refactoring.changeSignature.MethodNodeBase;
import com.intellij.refactoring.changeSignature.inCallers.JavaCallerChooser;
import com.intellij.refactoring.changeSignature.inCallers.JavaMethodNode;
@@ -53,8 +55,7 @@ abstract class MakeStaticJavaCallerChooser extends JavaCallerChooser {
OverridingMethodsSearch.search(containingMethod).findFirst() == null) {
final PsiClass containingClass = containingMethod.getContainingClass();
if (containingClass != null) {
- final PsiClass gContainingClass = containingClass.getContainingClass();
- if (gContainingClass == null || gContainingClass.hasModifierProperty(PsiModifier.STATIC)) {
+ if (ClassUtil.isTopLevelClass(containingClass) || containingClass.hasModifierProperty(PsiModifier.STATIC)) {
final InternalUsageInfo[] refsInMember = MakeStaticUtil.findClassRefsInMember(containingMethod, true);
for (InternalUsageInfo info : refsInMember) {
final PsiElement referencedElement = info.getReferencedElement();
diff --git a/java/java-tests/testData/refactoring/makeMethodStatic/afterDeep.java b/java/java-tests/testData/refactoring/makeMethodStatic/afterDeep.java
index 40d65f21d627..185063cf39a3 100644
--- a/java/java-tests/testData/refactoring/makeMethodStatic/afterDeep.java
+++ b/java/java-tests/testData/refactoring/makeMethodStatic/afterDeep.java
@@ -8,4 +8,13 @@ class P {
bazz(k);
bazz(k);
}
+
+ void m(int k) {
+ new Runnable() {
+ public void run() {}
+ void mm() {
+ bazz(k);
+ }
+ };
+ }
}
\ No newline at end of file
diff --git a/java/java-tests/testData/refactoring/makeMethodStatic/beforeDeep.java b/java/java-tests/testData/refactoring/makeMethodStatic/beforeDeep.java
index 2a62cc60aebb..baa8efdd1293 100644
--- a/java/java-tests/testData/refactoring/makeMethodStatic/beforeDeep.java
+++ b/java/java-tests/testData/refactoring/makeMethodStatic/beforeDeep.java
@@ -8,4 +8,13 @@ class P {
bazz(k);
bazz(k);
}
+
+ void m(int k) {
+ new Runnable() {
+ public void run() {}
+ void mm() {
+ bazz(k);
+ }
+ };
+ }
}
\ No newline at end of file
diff --git a/platform/build-scripts/icons/src/com/intellij/build/scripts/IconsClassGenerator.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/IconsClassGenerator.kt
new file mode 100644
index 000000000000..a704cfc258c2
--- /dev/null
+++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/IconsClassGenerator.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.util.containers.ContainerUtil
+import org.jetbrains.jps.model.JpsSimpleElement
+import org.jetbrains.jps.model.java.JavaSourceRootProperties
+import org.jetbrains.jps.model.java.JavaSourceRootType
+import org.jetbrains.jps.model.module.JpsModule
+import org.jetbrains.jps.util.JpsPathUtil
+import java.io.BufferedReader
+import java.io.File
+import java.io.StringReader
+import java.util.*
+import java.util.function.Function
+
+class IconsClassGenerator(val projectHome: File, val util: JpsModule) {
+ fun processModule(module: JpsModule) {
+ val customLoad: Boolean
+ val packageName: String
+ val className: String
+ val outFile: File
+ if ("icons" == module.name) {
+ customLoad = false
+ packageName = "com.intellij.icons"
+ className = "AllIcons"
+
+ val dir = util.getSourceRoots(JavaSourceRootType.SOURCE).first().file.absolutePath + "/com/intellij/icons"
+ outFile = File(dir, "AllIcons.java")
+ }
+ else {
+ customLoad = true
+ packageName = "icons"
+
+ val firstRoot = module.getSourceRoots(JavaSourceRootType.SOURCE).firstOrNull()
+ if (firstRoot == null) return
+
+ val generatedRoot = module.getSourceRoots(JavaSourceRootType.SOURCE).find { it.properties.isForGeneratedSources }
+ val targetRoot = File((generatedRoot ?: firstRoot).file, "icons")
+
+ val firstRootDir = File(firstRoot.file, "icons")
+ if (firstRootDir.isDirectory && firstRootDir.list().isEmpty()) {
+ //this is added to remove unneeded empty directories created by previous version of this script
+ println("deleting empty directory ${firstRootDir.absolutePath}")
+ firstRootDir.delete()
+ }
+
+ var oldClassName = findIconClass(firstRootDir)
+ if (generatedRoot != null && oldClassName != null) {
+ val oldFile = File(firstRootDir, "${oldClassName}.java")
+ println("deleting $oldFile from source root which isn't marked as 'generated'")
+ oldFile.delete()
+ }
+ if (oldClassName == null) {
+ oldClassName = findIconClass(targetRoot)
+ }
+
+ className = oldClassName ?: directoryName(module) + "Icons"
+ outFile = File(targetRoot, "${className}.java")
+ }
+
+ val copyrightComment = getCopyrightComment(outFile)
+ val text = generate(module, className, packageName, customLoad, copyrightComment)
+ if (text != null) {
+ if (!outFile.exists() || !sameLines(outFile.readText(), text)) {
+ outFile.parentFile.mkdirs()
+ outFile.writeText(text)
+ println("Updated icons class: ${outFile.name}")
+ }
+ }
+ }
+
+ private fun findIconClass(dir: File): String? {
+ var className: String? = null
+ dir.children.forEach {
+ if (it.name.endsWith("Icons.java")) {
+ className = it.name.substring(0, it.name.length - ".java".length)
+ }
+ }
+ return className
+ }
+
+ private fun getCopyrightComment(file: File): String {
+ if (!file.isFile) return ""
+ val text = file.readText()
+ val i = text.indexOf("package ")
+ if (i == -1) return ""
+ val comment = text.substring(0, i)
+ return if (comment.trim().endsWith("*/")) comment else ""
+ }
+
+ private fun sameLines(a: String, b: String): Boolean {
+ val ra = BufferedReader(StringReader(a))
+ val rb = BufferedReader(StringReader(b))
+
+ while (true) {
+ val la = ra.readLine()
+ val lb = rb.readLine()
+ if (la != lb) return false
+ if (la == null) return true
+ }
+ }
+
+ private fun generate(module: JpsModule, className: String, packageName: String, customLoad: Boolean, copyrightComment: String): String? {
+ val answer = StringBuilder()
+ answer.append(copyrightComment)
+ append(answer, "package $packageName;\n", 0)
+ append(answer, "import com.intellij.openapi.util.IconLoader;", 0)
+ append(answer, "", 0)
+ append(answer, "import javax.swing.*;", 0)
+ append(answer, "", 0)
+
+ // IconsGeneratedSourcesFilter depends on following comment, if you going to change the text
+ // please do corresponding changes in IconsGeneratedSourcesFilter as well
+ append(answer, "/**", 0)
+ append(answer, " * NOTE THIS FILE IS AUTO-GENERATED", 0)
+ append(answer, " * DO NOT EDIT IT BY HAND, run build/scripts/icons.gant instead", 0)
+ append(answer, " */", 0)
+
+ append(answer, "public class $className {", 0)
+ if (customLoad) {
+ append(answer, "private static Icon load(String path) {", 1)
+ append(answer, "return IconLoader.getIcon(path, ${className}.class);", 2)
+ append(answer, "}", 1)
+ append(answer, "", 0)
+ }
+
+ val imageCollector = ImageCollector(projectHome, true)
+ val images = imageCollector.collect(module)
+ imageCollector.printUsedIconRobots()
+
+ val inners = StringBuilder()
+ processIcons(images, inners, customLoad, 0)
+ if (inners.isEmpty()) return null
+
+ answer.append(inners)
+ append(answer, "}", 0)
+ return answer.toString()
+ }
+
+ private fun processIcons(images: List, answer: StringBuilder, customLoad: Boolean, depth: Int) {
+ val level = depth + 1
+
+ val (nodes, leafs) = images.partition { getImageId(it, depth).contains('/') }
+ val nodeMap = nodes.groupBy { getImageId(it, depth).substringBefore('/') }
+ val leafMap = ContainerUtil.newMapFromValues(leafs.iterator(), { getImageId(it, depth) })
+
+ val sortedKeys = (nodeMap.keys + leafMap.keys).sortedWith(NAME_COMPARATOR)
+ sortedKeys.forEach { key ->
+ val group = nodeMap[key]
+ val image = leafMap[key]
+ assert(group == null || image == null)
+
+ if (group != null) {
+ val inners = StringBuilder()
+ processIcons(group, inners, customLoad, depth + 1)
+
+ if (inners.isNotEmpty()) {
+ append(answer, "", level)
+ append(answer, "public static class " + className(key) + " {", level)
+ append(answer, inners.toString(), 0)
+ append(answer, "}", level)
+ }
+ }
+
+ if (image != null) {
+ val file = image.file
+ if (file != null) {
+ val name = file.name
+ val used = image.used
+ val deprecated = image.deprecated
+
+ if (isIcon(file)) {
+ if (used || deprecated) {
+ append(answer, "", level)
+ append(answer, "@SuppressWarnings(\"unused\")", level)
+ }
+ if (deprecated) {
+ append(answer, "@Deprecated", level)
+ }
+
+ val sourceRoot = image.sourceRoot
+ var root_prefix: String = ""
+ if (sourceRoot.rootType == JavaSourceRootType.SOURCE) {
+ @Suppress("UNCHECKED_CAST")
+ val packagePrefix = (sourceRoot.properties as JpsSimpleElement).data.packagePrefix
+ if (!packagePrefix.isEmpty()) root_prefix = "/" + packagePrefix.replace('.', '/')
+ }
+
+ val size = imageSize(file)
+ val method = if (customLoad) "load" else "IconLoader.getIcon"
+ val relativePath = root_prefix + "/" + FileUtil.getRelativePath(sourceRoot.file, file)!!.replace('\\', '/')
+ append(answer,
+ "public static final Icon ${iconName(name)} = $method(\"$relativePath\"); // ${size.width}x${size.height}",
+ level)
+ }
+ }
+ }
+ }
+ }
+
+ private fun append(answer: StringBuilder, text: String, level: Int) {
+ answer.append(" ".repeat(level))
+ answer.append(text).append("\n")
+ }
+
+ private fun getImageId(image: ImagePaths, depth: Int): String {
+ val path = StringUtil.trimStart(image.id, "/").split("/")
+ if (path.size < depth) throw IllegalArgumentException("Can't get image ID - ${image.id}, $depth")
+ return path.drop(depth).joinToString("/")
+ }
+
+ private fun directoryName(module: JpsModule): String {
+ return directoryNameFromConfig(module) ?: className(module.name)
+ }
+
+ private fun directoryNameFromConfig(module: JpsModule): String? {
+ val rootUrl = getFirstContentRootUrl(module) ?: return null
+ val rootDir = File(JpsPathUtil.urlToPath(rootUrl))
+ if (!rootDir.isDirectory) return null
+
+ val file = File(rootDir, "icon-robots.txt")
+ if (!file.exists()) return null
+
+ val prefix = "name:"
+ var moduleName: String? = null
+ file.forEachLine {
+ if (it.startsWith(prefix)) {
+ val name = it.substring(prefix.length).trim()
+ if (name.isNotEmpty()) moduleName = name
+ }
+ }
+ return moduleName
+ }
+
+ private fun getFirstContentRootUrl(module: JpsModule): String? {
+ return module.contentRootsList.urls.firstOrNull()
+ }
+
+ private fun className(name: String): String {
+ val answer = StringBuilder()
+ name.split("-", "_").forEach {
+ answer.append(capitalize(it))
+ }
+ return toJavaIdentifier(answer.toString())
+ }
+
+ private fun iconName(name: String): String {
+ val id = capitalize(name.substring(0, name.lastIndexOf('.')))
+ return toJavaIdentifier(id)
+ }
+
+ private fun toJavaIdentifier(id: String): String {
+ val sb = StringBuilder()
+ id.forEach {
+ if (Character.isJavaIdentifierPart(it)) {
+ sb.append(it)
+ }
+ else {
+ sb.append('_')
+ }
+ }
+
+ if (Character.isJavaIdentifierStart(sb.first())) {
+ return sb.toString()
+ }
+ else {
+ return "_" + sb.toString()
+ }
+ }
+
+ private fun capitalize(name: String): String {
+ if (name.length == 2) return name.toUpperCase()
+ return name.capitalize()
+ }
+
+ // legacy ordering
+ private val NAME_COMPARATOR: Comparator = Comparator.comparing(Function { it.toLowerCase() + "." })
+}
diff --git a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt
new file mode 100644
index 000000000000..7b3ad9825f20
--- /dev/null
+++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.openapi.util.io.FileUtil
+import com.intellij.openapi.util.io.FileUtilRt
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.util.containers.ContainerUtil
+import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes
+import org.jetbrains.jps.model.module.JpsModule
+import org.jetbrains.jps.model.module.JpsModuleSourceRoot
+import java.io.File
+import java.util.*
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+internal class ImagePaths(val id: String, val sourceRoot: JpsModuleSourceRoot, val used: Boolean, val deprecated: Boolean) {
+ var files: MutableMap = HashMap()
+ var ambiguous: Boolean = false
+
+ val file: File? get() = files[ImageType.BASIC]
+ val presentablePath: File get() = file ?: files.values.first() ?: File("")
+}
+
+internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = true, val ignoreSkipTag: Boolean = false) {
+ private val result = HashMap ()
+
+ private val usedIconsRobots: MutableSet = HashSet()
+
+ fun collect(module: JpsModule): List {
+ module.sourceRoots.forEach {
+ processRoot(it)
+ }
+ return result.values.toList()
+ }
+
+ fun printUsedIconRobots() {
+ usedIconsRobots.forEach {
+ println("Found icon-robots: $it")
+ }
+ }
+
+ private fun processRoot(sourceRoot: JpsModuleSourceRoot) {
+ val root = sourceRoot.file
+ if (!root.exists()) return
+ if (!JavaModuleSourceRootTypes.PRODUCTION.contains(sourceRoot.rootType)) return
+
+ val iconsRoot = downToRoot(root)
+ if (iconsRoot == null) return
+
+ val rootRobotData = upToProjectHome(root)
+ if (rootRobotData.isSkipped(root)) return
+
+ val robotData = rootRobotData.fork(iconsRoot, root)
+
+ processDirectory(iconsRoot, sourceRoot, robotData, emptyList())
+ }
+
+ private fun processDirectory(dir: File, sourceRoot: JpsModuleSourceRoot, robotData: IconRobotsData, prefix: List) {
+ dir.children.forEach { file ->
+ if (file.isDirectory) {
+ val root = sourceRoot.file
+ val childRobotData = robotData.fork(file, root)
+ val childPrefix = ContainerUtil.append(prefix, file.name)
+ processDirectory(file, sourceRoot, childRobotData, childPrefix)
+ }
+ else if (isImage(file, iconsOnly)) {
+ processImageFile(file, sourceRoot, robotData, prefix)
+ }
+ }
+ }
+
+ private fun processImageFile(file: File, sourceRoot: JpsModuleSourceRoot, robotData: IconRobotsData, prefix: List) {
+ val nameWithoutExtension = FileUtil.getNameWithoutExtension(file.name)
+ val type = ImageType.fromName(nameWithoutExtension)
+ val id = type.getBasicName((prefix + nameWithoutExtension).joinToString("/"))
+
+ val skipped = robotData.isSkipped(file)
+ val used = robotData.isUsed(file)
+ val deprecated = robotData.isDeprecated(file)
+ if (skipped) return
+
+ val iconPaths = result.computeIfAbsent(id, { ImagePaths(id, sourceRoot, used, deprecated) })
+ if (iconPaths.files[type] == null) {
+ iconPaths.files[type] = file
+ }
+ else {
+ iconPaths.ambiguous = true
+ }
+ }
+
+ private fun upToProjectHome(dir: File): IconRobotsData {
+ if (dir == projectHome) return IconRobotsData()
+ val parent = dir.parentFile ?: return IconRobotsData()
+ return upToProjectHome(parent).fork(parent, projectHome)
+ }
+
+ private fun downToRoot(dir: File): File? {
+ val answer = downToRoot(dir, dir, null, IconRobotsData())
+ return if (answer == null || answer.isDirectory) answer else answer.parentFile
+ }
+
+ private fun downToRoot(root: File, file: File, common: File?, robotData: IconRobotsData): File? {
+ if (robotData.isSkipped(file)) return common
+
+ if (file.isDirectory) {
+ val childRobotData = robotData.fork(file, root)
+
+ var childCommon = common
+ file.children.forEach {
+ childCommon = downToRoot(root, it, childCommon, childRobotData)
+ }
+ return childCommon
+ }
+ else if (isImage(file, iconsOnly)) {
+ return getCommonAncestor(common, file)
+ }
+ else {
+ return common
+ }
+ }
+
+ private fun getCommonAncestor(file1: File?, file2: File?): File? {
+ if (file1 == null) return file2
+ if (file2 == null) return file1
+
+ val c1 = pathComponents(file1)
+ val c2 = pathComponents(file2)
+
+ val size = Math.min(c1.size, c2.size)
+ var cur: File? = null
+ for (i in 0..size - 1) {
+ if (c1[i] != c2[i]) break
+ cur = c1[i]
+ }
+ return cur
+ }
+
+ private fun pathComponents(file: File): List {
+ val answer = ArrayList()
+ var cur: File? = file
+ while (cur != null) {
+ answer.add(cur)
+ cur = cur.parentFile
+ }
+ return answer.reversed()
+ }
+
+ private inner class IconRobotsData(private val parent: IconRobotsData? = null) {
+ private val skip: MutableSet = HashSet()
+ private val used: MutableSet = HashSet()
+ private val deprecated: MutableSet = HashSet()
+
+ fun isSkipped(file: File): Boolean = !ignoreSkipTag && (matches(file, skip) || parent?.isSkipped(file) ?: false)
+ fun isUsed(file: File): Boolean = matches(file, used) || parent?.isUsed(file) ?: false
+ fun isDeprecated(file: File): Boolean = matches(file, deprecated) || parent?.isDeprecated(file) ?: false
+
+ fun fork(dir: File, root: File): IconRobotsData {
+ val robots = File(dir, "icon-robots.txt")
+ if (!robots.exists()) return this
+
+ usedIconsRobots.add(robots)
+
+ val answer = IconRobotsData(this)
+ parse(robots,
+ Pair("skip:", { value -> compilePattern(answer.skip, dir, root, value) }),
+ Pair("used:", { value -> compilePattern(answer.used, dir, root, value) }),
+ Pair("deprecated:", { value -> compilePattern(answer.deprecated, dir, root, value) }),
+ Pair("name:", { value -> }), // ignore
+ Pair("#", { value -> }) // comment
+ )
+ return answer
+ }
+
+ private fun parse(robots: File, vararg handlers: Pair Unit>) {
+ robots.forEachLine { line ->
+ if (line.isBlank()) return@forEachLine
+ for (h in handlers) {
+ if (line.startsWith(h.first)) {
+ h.second(StringUtil.trimStart(line, h.first))
+ return@forEachLine
+ }
+ }
+ throw Exception("Can't parse $robots. Line: $line")
+ }
+ }
+
+ private fun compilePattern(set: MutableSet, dir: File, root: File, value: String) {
+ var pattern = value.trim()
+
+ if (pattern.startsWith("/")) {
+ pattern = root.absolutePath + pattern
+ }
+ else {
+ pattern = dir.absolutePath + '/' + pattern
+ }
+
+ val regExp = FileUtil.convertAntToRegexp(pattern, false)
+ try {
+ set.add(Pattern.compile(regExp).matcher(""))
+ }
+ catch (e: Exception) {
+ throw Exception("Cannot compile pattern: $pattern. Built on based in $dir/icon-robots.txt")
+ }
+ }
+
+ private fun matches(file: File, matcher: Set): Boolean {
+ val path = file.absolutePath.replace('\\', '/')
+
+ val pathWithoutExtension = FileUtilRt.getNameWithoutExtension(path)
+ val extension = FileUtilRt.getExtension(path)
+
+ val basicPathWithoutExtension = ImageType.stripSuffix(pathWithoutExtension)
+ val basicPath = basicPathWithoutExtension + if (extension.isNotEmpty()) "." + extension else ""
+
+ return matcher.any { it.reset(basicPath).matches() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt
new file mode 100644
index 000000000000..442bcceb27cf
--- /dev/null
+++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.build.scripts.ImageSanityCheckerBase.Severity.*
+import com.intellij.build.scripts.ImageType.*
+import com.intellij.openapi.util.io.FileUtil
+import org.jetbrains.jps.model.module.JpsModule
+import java.awt.Dimension
+import java.io.File
+import java.util.*
+
+abstract class ImageSanityCheckerBase(val projectHome: File, val ignoreSkipTag: Boolean) {
+ fun check(module: JpsModule) {
+ val allImages = ImageCollector(projectHome, false, ignoreSkipTag).collect(module)
+
+ val (images, broken) = allImages.partition { it.file != null }
+ log(Severity.ERROR, "image without base version", module, broken)
+
+ checkHaveRetinaVersion(images, module)
+ checkHaveCompleteIconSet(images, module)
+ checkHaveValidSize(images, module)
+ checkAreNotAmbiguous(images, module)
+ }
+
+ private fun checkHaveRetinaVersion(images: List, module: JpsModule) {
+ process(images, Severity.INFO, "image without retina version", module) { image ->
+ val hasRetina = image.files[RETINA] != null
+ val hasRetinaDarcula = image.files[RETINA_DARCULA] != null
+ return@process hasRetina || hasRetinaDarcula
+ }
+ }
+
+ private fun checkHaveCompleteIconSet(images: List, module: JpsModule) {
+ process(images, WARNING, "image without complete set of additional icons", module) { image ->
+ val hasRetina = image.files[RETINA] != null
+ val hasDarcula = image.files[DARCULA] != null
+ val hasRetinaDarcula = image.files[RETINA_DARCULA] != null
+
+ if (hasRetinaDarcula) {
+ return@process hasRetina && hasDarcula
+ }
+ else {
+ return@process !hasRetina || !hasDarcula
+ }
+ }
+ }
+
+ private fun checkHaveValidSize(images: List, module: JpsModule) {
+ val excludedPaths = arrayOf(
+ "/tips/images/",
+ "/ide/ui/laf/icons/"
+ )
+
+ process(images, WARNING, "icon with suspicious size", module) { image ->
+ if (!isIcon(image.file!!)) return@process true
+ val path = FileUtil.normalize(image.file!!.path)
+ if (excludedPaths.any { path.contains(it) }) return@process true
+ val sizes = image.files.mapValues { imageSize(it.value) }
+
+ val sizeBasic = sizes[BASIC]!!
+ val sizeRetina = sizes[RETINA]
+ val sizeDarcula = sizes[DARCULA]
+ val sizeRetinaDarcula = sizes[RETINA_DARCULA]
+
+ val sizeBasicTwice = Dimension(sizeBasic.width * 2, sizeBasic.height * 2)
+ return@process (sizeDarcula == null || sizeBasic == sizeDarcula) &&
+ (sizeRetina == null || sizeRetinaDarcula == null || sizeRetina == sizeRetinaDarcula) &&
+ (sizeRetina == null || sizeBasicTwice == sizeRetina)
+ }
+ }
+
+ private fun checkAreNotAmbiguous(images: List, module: JpsModule) {
+ process(images, WARNING, "image with ambiguous definition (ex: has both '.png' and '.gif' versions)", module) { image ->
+ return@process !image.ambiguous
+ }
+ }
+
+ private fun process(images: List, severity: Severity, message: String, module: JpsModule,
+ processor: (ImagePaths) -> Boolean) {
+ val result = ArrayList()
+ images.forEach {
+ if (!processor(it)) result.add(it)
+ }
+ log(severity, message, module, result)
+ }
+
+ internal abstract fun log(severity: Severity, message: String, module: JpsModule, images: Collection)
+
+ enum class Severity { INFO, WARNING, ERROR }
+}
+
+class ImageSanityChecker(projectHome: File) : ImageSanityCheckerBase(projectHome, false) {
+ private val infos: StringBuilder = StringBuilder()
+ private val warnings: StringBuilder = StringBuilder()
+
+ fun printInfo() {
+ if (infos.isNotEmpty()) {
+ println("")
+ println(infos)
+ }
+ }
+
+ fun printWarnings() {
+ if (warnings.isNotEmpty()) {
+ println("")
+ println(warnings)
+ }
+ }
+
+ override fun log(severity: Severity, message: String, module: JpsModule, images: Collection) {
+ val logger = when (severity) {
+ ERROR -> warnings
+ WARNING -> warnings
+ INFO -> infos
+ }
+ val prefix = when (severity) {
+ ERROR -> "ERROR:"
+ WARNING -> "WARNING:"
+ INFO -> "INFO:"
+ }
+
+ if (images.isEmpty()) return
+ logger.append("$prefix $message found in module '${module.name}'\n")
+ images.sortedBy { it.id }.forEach {
+ logger.append(" ${it.id} - ${it.presentablePath.path}\n")
+ }
+ logger.append("\n")
+ }
+}
diff --git a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt
new file mode 100644
index 000000000000..0864d3b8e649
--- /dev/null
+++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.openapi.util.io.FileUtil
+import org.jetbrains.jps.model.module.JpsModule
+import java.awt.image.BufferedImage
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.IOException
+import javax.imageio.ImageIO
+
+class ImageSizeOptimizer(val projectHome: File) {
+ private var optimizedTotal: Long = 0
+
+ fun optimizeIcons(module: JpsModule) {
+ val icons = ImageCollector(projectHome).collect(module)
+ icons.forEach {
+ it.files.values.forEach {
+ tryToReduceSize(it)
+ }
+ }
+ }
+
+ fun optimizeImages(file: File) {
+ if (file.isDirectory) {
+ file.listFiles().forEach {
+ optimizeImages(it)
+ }
+ }
+ else {
+ tryToReduceSize(file)
+ }
+ }
+
+ fun printStats() {
+ println("")
+ println("PNG size optimization: $optimizedTotal bytes in total")
+ }
+
+ private fun tryToReduceSize(file: File) {
+ val image = optimizeImage(file) ?: return
+
+ if (image.hasOptimumSize) return
+
+ try {
+ FileUtil.writeToFile(file, image.optimizedArray)
+ optimizedTotal += image.sizeBefore - image.sizeAfter
+ }
+ catch (e: IOException) {
+ throw Exception("Cannot optimize " + file.absolutePath)
+ }
+ println("${file.absolutePath} ${image.compressionStats}")
+ }
+
+ companion object {
+ fun optimizeImage(file: File): OptimizedImage? {
+ if (!file.name.endsWith(".png")) return null
+ val image = ImageIO.read(file)
+ if (image == null) {
+ println(file.absolutePath + " loading failed")
+ return null
+ }
+
+ val byteArrayOutputStream = ByteArrayOutputStream()
+ ImageIO.write(image, "png", byteArrayOutputStream)
+
+ val byteArray = byteArrayOutputStream.toByteArray()
+ return OptimizedImage(file, image, byteArray)
+ }
+ }
+
+ class OptimizedImage(val file: File, val image: BufferedImage, val optimizedArray: ByteArray) {
+ val sizeBefore = file.length()
+ val sizeAfter = optimizedArray.size
+
+ val compressionStats: String get() {
+ val compression = (sizeBefore - sizeAfter) * 100 / sizeBefore
+ return "$compression% optimized ($sizeBefore->$sizeAfter bytes)"
+ }
+
+ val hasOptimumSize: Boolean get() = sizeBefore <= sizeAfter
+ }
+}
\ No newline at end of file
diff --git a/platform/build-scripts/icons/src/com/intellij/build/scripts/util.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/util.kt
new file mode 100644
index 000000000000..bed460913060
--- /dev/null
+++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/util.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.openapi.util.text.StringUtil
+import java.awt.*
+import java.io.File
+
+internal val File.children: List get() = if (this.isDirectory) this.listFiles().toList() else emptyList()
+
+internal fun isImage(file: File, iconsOnly: Boolean): Boolean {
+ if (!isImage(file.name)) return false
+ return !iconsOnly || isIcon(file)
+}
+
+internal fun isIcon(file: File): Boolean {
+ if (!isImage(file.name)) return false
+ val size = imageSize(file)
+ return size.height == size.width || size.height <= 100 && size.width <= 100
+}
+
+private fun isImage(name: String) = name.endsWith(".png") || name.endsWith(".gif")
+
+internal fun imageSize(file: File): Dimension {
+ val image = loadImage(file)
+ val width = image.getWidth(null)
+ val height = image.getHeight(null)
+ return Dimension(width, height)
+}
+
+internal fun loadImage(path: File): Image {
+ val image = Toolkit.getDefaultToolkit().createImage(path.absolutePath)
+ waitForImage(image)
+ return image
+}
+
+private fun waitForImage(image: Image?): Boolean {
+ if (image == null) return false
+ if (image.getWidth(null) > 0) return true
+ val mediaTracker = MediaTracker(object : Component() {})
+ mediaTracker.addImage(image, 1)
+ mediaTracker.waitForID(1, 5000)
+ return !mediaTracker.isErrorID(1)
+}
+
+internal enum class ImageType(private val suffix: String) {
+ BASIC(""), RETINA("@2x"), DARCULA("_dark"), RETINA_DARCULA("@2x_dark");
+
+ fun getBasicName(name: String): String = StringUtil.trimEnd(name, suffix)
+
+ companion object {
+ fun fromName(name: String): ImageType {
+ if (name.endsWith(RETINA_DARCULA.suffix)) return RETINA_DARCULA
+ if (name.endsWith(RETINA.suffix)) return RETINA
+ if (name.endsWith(DARCULA.suffix)) return DARCULA
+ return BASIC
+ }
+
+ fun stripSuffix(name: String): String {
+ val type = fromName(name)
+ return StringUtil.trimEnd(name, type.suffix)
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt b/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt
new file mode 100644
index 000000000000..754d23b401d1
--- /dev/null
+++ b/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2000-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 com.intellij.build.scripts
+
+import com.intellij.openapi.application.PathManager
+import org.jetbrains.jps.model.JpsElementFactory
+import org.jetbrains.jps.model.module.JpsModule
+import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService
+import org.jetbrains.jps.model.serialization.JpsProjectLoader
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+import java.io.File
+import java.util.*
+import kotlin.comparisons.compareBy
+import kotlin.comparisons.thenBy
+
+class CommunityImageResourcesSanityTest : ImageResourcesTestBase() {
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun data(): Collection> {
+ return ImageResourcesTestBase.collectBadIcons(false)
+ }
+ }
+}
+
+class CommunityImageResourcesOptimumSizeTest : ImageResourcesTestBase() {
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun data(): Collection> {
+ return ImageResourcesTestBase.collectIconsWithNonOptimumSize(false)
+ }
+ }
+}
+
+@Ignore
+class AllImageResourcesSanityTest : ImageResourcesTestBase() {
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun data(): Collection> {
+ return ImageResourcesTestBase.collectBadIcons(true)
+ }
+ }
+}
+
+@Ignore
+class AllImageResourcesOptimumSizeTest : ImageResourcesTestBase() {
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun data(): Collection> {
+ return ImageResourcesTestBase.collectIconsWithNonOptimumSize(true)
+ }
+ }
+}
+
+
+@RunWith(Parameterized::class)
+abstract class ImageResourcesTestBase {
+ @Parameter(value = 0) lateinit var testName: String
+ @Parameter(value = 1) lateinit var exception: Throwable
+
+ @Test
+ fun test() {
+ throw exception
+ }
+
+ companion object {
+ @JvmStatic
+ fun collectBadIcons(ignoreSkipTag: Boolean): List> {
+ val checker = MySanityChecker(File(PathManager.getHomePath()), ignoreSkipTag)
+ forEachModule {
+ checker.check(it)
+ }
+ return createTestData(checker.failures)
+ }
+
+ @JvmStatic
+ fun collectIconsWithNonOptimumSize(ignoreSkipTag: Boolean): List> {
+ val checker = MyOptimumSizeChecker(File(PathManager.getHomePath()), ignoreSkipTag)
+ forEachModule {
+ checker.checkOptimumSizes(it)
+ }
+ return createTestData(checker.failures)
+ }
+
+ private fun createTestData(failures: Collection): List> {
+ return failures
+ .sortedWith(compareBy { it.module }.thenBy { it.id }.thenBy { it.message })
+ .map { arrayOf(it.getTestName(), it.getException()) }
+ }
+
+ private fun forEachModule(action: (JpsModule) -> Unit) {
+ val home = PathManager.getHomePath()
+ val model = JpsElementFactory.getInstance().createModel()
+
+ val pathVariables = JpsModelSerializationDataService.computeAllPathVariables(model.global)
+ JpsProjectLoader.loadProject(model.project, pathVariables, home)
+
+ model.project.modules.forEach(action)
+ }
+ }
+}
+
+private class MySanityChecker(projectHome: File, ignoreSkipTag: Boolean) : ImageSanityCheckerBase(projectHome, ignoreSkipTag) {
+ val failures = ArrayList()
+
+ override fun log(severity: ImageSanityCheckerBase.Severity,
+ message: String,
+ module: JpsModule,
+ images: Collection) {
+ if (severity == Severity.INFO) return
+ images.forEach { image ->
+ failures.add(FailedTest(module, message, image))
+ }
+ }
+}
+
+private class MyOptimumSizeChecker(val projectHome: File, val ignoreSkipTag: Boolean) {
+ val failures = ArrayList()
+
+ fun checkOptimumSizes(module: JpsModule) {
+ val allImages = ImageCollector(projectHome, false, ignoreSkipTag).collect(module)
+ val images = allImages.filter { it.file != null }
+
+ images.forEach { image ->
+ image.files.values.forEach { file ->
+ val optimized = ImageSizeOptimizer.optimizeImage(file)
+ if (optimized != null && !optimized.hasOptimumSize) {
+ failures.add(FailedTest(module, "image size can be optimized: ${optimized.compressionStats}", image, file))
+ }
+ }
+ }
+ }
+}
+
+class FailedTest(val module: String, val message: String, val id: String, val path: String) {
+ internal constructor(module: JpsModule, message: String, image: ImagePaths, file: File = image.presentablePath) :
+ this(module.name, message, image.id, file.absolutePath)
+
+ fun getTestName(): String = "'${module}' - $id - $message"
+ fun getException(): Throwable = Exception("${message} - ${path}")
+}
\ No newline at end of file
diff --git a/platform/build-scripts/platform-build-scripts.iml b/platform/build-scripts/platform-build-scripts.iml
index 8a0f5b366785..122b8972021a 100644
--- a/platform/build-scripts/platform-build-scripts.iml
+++ b/platform/build-scripts/platform-build-scripts.iml
@@ -4,6 +4,8 @@
+
+
@@ -32,5 +34,7 @@
+
+
\ No newline at end of file
diff --git a/platform/core-api/src/com/intellij/openapi/progress/EmptyProgressIndicator.java b/platform/core-api/src/com/intellij/openapi/progress/EmptyProgressIndicator.java
index e1f76c7bfd4e..f1b2c6ae4438 100644
--- a/platform/core-api/src/com/intellij/openapi/progress/EmptyProgressIndicator.java
+++ b/platform/core-api/src/com/intellij/openapi/progress/EmptyProgressIndicator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2014 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
@@ -154,7 +154,6 @@ public class EmptyProgressIndicator implements StandardProgressIndicator {
if (indicator != null) {
return indicator;
}
- LOG.info("No progress indicator");
return new EmptyProgressIndicator();
}
}
diff --git a/platform/dvcs-impl/resources/icons/loading_old@2x.gif b/platform/dvcs-impl/resources/icons/loading_old@2x.gif
deleted file mode 100644
index 7e6d0c3899eb..000000000000
Binary files a/platform/dvcs-impl/resources/icons/loading_old@2x.gif and /dev/null differ
diff --git a/platform/dvcs-impl/resources/icons/loading_old@2x_dark.gif b/platform/dvcs-impl/resources/icons/loading_old@2x_dark.gif
deleted file mode 100644
index f76af050bb9d..000000000000
Binary files a/platform/dvcs-impl/resources/icons/loading_old@2x_dark.gif and /dev/null differ
diff --git a/platform/editor-ui-ex/src/com/intellij/openapi/editor/colors/impl/AbstractColorsScheme.java b/platform/editor-ui-ex/src/com/intellij/openapi/editor/colors/impl/AbstractColorsScheme.java
index 247d89fa6210..eedf4c8f199f 100644
--- a/platform/editor-ui-ex/src/com/intellij/openapi/editor/colors/impl/AbstractColorsScheme.java
+++ b/platform/editor-ui-ex/src/com/intellij/openapi/editor/colors/impl/AbstractColorsScheme.java
@@ -525,7 +525,7 @@ public abstract class AbstractColorsScheme implements EditorColorsScheme, Serial
if (!isDefault) {
size = (fontScale != null) ? size / fontScale : DEFAULT_FONT_SIZE.getSize();
}
- return JBUI.scaleFontSize(size);
+ return (int)JBUI.scale(size);
}
private void readFontSettings(@NotNull Element element,
diff --git a/platform/icons/src/actions/help@2x_dark.png b/platform/icons/src/actions/help@2x_dark.png
deleted file mode 100644
index fef9b644ecdb..000000000000
Binary files a/platform/icons/src/actions/help@2x_dark.png and /dev/null differ
diff --git a/platform/icons/src/actions/menu-find@2x_dark.png b/platform/icons/src/actions/menu-find@2x_dark.png
deleted file mode 100644
index 1154a4ce1af8..000000000000
Binary files a/platform/icons/src/actions/menu-find@2x_dark.png and /dev/null differ
diff --git a/platform/icons/src/actions/menu-help@2x_dark.png b/platform/icons/src/actions/menu-help@2x_dark.png
deleted file mode 100644
index fef9b644ecdb..000000000000
Binary files a/platform/icons/src/actions/menu-help@2x_dark.png and /dev/null differ
diff --git a/platform/icons/src/fileTypes/jsonSchema.png b/platform/icons/src/fileTypes/jsonSchema.png
index 7494963c3c37..e32706ba7309 100644
Binary files a/platform/icons/src/fileTypes/jsonSchema.png and b/platform/icons/src/fileTypes/jsonSchema.png differ
diff --git a/platform/icons/src/fileTypes/jsonSchema@2x.png b/platform/icons/src/fileTypes/jsonSchema@2x.png
index 57d2586ffbad..b741ca9d9da6 100644
Binary files a/platform/icons/src/fileTypes/jsonSchema@2x.png and b/platform/icons/src/fileTypes/jsonSchema@2x.png differ
diff --git a/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FTManager.java b/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FTManager.java
index 67c27197fd9a..12c5205b0cd8 100644
--- a/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FTManager.java
+++ b/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FTManager.java
@@ -154,6 +154,10 @@ class FTManager {
final FileTemplateBase template = getTemplates().get(qName);
if (template instanceof CustomFileTemplate) {
getTemplates().remove(qName);
+ File file = new File(getConfigRoot(false), qName);
+ if (file.exists()) {
+ file.delete();
+ }
mySortedTemplates = null;
}
else if (template instanceof BundledFileTemplate){
@@ -176,6 +180,7 @@ class FTManager {
_template.setReformatCode(template.isReformatCode());
_template.setLiveTemplateEnabled(template.isLiveTemplateEnabled());
}
+ saveTemplates(true);
}
private void restoreDefaults(Set toDisable) {
@@ -257,6 +262,10 @@ class FTManager {
}
public void saveTemplates() {
+ saveTemplates(false);
+ }
+
+ private void saveTemplates(boolean removeDeleted) {
final File configRoot = getConfigRoot(true);
final File[] files = configRoot.listFiles();
@@ -300,7 +309,9 @@ class FTManager {
}
else if (templateToSave == null) {
// template was removed
- FileUtil.delete(customizedTemplateFile);
+ if (removeDeleted) {
+ FileUtil.delete(customizedTemplateFile);
+ }
}
else {
// both customized content on disk and corresponding template are present
diff --git a/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FileTemplateManagerImpl.java b/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FileTemplateManagerImpl.java
index 1c2604c722e7..532c0bcd6b9d 100644
--- a/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FileTemplateManagerImpl.java
+++ b/platform/lang-impl/src/com/intellij/ide/fileTemplates/impl/FileTemplateManagerImpl.java
@@ -391,7 +391,6 @@ public class FileTemplateManagerImpl extends FileTemplateManager implements Pers
for (FTManager manager : getAllManagers()) {
if (templatesCategory.equals(manager.getName())) {
manager.updateTemplates(templates);
- manager.saveTemplates();
break;
}
}
diff --git a/platform/platform-api/src/com/intellij/openapi/ui/ThreeComponentsSplitter.java b/platform/platform-api/src/com/intellij/openapi/ui/ThreeComponentsSplitter.java
index 9e537a852f90..b4c9cb64ac49 100644
--- a/platform/platform-api/src/com/intellij/openapi/ui/ThreeComponentsSplitter.java
+++ b/platform/platform-api/src/com/intellij/openapi/ui/ThreeComponentsSplitter.java
@@ -35,6 +35,8 @@ import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.util.*;
+import java.util.List;
/**
* @author Vladimir Kondratyev
@@ -615,7 +617,8 @@ public class ThreeComponentsSplitter extends JPanel implements Disposable {
if (window != null) {
Point point = SwingUtilities.convertPoint(this, p, window);
Component component = SwingUtilities.getDeepestComponentAt(window, point.x, point.y);
- if (UIUtil.findParentByCondition(component, c -> c == ThreeComponentsSplitter.this) == null) return false;
+ List components = Arrays.asList(myFirstComponent, myFirstDivider, myInnerComponent, myLastDivider, myLastComponent);
+ if (UIUtil.findParentByCondition(component, c -> c != null && components.contains(c)) == null) return false;
}
int dndOff = myIsOnePixel ? JBUI.scale(Registry.intValue("ide.splitter.mouseZone")) / 2 : 0;
diff --git a/platform/platform-tests/testSrc/com/intellij/ide/fileTemplates/impl/LightFileTemplatesTest.java b/platform/platform-tests/testSrc/com/intellij/ide/fileTemplates/impl/LightFileTemplatesTest.java
index f628f6e7a365..9d2da31e1b96 100644
--- a/platform/platform-tests/testSrc/com/intellij/ide/fileTemplates/impl/LightFileTemplatesTest.java
+++ b/platform/platform-tests/testSrc/com/intellij/ide/fileTemplates/impl/LightFileTemplatesTest.java
@@ -31,7 +31,9 @@ import com.intellij.util.ArrayUtil;
import org.jdom.Element;
import java.io.File;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* @author Dmitry Avdeev
@@ -111,7 +113,7 @@ public class LightFileTemplatesTest extends LightPlatformTestCase {
public void testSurviveOnProjectReopen() throws Exception {
File foo = PlatformTestCase.createTempDir("foo");
Project reloaded = null;
- final Project project = ProjectManager.getInstance().createProject("foo", foo.getPath());;
+ final Project project = ProjectManager.getInstance().createProject("foo", foo.getPath());
try {
assertNotNull(project);
FileTemplateManager manager = FileTemplateManager.getInstance(project);
@@ -139,6 +141,46 @@ public class LightFileTemplatesTest extends LightPlatformTestCase {
}
}
+ public void testAddRemoveShared() throws Exception {
+ File foo = PlatformTestCase.createTempDir("foo");
+ final Project project = ProjectManager.getInstance().createProject("foo", foo.getPath());;
+ try {
+ assertNotNull(project);
+ FileTemplateManager manager = FileTemplateManager.getInstance(project);
+ manager.setCurrentScheme(manager.getProjectScheme());
+ manager.saveAllTemplates();
+
+ FileTemplateSettings settings = ServiceManager.getService(project, FileTemplateSettings.class);
+ FTManager ftManager = settings.getDefaultTemplatesManager();
+ File root = ftManager.getConfigRoot(false);
+ assertTrue(root.exists());
+ File file = new File(root, "Foo.java");
+ assertTrue(file.createNewFile());
+ manager.saveAllTemplates();
+ assertTrue(file.exists());
+
+ FileTemplate template = manager.addTemplate("Foo", "java");
+ // now remove it via "remove template" call
+ manager.removeTemplate(template);
+ manager.saveAllTemplates();
+ assertFalse(file.exists());
+
+ // check "setTemplates" call
+ FileTemplateBase templateBase = (FileTemplateBase)manager.addTemplate("Foo", "java");
+ List templates = new ArrayList<>(ftManager.getAllTemplates(true));
+ assertTrue(templates.contains(templateBase));
+ ftManager.saveTemplates();
+ assertTrue(file.exists());
+
+ templates.remove(templateBase);
+ manager.setTemplates(FileTemplateManager.DEFAULT_TEMPLATES_CATEGORY, templates);
+ assertFalse(file.exists());
+ }
+ finally {
+ closeProject(project);
+ }
+ }
+
private static void closeProject(final Project project) {
if (project != null && !project.isDisposed()) {
ProjectManager.getInstance().closeProject(project);
diff --git a/platform/util/src/com/intellij/ui/PopupBorder.java b/platform/util/src/com/intellij/ui/PopupBorder.java
index ff5311d0ac41..fa62015a9371 100644
--- a/platform/util/src/com/intellij/ui/PopupBorder.java
+++ b/platform/util/src/com/intellij/ui/PopupBorder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2016 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
@@ -16,6 +16,7 @@
package com.intellij.ui;
import com.intellij.openapi.util.SystemInfo;
+import com.intellij.ui.paint.RectanglePainter;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.NotNull;
@@ -70,7 +71,7 @@ public interface PopupBorder extends Border {
Color color = myActive ? myActiveColor : myPassiveColor;
g.setColor(color);
- g.drawRect(x, y, width - 1, height - 1);
+ RectanglePainter.DRAW.paint((Graphics2D)g, x, y, width, height, null);
}
@Override
diff --git a/platform/platform-api/src/com/intellij/ui/paint/RectanglePainter.java b/platform/util/src/com/intellij/ui/paint/RectanglePainter.java
similarity index 98%
rename from platform/platform-api/src/com/intellij/ui/paint/RectanglePainter.java
rename to platform/util/src/com/intellij/ui/paint/RectanglePainter.java
index cc7b69a710eb..50566c777010 100644
--- a/platform/platform-api/src/com/intellij/ui/paint/RectanglePainter.java
+++ b/platform/util/src/com/intellij/ui/paint/RectanglePainter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2016 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
diff --git a/platform/platform-api/src/com/intellij/util/ui/RegionPainter.java b/platform/util/src/com/intellij/util/ui/RegionPainter.java
similarity index 99%
rename from platform/platform-api/src/com/intellij/util/ui/RegionPainter.java
rename to platform/util/src/com/intellij/util/ui/RegionPainter.java
index 5dc0dcb838ec..a35b09e6c96f 100644
--- a/platform/platform-api/src/com/intellij/util/ui/RegionPainter.java
+++ b/platform/util/src/com/intellij/util/ui/RegionPainter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2015 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/synthetic/GroovyScriptClass.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/synthetic/GroovyScriptClass.java
index 4fe92022a6dd..3883d9b6a201 100644
--- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/synthetic/GroovyScriptClass.java
+++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/synthetic/GroovyScriptClass.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2000-2016 JetBrains s.r.o.
+ * Copyright 2000-2017 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.
@@ -27,6 +27,7 @@ import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.ui.RowIcon;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.PathUtil;
+import icons.JetgroovyIcons;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -231,8 +232,7 @@ public class GroovyScriptClass extends GrLightTypeDefinitionBase implements Synt
@Override
@Nullable
public Icon getIcon(int flags) {
- final Icon icon = myFile.getIcon(flags);
- RowIcon baseIcon = ElementBase.createLayeredIcon(this, icon, 0);
+ RowIcon baseIcon = ElementBase.createLayeredIcon(this, JetgroovyIcons.Groovy.Class, 0);
return ElementPresentationUtil.addVisibilityIcon(this, flags, baseIcon);
}
diff --git a/plugins/xpath/xpath-lang/gen/icons/XpathIcons.java b/plugins/xpath/xpath-lang/gen/icons/XpathIcons.java
index bc38ef68dbb8..665242778b6e 100644
--- a/plugins/xpath/xpath-lang/gen/icons/XpathIcons.java
+++ b/plugins/xpath/xpath-lang/gen/icons/XpathIcons.java
@@ -19,8 +19,8 @@ public class XpathIcons {
public static final Icon Namespace = load("/icons/namespace.png"); // 21x21
public static final Icon Tag = load("/icons/tag.png"); // 16x16
public static final Icon Template = load("/icons/template.png"); // 18x18
+ public static final Icon Xml = load("/icons/xml.png"); // 16x16
public static final Icon Xpath = load("/icons/xpath.png"); // 12x16
public static final Icon Xslt_filetype_overlay = load("/icons/xslt-filetype-overlay.png"); // 11x16
public static final Icon Xslt = load("/icons/xslt.png"); // 16x16
- public static final Icon Xml = load("/icons/xml.png"); // 16x16
}
diff --git a/python/educational-python/resources/tips/images/breakpoint_speedmenu@_dark.png b/python/educational-python/resources/tips/images/breakpoint_speedmenu_dark.png
similarity index 100%
rename from python/educational-python/resources/tips/images/breakpoint_speedmenu@_dark.png
rename to python/educational-python/resources/tips/images/breakpoint_speedmenu_dark.png
diff --git a/python/python-community-ide-resources/resources/tips/images/breakpoint_speedmenu@_dark.png b/python/python-community-ide-resources/resources/tips/images/breakpoint_speedmenu_dark.png
similarity index 100%
rename from python/python-community-ide-resources/resources/tips/images/breakpoint_speedmenu@_dark.png
rename to python/python-community-ide-resources/resources/tips/images/breakpoint_speedmenu_dark.png
diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml
index c74390e381ae..7e328e0ad6a7 100644
--- a/python/src/META-INF/python-core-common.xml
+++ b/python/src/META-INF/python-core-common.xml
@@ -28,6 +28,7 @@
+
diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties
index 2faa0aa3aa90..8070a9e5bd51 100644
--- a/python/src/com/jetbrains/python/PyBundle.properties
+++ b/python/src/com/jetbrains/python/PyBundle.properties
@@ -1033,7 +1033,9 @@ formatter.multi.clause.statements=Multi-clause statements
formatter.force.new.line.after.colon=Force new line after colon
formatter.align.when.multiline=Align when multiline
formatter.collections.and.comprehensions=Collections and Comprehensions
-formatter.import.statements=Import Statements
+formatter.from.import.statements="From" Import Statements
+formatter.from.import.statements.force.parentheses.if.multiline=Force parentheses if multiline
+formatter.from.import.statements.force.comma.if.multline=Force trailing comma if multiline
formatter.dictionary.literals=Dictionary literals
formatter.hang.closing.brackets=Hang closing brackets
diff --git a/python/src/com/jetbrains/python/formatter/PyBlock.java b/python/src/com/jetbrains/python/formatter/PyBlock.java
index 4d3be1c9bf87..e3427af2cc5b 100644
--- a/python/src/com/jetbrains/python/formatter/PyBlock.java
+++ b/python/src/com/jetbrains/python/formatter/PyBlock.java
@@ -21,7 +21,6 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
-import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.tree.IElementType;
@@ -90,10 +89,13 @@ public class PyBlock implements ASTBlock {
private final PyBlockContext myContext;
private List mySubBlocks = null;
private Map mySubBlockByNode = null;
- private Alignment myChildAlignment;
- private final Alignment myDictAlignment;
- private final Wrap myDictWrapping;
private final boolean myEmptySequence;
+
+ // Shared among multiple children sub-blocks
+ private Alignment myChildAlignment = null;
+ private Alignment myDictAlignment = null;
+ private Wrap myDictWrapping = null;
+ private Wrap myFromImportWrapping = null;
public PyBlock(@Nullable PyBlock parent,
@NotNull ASTNode node,
@@ -109,13 +111,13 @@ public class PyBlock implements ASTBlock {
myContext = context;
myEmptySequence = isEmptySequence(node);
+ final PyCodeStyleSettings pySettings = myContext.getPySettings();
if (node.getElementType() == PyElementTypes.DICT_LITERAL_EXPRESSION) {
myDictAlignment = Alignment.createAlignment(true);
- myDictWrapping = Wrap.createWrap(myContext.getPySettings().DICT_WRAPPING, true);
+ myDictWrapping = Wrap.createWrap(pySettings.DICT_WRAPPING, true);
}
- else {
- myDictAlignment = null;
- myDictWrapping = null;
+ else if (node.getElementType() == PyElementTypes.FROM_IMPORT_STATEMENT) {
+ myFromImportWrapping = Wrap.createWrap(pySettings.FROM_IMPORT_WRAPPING, false);
}
}
@@ -184,9 +186,11 @@ public class PyBlock implements ASTBlock {
final IElementType grandparentType = grandParentNode == null ? null : grandParentNode.getElementType();
final IElementType childType = child.getElementType();
- Wrap wrap = null;
+ Wrap childWrap = null;
Indent childIndent = Indent.getNoneIndent();
Alignment childAlignment = null;
+
+ final PyCodeStyleSettings settings = myContext.getPySettings();
if (parentType == PyElementTypes.BINARY_EXPRESSION && !isInControlStatement()) {
//Setup alignments for binary expression
@@ -220,7 +224,12 @@ public class PyBlock implements ASTBlock {
}
}
else if (childType == PyElementTypes.IMPORT_ELEMENT) {
- wrap = Wrap.createWrap(WrapType.NORMAL, true);
+ if (parentType == PyElementTypes.FROM_IMPORT_STATEMENT) {
+ childWrap = myFromImportWrapping;
+ }
+ else {
+ childWrap = Wrap.createWrap(WrapType.NORMAL, true);
+ }
childIndent = Indent.getNormalIndent();
}
if (childType == PyTokenTypes.END_OF_LINE_COMMENT && parentType == PyElementTypes.FROM_IMPORT_STATEMENT) {
@@ -232,7 +241,7 @@ public class PyBlock implements ASTBlock {
!ourBrackets.contains(childType) &&
childType != PyTokenTypes.COMMA &&
!isSliceOperand(child) /*&& !isSubscriptionOperand(child)*/) {
- wrap = Wrap.createWrap(WrapType.NORMAL, true);
+ childWrap = Wrap.createWrap(WrapType.NORMAL, true);
}
if (needListAlignment(child) && !myEmptySequence) {
childAlignment = getAlignmentForChildren();
@@ -251,7 +260,7 @@ public class PyBlock implements ASTBlock {
}
}
- final PyCodeStyleSettings settings = CodeStyleSettingsManager.getSettings(child.getPsi().getProject()).getCustomSettings(PyCodeStyleSettings.class);
+
if (parentType == PyElementTypes.LIST_LITERAL_EXPRESSION || parentType == PyElementTypes.LIST_COMP_EXPRESSION) {
if ((childType == PyTokenTypes.RBRACKET && !settings.HANG_CLOSING_BRACKETS) || childType == PyTokenTypes.LBRACKET) {
childIndent = Indent.getNoneIndent();
@@ -286,7 +295,8 @@ public class PyBlock implements ASTBlock {
}
if (childType == PyTokenTypes.RPAR) {
childIndent = Indent.getNoneIndent();
- if (!hasHangingIndent(myNode.getPsi())) {
+ // Don't have hanging indent and is not going to have it due to the setting about opening parenthesis
+ if (!hasHangingIndent(myNode.getPsi()) && !settings.FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS) {
childAlignment = getAlignmentForChildren();
}
else if (settings.HANG_CLOSING_BRACKETS) {
@@ -370,7 +380,7 @@ public class PyBlock implements ASTBlock {
}
}
if (childType == PyElementTypes.KEY_VALUE_EXPRESSION && isChildOfDictLiteral(child)) {
- wrap = myDictWrapping;
+ childWrap = myDictWrapping;
childIndent = Indent.getNormalIndent();
}
@@ -406,7 +416,7 @@ public class PyBlock implements ASTBlock {
prev = prev.getTreePrev();
}
- return new PyBlock(this, child, childAlignment, childIndent, wrap, myContext);
+ return new PyBlock(this, child, childAlignment, childIndent, childWrap, myContext);
}
private static boolean isValueOfKeyValuePairOfDictLiteral(@NotNull ASTNode node) {
@@ -607,7 +617,8 @@ public class PyBlock implements ASTBlock {
myContext.getMode() == FormattingMode.ADJUST_INDENT) {
return true;
}
- return !hasHangingIndent(myNode.getPsi());
+ return !hasHangingIndent(myNode.getPsi()) && !(myNode.getElementType() == PyElementTypes.DICT_LITERAL_EXPRESSION &&
+ myContext.getPySettings().DICT_NEW_LINE_AFTER_LEFT_BRACE);
}
if (myNode.getElementType() == PyElementTypes.ARGUMENT_LIST) {
if (!myContext.getSettings().ALIGN_MULTILINE_PARAMETERS_IN_CALLS || hasHangingIndent(myNode.getPsi())) {
diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java
index 26133f0842ce..a8d81a9de18d 100644
--- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java
+++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java
@@ -17,8 +17,10 @@ package com.jetbrains.python.formatter;
import com.intellij.formatting.WrapType;
import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
import com.jetbrains.python.PyBundle;
+import org.intellij.lang.annotations.MagicConstant;
/**
* @author yole
@@ -74,6 +76,12 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings {
public boolean SPACE_BEFORE_NUMBER_SIGN = true;
public int DICT_ALIGNMENT = DICT_ALIGNMENT_NONE;
+ @MagicConstant(intValues = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
public int DICT_WRAPPING = WrapType.NORMAL.getLegacyRepresentation();
public boolean DICT_NEW_LINE_AFTER_LEFT_BRACE = false;
public boolean DICT_NEW_LINE_BEFORE_RIGHT_BRACE = false;
@@ -93,6 +101,27 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings {
public boolean OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST = true;
public boolean OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE = false;
+ /**
+ * Affects wrapping of multiple imported names in a single "from" import.
+ */
+ @MagicConstant(intValues = {
+ CommonCodeStyleSettings.DO_NOT_WRAP,
+ CommonCodeStyleSettings.WRAP_AS_NEEDED,
+ CommonCodeStyleSettings.WRAP_ALWAYS,
+ CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM
+ })
+ public int FROM_IMPORT_WRAPPING = WrapType.NORMAL.getLegacyRepresentation();
+ public boolean FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = false;
+ public boolean FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = false;
+
+ @MagicConstant(intValues = {
+ CommonCodeStyleSettings.DO_NOT_FORCE,
+ CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE,
+ CommonCodeStyleSettings.FORCE_BRACES_ALWAYS}
+ )
+ public boolean FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = false;
+ public boolean FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = false;
+
/**
* Corresponds to the option of pycodestyle.py "--hang-closing". Basically, it means that the closing brace of a collection literal,
* a comprehension, an argument list, a parameter list or parentheses in "from" import statement should have the same indent as the items
diff --git a/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java
new file mode 100644
index 000000000000..825fb2153188
--- /dev/null
+++ b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2000-2017 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 com.jetbrains.python.formatter;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor;
+import com.intellij.psi.impl.source.codeStyle.PostFormatProcessorHelper;
+import com.intellij.util.containers.ContainerUtil;
+import com.jetbrains.python.PyTokenTypes;
+import com.jetbrains.python.psi.*;
+import com.jetbrains.python.psi.impl.PyPsiUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class PyFromImportPostFormatProcessor implements PostFormatProcessor {
+ @Override
+ public PsiElement processElement(@NotNull PsiElement source, @NotNull CodeStyleSettings settings) {
+ return new Visitor(settings).processElement(source);
+ }
+
+ @Override
+ public TextRange processText(@NotNull PsiFile source, @NotNull TextRange rangeToReformat, @NotNull CodeStyleSettings settings) {
+ return new Visitor(settings).processTextRange(source, rangeToReformat);
+ }
+
+ private static class Visitor extends PyRecursiveElementVisitor {
+ private final PostFormatProcessorHelper myHelper;
+ private final List myImportStatements = new ArrayList<>();
+
+ public Visitor(@NotNull CommonCodeStyleSettings settings) {
+ myHelper = new PostFormatProcessorHelper(settings);
+ }
+
+ @Override
+ public void visitPyFromImportStatement(PyFromImportStatement node) {
+ if (myHelper.isElementFullyInRange(node)) {
+ // If non-parenthesized "from" import ends with one or more of trailing commas, the array returned by getImportElements()
+ // contains empty import elements at the end
+ final List importedNames = ContainerUtil.filter(node.getImportElements(), elem -> elem.getTextLength() != 0);
+ if (importedNames.size() > 1) {
+
+ final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class);
+ final boolean forcedParens = pySettings.FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE && PostFormatProcessorHelper.isMultiline(node);
+ final boolean forcedComma = pySettings.FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE && PostFormatProcessorHelper.isMultiline(node);
+ final PyImportElement lastImportedName = importedNames.get(importedNames.size() - 1);
+ final PsiElement afterLastName = PyPsiUtils.getNextNonCommentSibling(lastImportedName, true);
+ final PsiElement openingParen = node.getLeftParen();
+ final boolean missingComma = afterLastName == null || afterLastName.getNode().getElementType() != PyTokenTypes.COMMA;
+ // Trailing comma is allowed only in "from" imports wrapped in parentheses
+ if (forcedParens && openingParen == null || forcedComma && missingComma && openingParen != null) {
+ myImportStatements.add(node);
+ }
+ }
+ }
+ }
+
+ @NotNull
+ public PsiElement processElement(@NotNull PsiElement element) {
+ findAndReplaceFromImports(element);
+ return element;
+ }
+
+ @NotNull
+ public TextRange processTextRange(@NotNull PsiFile file, @NotNull TextRange range) {
+ myHelper.setResultTextRange(range);
+ findAndReplaceFromImports(file);
+ return myHelper.getResultTextRange();
+ }
+
+ private void findAndReplaceFromImports(@NotNull PsiElement element) {
+ if (element.getContainingFile() instanceof PyFile) {
+ element.accept(this);
+ Collections.reverse(myImportStatements);
+ for (PyFromImportStatement statement : myImportStatements) {
+ replaceFromImport(statement);
+ }
+ }
+ }
+
+ @NotNull
+ private PyFromImportStatement replaceFromImport(@NotNull PyFromImportStatement fromImport) {
+ final PyImportElement[] allNames = fromImport.getImportElements();
+ final PyImportElement firstName = allNames[0];
+ final PyElementGenerator generator = PyElementGenerator.getInstance(fromImport.getProject());
+ final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(fromImport.getProject());
+
+ if (fromImport.getLeftParen() == null) {
+ // Surround with parentheses stripping obsolete continuation backslashes and added trailing comma if necessary
+ final String beforeFirstName = fromImport.getText().substring(0, firstName.getStartOffsetInParent());
+ final StringBuilder newStatementText = new StringBuilder(beforeFirstName);
+ newStatementText.append("(");
+ boolean lastElementWasComment = false;
+ int lastVisibleNameCommaOffset = -1;
+ for (PsiElement cur = firstName; cur != null; cur = cur.getNextSibling()) {
+ if (cur instanceof PsiWhiteSpace) {
+ newStatementText.append(cur.getText().replace("\\", ""));
+ }
+ else {
+ newStatementText.append(cur.getText());
+ }
+ if (cur instanceof PyImportElement && cur.getTextLength() != 0) {
+ lastVisibleNameCommaOffset = newStatementText.length();
+ }
+ else if (lastVisibleNameCommaOffset != -1 && cur.getNode().getElementType() == PyTokenTypes.COMMA) {
+ lastVisibleNameCommaOffset = -1;
+ }
+ lastElementWasComment = cur instanceof PsiComment;
+ }
+ final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class);
+ if (lastVisibleNameCommaOffset != -1 && pySettings.FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE) {
+ newStatementText.insert(lastVisibleNameCommaOffset, ",");
+ }
+ if (lastElementWasComment) {
+ newStatementText.append("\n");
+ }
+ newStatementText.append(")");
+
+ final LanguageLevel level = LanguageLevel.forElement(fromImport);
+ PyFromImportStatement newFromImport = generator.createFromText(level, PyFromImportStatement.class, newStatementText.toString());
+ newFromImport = (PyFromImportStatement)fromImport.replace(newFromImport);
+ newFromImport = (PyFromImportStatement)codeStyleManager.reformat(newFromImport, true);
+ myHelper.updateResultRange(fromImport.getTextLength(), newFromImport.getTextLength());
+ return newFromImport;
+ }
+ else {
+ // Add only trailing comma
+ final PsiElement comma = fromImport.addAfter(generator.createComma().getPsi(), allNames[allNames.length - 1]);
+ codeStyleManager.reformat(comma);
+ return fromImport;
+ }
+ }
+ }
+
+}
diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java
index 310d9b24df82..511cca5aafed 100644
--- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java
+++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java
@@ -119,9 +119,24 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin
consumer.showCustomOption(PyCodeStyleSettings.class, "ALIGN_COLLECTIONS_AND_COMPREHENSIONS",
PyBundle.message("formatter.align.when.multiline"),
PyBundle.message("formatter.collections.and.comprehensions"));
+
+ consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_WRAPPING",
+ PyBundle.message("formatter.from.import.statements"), null, WRAP_OPTIONS, WRAP_VALUES);
consumer.showCustomOption(PyCodeStyleSettings.class, "ALIGN_MULTILINE_IMPORTS",
PyBundle.message("formatter.align.when.multiline"),
- PyBundle.message("formatter.import.statements"));
+ PyBundle.message("formatter.from.import.statements"));
+ consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS",
+ ApplicationBundle.message("wrapping.new.line.after.lpar"),
+ PyBundle.message("formatter.from.import.statements"));
+ consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS",
+ ApplicationBundle.message("wrapping.rpar.on.new.line"),
+ PyBundle.message("formatter.from.import.statements"));
+ consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE",
+ PyBundle.message("formatter.from.import.statements.force.parentheses.if.multiline"),
+ PyBundle.message("formatter.from.import.statements"));
+ consumer.showCustomOption(PyCodeStyleSettings.class, "FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE",
+ PyBundle.message("formatter.from.import.statements.force.comma.if.multline"),
+ PyBundle.message("formatter.from.import.statements"));
consumer.showCustomOption(PyCodeStyleSettings.class, "DICT_WRAPPING",
PyBundle.message("formatter.dictionary.literals"), null, WRAP_OPTIONS, WRAP_VALUES);
@@ -177,17 +192,32 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin
" def foo(self):\n" +
" pass";
@SuppressWarnings("FieldCanBeLocal")
- private static String WRAP_SETTINGS_PREVIEW = "from foo import (bar,\n" +
- " baz)\n\n" +
- "long_expression = component_one + component_two + component_three + component_four + component_five + component_six\n\n" +
+ private static String WRAP_SETTINGS_PREVIEW = "from module import foo, bar, baz, quux\n" +
+ "\n" +
+ "long_expression = component_one + component_two + component_three + component_four + component_five + component_six\n" +
+ "\n" +
"def xyzzy(long_parameter_1,\n" +
"long_parameter_2):\n" +
- " pass\n\n" +
+ " pass\n" +
+ "\n" +
"xyzzy('long_string_constant1',\n" +
" 'long_string_constant2')\n" +
+ "\n" +
+ "xyzzy(\n" +
+ " 'with',\n" +
+ " 'hanging',\n" +
+ " 'indent'\n" +
+ ")\n" +
"attrs = [e.attr for e in\n" +
- " items]\n\n" +
- "if True: pass\n\n" +
+ " items]\n" +
+ "\n" +
+ "ingredients = [\n" +
+ " 'green',\n" +
+ " 'eggs',\n" +
+ "]\n" +
+ "\n" +
+ "if True: pass\n" +
+ "\n" +
"try: pass\n" +
"finally: pass\n";
@SuppressWarnings("FieldCanBeLocal")
diff --git a/python/src/com/jetbrains/python/formatter/PythonFormattingModelBuilder.java b/python/src/com/jetbrains/python/formatter/PythonFormattingModelBuilder.java
index cf1a1d3dc06c..71100d5f408b 100644
--- a/python/src/com/jetbrains/python/formatter/PythonFormattingModelBuilder.java
+++ b/python/src/com/jetbrains/python/formatter/PythonFormattingModelBuilder.java
@@ -101,19 +101,23 @@ public class PythonFormattingModelBuilder implements FormattingModelBuilderEx, C
.afterInside(RARROW, ANNOTATION).spaces(1)
.between(allButLambda(), PARAMETER_LIST).spaceIf(commonSettings.SPACE_BEFORE_METHOD_PARENTHESES)
-
- .betweenInside(COMMA, RBRACE, DICT_LITERAL_EXPRESSION).spaceIf(pySettings.SPACE_WITHIN_BRACES | commonSettings.SPACE_AFTER_COMMA,
+
+ .betweenInside(COMMA, RBRACE, DICT_LITERAL_EXPRESSION).spaceIf(pySettings.SPACE_WITHIN_BRACES | commonSettings.SPACE_AFTER_COMMA,
pySettings.DICT_NEW_LINE_BEFORE_RIGHT_BRACE)
.afterInside(LBRACE, DICT_LITERAL_EXPRESSION).spaceIf(pySettings.SPACE_WITHIN_BRACES, pySettings.DICT_NEW_LINE_AFTER_LEFT_BRACE)
.beforeInside(RBRACE, DICT_LITERAL_EXPRESSION).spaceIf(pySettings.SPACE_WITHIN_BRACES, pySettings.DICT_NEW_LINE_BEFORE_RIGHT_BRACE)
.between(COMMA, RBRACE).spaceIf(pySettings.SPACE_WITHIN_BRACES | commonSettings.SPACE_AFTER_COMMA)
.withinPair(LBRACE, RBRACE).spaceIf(pySettings.SPACE_WITHIN_BRACES)
-
+
.between(COMMA, RBRACKET).spaceIf(commonSettings.SPACE_WITHIN_BRACKETS | commonSettings.SPACE_AFTER_COMMA)
.withinPair(LBRACKET, RBRACKET).spaceIf(commonSettings.SPACE_WITHIN_BRACKETS)
-
+
.before(COLON).spaceIf(pySettings.SPACE_BEFORE_PY_COLON)
+ .afterInside(LPAR, FROM_IMPORT_STATEMENT).spaces(0, pySettings.FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS)
+ .betweenInside(COMMA, RPAR, FROM_IMPORT_STATEMENT).spaceIf(commonSettings.SPACE_AFTER_COMMA,
+ pySettings.FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS)
+ .beforeInside(RPAR, FROM_IMPORT_STATEMENT).spaces(0, pySettings.FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS)
.after(COMMA).spaceIf(commonSettings.SPACE_AFTER_COMMA)
.before(COMMA).spaceIf(commonSettings.SPACE_BEFORE_COMMA)
.between(FROM_KEYWORD, DOT).spaces(1)
@@ -125,7 +129,6 @@ public class PythonFormattingModelBuilder implements FormattingModelBuilderEx, C
.withinPairInside(LPAR, RPAR, ARGUMENT_LIST).spaceIf(commonSettings.SPACE_WITHIN_METHOD_CALL_PARENTHESES)
.betweenInside(LPAR, RPAR, PARAMETER_LIST).spaceIf(commonSettings.SPACE_WITHIN_EMPTY_METHOD_PARENTHESES)
.withinPairInside(LPAR, RPAR, PARAMETER_LIST).spaceIf(commonSettings.SPACE_WITHIN_METHOD_PARENTHESES)
- .withinPairInside(LPAR, RPAR, FROM_IMPORT_STATEMENT).spaces(0)
.withinPairInside(LPAR, RPAR, GENERATOR_EXPRESSION).spaces(0)
.withinPairInside(LPAR, RPAR, PARENTHESIZED_EXPRESSION).spaces(0)
.before(LBRACKET).spaceIf(pySettings.SPACE_BEFORE_LBRACKET)
diff --git a/python/testData/formatter/fromImportForceParenthesesIfMultiline.py b/python/testData/formatter/fromImportForceParenthesesIfMultiline.py
new file mode 100644
index 000000000000..4367abfb876b
--- /dev/null
+++ b/python/testData/formatter/fromImportForceParenthesesIfMultiline.py
@@ -0,0 +1,12 @@
+from module import foo
+from module import foo, bar
+# | margin
+from module import foo, bar, baz
+from module import foo, \
+ bar
+from module import foo, \
+ bar # comment
+from module import foo, \
+ \
+ \
+ bar,
\ No newline at end of file
diff --git a/python/testData/formatter/fromImportForceParenthesesIfMultiline_after.py b/python/testData/formatter/fromImportForceParenthesesIfMultiline_after.py
new file mode 100644
index 000000000000..40f208af690d
--- /dev/null
+++ b/python/testData/formatter/fromImportForceParenthesesIfMultiline_after.py
@@ -0,0 +1,13 @@
+from module import foo
+from module import foo, bar
+# | margin
+from module import (foo, bar,
+ baz)
+from module import (foo,
+ bar)
+from module import (foo,
+ bar # comment
+ )
+from module import (foo,
+
+ bar, )
diff --git a/python/testData/formatter/fromImportParenthesesPlacement.py b/python/testData/formatter/fromImportParenthesesPlacement.py
new file mode 100644
index 000000000000..fb69478bd4e4
--- /dev/null
+++ b/python/testData/formatter/fromImportParenthesesPlacement.py
@@ -0,0 +1,11 @@
+from module import (foo, bar)
+from module import (foo, bar,)
+from module import (foo, bar, baz, quux)
+from module import (foo,
+ bar)
+from module import (foo,
+ bar,
+ )
+from module import (
+ foo,
+ bar)
\ No newline at end of file
diff --git a/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis.py b/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis.py
new file mode 100644
index 000000000000..fb69478bd4e4
--- /dev/null
+++ b/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis.py
@@ -0,0 +1,11 @@
+from module import (foo, bar)
+from module import (foo, bar,)
+from module import (foo, bar, baz, quux)
+from module import (foo,
+ bar)
+from module import (foo,
+ bar,
+ )
+from module import (
+ foo,
+ bar)
\ No newline at end of file
diff --git a/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis_after.py b/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis_after.py
new file mode 100644
index 000000000000..a91d6d73655d
--- /dev/null
+++ b/python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis_after.py
@@ -0,0 +1,17 @@
+from module import (foo, bar)
+from module import (foo, bar, )
+from module import (
+ foo, bar, baz, quux
+ )
+from module import (
+ foo,
+ bar
+ )
+from module import (
+ foo,
+ bar,
+ )
+from module import (
+ foo,
+ bar
+ )
diff --git a/python/testData/formatter/fromImportParenthesesPlacement_after.py b/python/testData/formatter/fromImportParenthesesPlacement_after.py
new file mode 100644
index 000000000000..ae41cfed13cc
--- /dev/null
+++ b/python/testData/formatter/fromImportParenthesesPlacement_after.py
@@ -0,0 +1,17 @@
+from module import (foo, bar)
+from module import (foo, bar, )
+from module import (
+ foo, bar, baz, quux
+)
+from module import (
+ foo,
+ bar
+)
+from module import (
+ foo,
+ bar,
+)
+from module import (
+ foo,
+ bar
+)
diff --git a/python/testData/formatter/fromImportTrailingCommaWithParentheses.py b/python/testData/formatter/fromImportTrailingCommaWithParentheses.py
new file mode 100644
index 000000000000..c45eaac8a40e
--- /dev/null
+++ b/python/testData/formatter/fromImportTrailingCommaWithParentheses.py
@@ -0,0 +1,23 @@
+from module import foo
+from module import foo, bar
+from module import foo, bar,
+# | margin
+from module import foo, bar, baz
+from module import foo, \
+ bar
+from module import foo, \
+ bar,
+
+from module import foo, \
+ bar # comment
+
+from module import (foo,
+ bar)
+
+from module import (foo,
+ bar,)
+
+from module import (
+ foo,
+ bar # comment
+)
\ No newline at end of file
diff --git a/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py b/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py
new file mode 100644
index 000000000000..0940bfab0ebb
--- /dev/null
+++ b/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py
@@ -0,0 +1,25 @@
+from module import foo
+from module import foo, bar
+from module import foo, bar,
+# | margin
+from module import (foo, bar,
+ baz, )
+from module import (foo,
+ bar, )
+from module import (foo,
+ bar, )
+
+from module import (foo,
+ bar, # comment
+ )
+
+from module import (foo,
+ bar, )
+
+from module import (foo,
+ bar, )
+
+from module import (
+ foo,
+ bar, # comment
+)
diff --git a/python/testData/formatter/fromImportTrailingCommaWithoutParentheses.py b/python/testData/formatter/fromImportTrailingCommaWithoutParentheses.py
new file mode 100644
index 000000000000..c45eaac8a40e
--- /dev/null
+++ b/python/testData/formatter/fromImportTrailingCommaWithoutParentheses.py
@@ -0,0 +1,23 @@
+from module import foo
+from module import foo, bar
+from module import foo, bar,
+# | margin
+from module import foo, bar, baz
+from module import foo, \
+ bar
+from module import foo, \
+ bar,
+
+from module import foo, \
+ bar # comment
+
+from module import (foo,
+ bar)
+
+from module import (foo,
+ bar,)
+
+from module import (
+ foo,
+ bar # comment
+)
\ No newline at end of file
diff --git a/python/testData/formatter/fromImportTrailingCommaWithoutParentheses_after.py b/python/testData/formatter/fromImportTrailingCommaWithoutParentheses_after.py
new file mode 100644
index 000000000000..bd6caf7ff688
--- /dev/null
+++ b/python/testData/formatter/fromImportTrailingCommaWithoutParentheses_after.py
@@ -0,0 +1,24 @@
+from module import foo
+from module import foo, bar
+from module import foo, bar,
+# | margin
+from module import foo, bar, \
+ baz
+from module import foo, \
+ bar
+from module import foo, \
+ bar,
+
+from module import foo, \
+ bar # comment
+
+from module import (foo,
+ bar, )
+
+from module import (foo,
+ bar, )
+
+from module import (
+ foo,
+ bar, # comment
+)
diff --git a/python/testData/formatter/fromImportWrappingChopDownIfLong.py b/python/testData/formatter/fromImportWrappingChopDownIfLong.py
new file mode 100644
index 000000000000..3d1efc572df2
--- /dev/null
+++ b/python/testData/formatter/fromImportWrappingChopDownIfLong.py
@@ -0,0 +1,4 @@
+from module import foo, bar
+from module import foo, bar, baz
+from module import (foo, bar)
+from module import (foo, bar, baz)
diff --git a/python/testData/formatter/fromImportWrappingChopDownIfLong_after.py b/python/testData/formatter/fromImportWrappingChopDownIfLong_after.py
new file mode 100644
index 000000000000..7fb0c1208f6e
--- /dev/null
+++ b/python/testData/formatter/fromImportWrappingChopDownIfLong_after.py
@@ -0,0 +1,8 @@
+from module import foo, bar
+from module import foo, \
+ bar, \
+ baz
+from module import (foo, bar)
+from module import (foo,
+ bar,
+ baz)
diff --git a/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine.py b/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine.py
new file mode 100644
index 000000000000..318891c69c57
--- /dev/null
+++ b/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine.py
@@ -0,0 +1,3 @@
+d = {"foo": 1,
+ "bar": 2
+ }
\ No newline at end of file
diff --git a/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine_after.py b/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine_after.py
new file mode 100644
index 000000000000..7f322119f4a7
--- /dev/null
+++ b/python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine_after.py
@@ -0,0 +1,4 @@
+d = {
+ "foo": 1,
+ "bar": 2
+}
diff --git a/python/testData/formatter/pyramidFromImportFormatting.py b/python/testData/formatter/pyramidFromImportFormatting.py
new file mode 100644
index 000000000000..72bf46150763
--- /dev/null
+++ b/python/testData/formatter/pyramidFromImportFormatting.py
@@ -0,0 +1,10 @@
+from module import foo
+from module import foo, bar
+from module import foo, \
+ bar
+from module import foo, \
+ bar # comment
+from module import foo, \
+ \
+ \
+ bar,
\ No newline at end of file
diff --git a/python/testData/formatter/pyramidFromImportFormatting_after.py b/python/testData/formatter/pyramidFromImportFormatting_after.py
new file mode 100644
index 000000000000..efa762b318ae
--- /dev/null
+++ b/python/testData/formatter/pyramidFromImportFormatting_after.py
@@ -0,0 +1,18 @@
+from module import foo
+from module import (
+ foo,
+ bar,
+ )
+from module import (
+ foo,
+ bar,
+ )
+from module import (
+ foo,
+ bar, # comment
+ )
+from module import (
+ foo,
+
+ bar,
+ )
diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java
index 0f217ea665d6..8f48d4970e9e 100644
--- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java
+++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java
@@ -484,6 +484,11 @@ public class PyFormatterTest extends PyTestCase {
public void testAlignmentOfClosingBraceInDictLiteralWhenNoHangingIndent() {
doTest();
}
+
+ public void testNoAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine() {
+ getPythonCodeStyleSettings().DICT_NEW_LINE_AFTER_LEFT_BRACE = true;
+ doTest();
+ }
// PY-13004
public void testAlignmentOfClosingParenthesisOfArgumentListWhenNoHangingIndent() {
@@ -733,6 +738,67 @@ public class PyFormatterTest extends PyTestCase {
doTest();
}
+ // PY-20633
+ public void testFromImportWrappingChopDownIfLong() {
+ getPythonCodeStyleSettings().FROM_IMPORT_WRAPPING = WrapType.CHOP_DOWN_IF_LONG.getLegacyRepresentation();
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
+ doTest();
+ }
+
+ // PY-20633
+ public void testFromImportParenthesesPlacement() {
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true;
+ getCommonCodeStyleSettings().SPACE_AFTER_COLON = true;
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 35);
+ doTest();
+ }
+
+ // PY-20633
+ public void testFromImportParenthesesPlacementHangClosingParenthesis() {
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true;
+ getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = true;
+ getCommonCodeStyleSettings().SPACE_AFTER_COLON = true;
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 35);
+ doTest();
+ }
+
+ // PY-20633
+ public void testFromImportForceParenthesesIfMultiline() {
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
+ getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = true;
+ doTest();
+ }
+
+ // PY-20633
+ // See http://docs.pylonsproject.org/en/latest/community/codestyle.html
+ public void testPyramidFromImportFormatting() {
+ getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_WRAPPING = WrapType.ALWAYS.getLegacyRepresentation();
+ getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
+ getPythonCodeStyleSettings().HANG_CLOSING_BRACKETS = true;
+ doTest();
+ }
+
+ // PY-9764
+ public void testFromImportTrailingCommaWithParentheses() {
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
+ getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = true;
+ getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
+ doTest();
+ }
+
+ // PY-9764
+ public void testFromImportTrailingCommaWithoutParentheses() {
+ getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30);
+ getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = false;
+ getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true;
+ doTest();
+ }
+
public void testVariableAnnotations() {
runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest);
}