From dc8bc9f4cc7417fd0966258b008f9ce83290b25a Mon Sep 17 00:00:00 2001 From: Daniil Ovchinnikov Date: Mon, 23 Jan 2017 16:07:56 +0300 Subject: [PATCH 01/25] [groovy] use class icon for script classes --- .../groovy/lang/psi/impl/synthetic/GroovyScriptClass.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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); } From 98ea9a0e8f6ddf15d906cdc9e2bd91e09ddcc22d Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Thu, 22 Dec 2016 14:17:37 +0300 Subject: [PATCH 02/25] icons.gant: split logic into several parts --- plugins/xpath/xpath-lang/gen/icons/XpathIcons.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 } From f0e7dea875b9bc9d1da17e342d26baf16fbf121f Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Thu, 12 Jan 2017 13:20:01 +0300 Subject: [PATCH 03/25] icons.gant: move to community --- .../build/scripts/IconsClassGenerator.kt | 294 ++++++++++++++++++ .../intellij/build/scripts/ImageCollector.kt | 226 ++++++++++++++ .../build/scripts/ImageSanityChecker.kt | 113 +++++++ .../build/scripts/ImageSizeOptimizer.kt | 83 +++++ .../icons/com/intellij/build/scripts/util.kt | 77 +++++ .../build-scripts/platform-build-scripts.iml | 2 + 6 files changed, 795 insertions(+) create mode 100644 platform/build-scripts/icons/com/intellij/build/scripts/IconsClassGenerator.kt create mode 100644 platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt create mode 100644 platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt create mode 100644 platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt create mode 100644 platform/build-scripts/icons/com/intellij/build/scripts/util.kt diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/IconsClassGenerator.kt b/platform/build-scripts/icons/com/intellij/build/scripts/IconsClassGenerator.kt new file mode 100644 index 000000000000..a704cfc258c2 --- /dev/null +++ b/platform/build-scripts/icons/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/com/intellij/build/scripts/ImageCollector.kt b/platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt new file mode 100644 index 000000000000..b79511bf380f --- /dev/null +++ b/platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt @@ -0,0 +1,226 @@ +/* + * 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() + + val file: File? get() = files[ImageType.BASIC] +} + +internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = true) { + 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) }) + assert(iconPaths.files[type] == null) + iconPaths.files[type] = file + } + + 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 = 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/com/intellij/build/scripts/ImageSanityChecker.kt b/platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt new file mode 100644 index 000000000000..8a2a70089599 --- /dev/null +++ b/platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt @@ -0,0 +1,113 @@ +/* + * 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.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.* + +class ImageSanityChecker(val projectHome: File) { + private val infos: StringBuilder = StringBuilder() + private val warnings: StringBuilder = StringBuilder() + + fun check(module: JpsModule) { + val allImages = ImageCollector(projectHome, false).collect(module) + + val (images, broken) = allImages.partition { it.file != null } + log(warnings, "ERROR: icons without base version found in module", module, broken) + + checkHaveRetinaVersion(images, module) + checkHaveCompleteIconSet(images, module) + checkHaveValidSize(images, module) + } + + fun printInfo() { + if (infos.isNotEmpty()) { + println("") + println(infos) + } + } + + fun printWarnings() { + if (warnings.isNotEmpty()) { + println("") + println(warnings) + } + } + + private fun checkHaveRetinaVersion(images: List, module: JpsModule) { + process(images, infos, "INFO: icons without retina version found in module", 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, warnings, "WARNING: icons without complete set of additional icons found in module", 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) { + process(images, warnings, "WARNING: icons with suspicious size found in module", module) { image -> + if (!isIcon(image.file!!)) return@process true + if (FileUtil.normalize(image.file!!.path).contains("/tips/images/")) 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 process(images: List, logger: StringBuilder, message: String, module: JpsModule, + processor: (ImagePaths) -> Boolean) { + val result = ArrayList() + images.forEach { + if (!processor(it)) result.add(it) + } + log(logger, message, module, result) + } + + private fun log(logger: StringBuilder, message: String, module: JpsModule, images: Collection) { + if (images.isEmpty()) return + logger.append("$message '${module.name}'\n") + images.sortedBy { it.id }.forEach { + val path = it.file ?: it.files.values.first() + logger.append(" ${it.id} - $path\n") + } + logger.append("\n") + } +} \ No newline at end of file diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt b/platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt new file mode 100644 index 000000000000..4ee1053e1ff9 --- /dev/null +++ b/platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt @@ -0,0 +1,83 @@ +/* + * 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.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import javax.imageio.ImageIO + +class ImageSizeOptimizer(val projectHome: File) { + private var initialTotal: Long = 0 + 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: ${initialTotal - optimizedTotal} bytes in total") + } + + private fun tryToReduceSize(file: File) { + if (!file.name.endsWith(".png")) return + val image = ImageIO.read(file) + if (image == null) { + println(file.absolutePath + " loading failed") + return + } + val byteArrayOutputStream = ByteArrayOutputStream() + ImageIO.write(image, "png", byteArrayOutputStream) + + val byteArray = byteArrayOutputStream.toByteArray() + + val initialSize = file.length() + initialTotal += initialSize + if (initialSize <= byteArray.size) { + optimizedTotal += initialSize + return + } + + optimizedTotal += byteArray.size + try { + FileUtil.writeToFile(file, byteArray) + } + catch (e: IOException) { + throw Exception("Cannot optimize " + file.absolutePath) + } + val compression = (initialSize - byteArray.size) * 100 / initialSize + println(file.absolutePath + " $compression% optimized ($initialSize->${byteArray.size} bytes)") + } +} \ No newline at end of file diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/util.kt b/platform/build-scripts/icons/com/intellij/build/scripts/util.kt new file mode 100644 index 000000000000..bed460913060 --- /dev/null +++ b/platform/build-scripts/icons/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/platform-build-scripts.iml b/platform/build-scripts/platform-build-scripts.iml index 8a0f5b366785..436bec2d501c 100644 --- a/platform/build-scripts/platform-build-scripts.iml +++ b/platform/build-scripts/platform-build-scripts.iml @@ -4,6 +4,7 @@ + @@ -32,5 +33,6 @@ + \ No newline at end of file From 3947168ec95315369a8890c107a982880e1fc3a2 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Thu, 12 Jan 2017 13:24:11 +0300 Subject: [PATCH 04/25] icons.gant: add test for icons --- community-main.iml | 1 + .../build/scripts/IconsClassGenerator.kt | 0 .../intellij/build/scripts/ImageCollector.kt | 13 +- .../build/scripts/ImageSanityChecker.kt | 155 +++++++++++------- .../build/scripts/ImageSizeOptimizer.kt | 0 .../com/intellij/build/scripts/util.kt | 0 .../build/scripts/ImageResourcesTest.kt | 108 ++++++++++++ .../build-scripts/platform-build-scripts.iml | 4 +- 8 files changed, 217 insertions(+), 64 deletions(-) rename platform/build-scripts/icons/{ => src}/com/intellij/build/scripts/IconsClassGenerator.kt (100%) rename platform/build-scripts/icons/{ => src}/com/intellij/build/scripts/ImageCollector.kt (95%) rename platform/build-scripts/icons/{ => src}/com/intellij/build/scripts/ImageSanityChecker.kt (58%) rename platform/build-scripts/icons/{ => src}/com/intellij/build/scripts/ImageSizeOptimizer.kt (100%) rename platform/build-scripts/icons/{ => src}/com/intellij/build/scripts/util.kt (100%) create mode 100644 platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt 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/platform/build-scripts/icons/com/intellij/build/scripts/IconsClassGenerator.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/IconsClassGenerator.kt similarity index 100% rename from platform/build-scripts/icons/com/intellij/build/scripts/IconsClassGenerator.kt rename to platform/build-scripts/icons/src/com/intellij/build/scripts/IconsClassGenerator.kt diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt similarity index 95% rename from platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt rename to platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt index b79511bf380f..02669bda64ba 100644 --- a/platform/build-scripts/icons/com/intellij/build/scripts/ImageCollector.kt +++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt @@ -29,11 +29,12 @@ 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] } -internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = true) { +internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = true, val ignoreSkipTag: Boolean = false) { private val result = HashMap () private val usedIconsRobots: MutableSet = HashSet() @@ -92,8 +93,12 @@ internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = tr if (skipped) return val iconPaths = result.computeIfAbsent(id, { ImagePaths(id, sourceRoot, used, deprecated) }) - assert(iconPaths.files[type] == null) - iconPaths.files[type] = file + if (iconPaths.files[type] == null) { + iconPaths.files[type] = file + } + else { + iconPaths.ambiguous = true + } } private fun upToProjectHome(dir: File): IconRobotsData { @@ -158,7 +163,7 @@ internal class ImageCollector(val projectHome: File, val iconsOnly: Boolean = tr private val used: MutableSet = HashSet() private val deprecated: MutableSet = HashSet() - fun isSkipped(file: File): Boolean = matches(file, skip) || parent?.isSkipped(file) ?: false + 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 diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt similarity index 58% rename from platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt rename to platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt index 8a2a70089599..4b5de4136a34 100644 --- a/platform/build-scripts/icons/com/intellij/build/scripts/ImageSanityChecker.kt +++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt @@ -15,6 +15,7 @@ */ 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 @@ -22,21 +23,97 @@ import java.awt.Dimension import java.io.File import java.util.* -class ImageSanityChecker(val projectHome: File) { - private val infos: StringBuilder = StringBuilder() - private val warnings: StringBuilder = StringBuilder() - +abstract class ImageSanityCheckerBase(val projectHome: File, val ignoreSkipTag: Boolean) { fun check(module: JpsModule) { - val allImages = ImageCollector(projectHome, false).collect(module) + val allImages = ImageCollector(projectHome, false, ignoreSkipTag).collect(module) val (images, broken) = allImages.partition { it.file != null } - log(warnings, "ERROR: icons without base version found in module", module, broken) + logErrors(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) + } + logErrors(severity, message, module, result) + } + + private fun logErrors(severity: Severity, message: String, module: JpsModule, images: Collection) { + log(severity, message, module, images.map { + val path = it.file ?: it.files.values.first() + Pair(it.id, path) + }) + } + + 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("") @@ -51,63 +128,23 @@ class ImageSanityChecker(val projectHome: File) { } } - private fun checkHaveRetinaVersion(images: List, module: JpsModule) { - process(images, infos, "INFO: icons without retina version found in module", module) { image -> - val hasRetina = image.files[RETINA] != null - val hasRetinaDarcula = image.files[RETINA_DARCULA] != null - return@process hasRetina || hasRetinaDarcula + override fun log(severity: Severity, message: String, module: JpsModule, images: Collection>) { + val logger = when (severity) { + ERROR -> warnings + WARNING -> warnings + INFO -> infos } - } - - private fun checkHaveCompleteIconSet(images: List, module: JpsModule) { - process(images, warnings, "WARNING: icons without complete set of additional icons found in module", 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 - } + val prefix = when (severity) { + ERROR -> "ERROR:" + WARNING -> "WARNING:" + INFO -> "INFO:" } - } - private fun checkHaveValidSize(images: List, module: JpsModule) { - process(images, warnings, "WARNING: icons with suspicious size found in module", module) { image -> - if (!isIcon(image.file!!)) return@process true - if (FileUtil.normalize(image.file!!.path).contains("/tips/images/")) 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 process(images: List, logger: StringBuilder, message: String, module: JpsModule, - processor: (ImagePaths) -> Boolean) { - val result = ArrayList() - images.forEach { - if (!processor(it)) result.add(it) - } - log(logger, message, module, result) - } - - private fun log(logger: StringBuilder, message: String, module: JpsModule, images: Collection) { if (images.isEmpty()) return - logger.append("$message '${module.name}'\n") - images.sortedBy { it.id }.forEach { - val path = it.file ?: it.files.values.first() - logger.append(" ${it.id} - $path\n") + logger.append("$prefix $message found in module '${module.name}'\n") + images.sortedBy { it.first }.forEach { + logger.append(" ${it.first} - ${it.second}\n") } logger.append("\n") } -} \ No newline at end of file +} diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt similarity index 100% rename from platform/build-scripts/icons/com/intellij/build/scripts/ImageSizeOptimizer.kt rename to platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt diff --git a/platform/build-scripts/icons/com/intellij/build/scripts/util.kt b/platform/build-scripts/icons/src/com/intellij/build/scripts/util.kt similarity index 100% rename from platform/build-scripts/icons/com/intellij/build/scripts/util.kt rename to platform/build-scripts/icons/src/com/intellij/build/scripts/util.kt 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..165d16fc7209 --- /dev/null +++ b/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt @@ -0,0 +1,108 @@ +/* + * 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 CommunityImageResourcesTest : ImageResourcesTestBase() { + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun data(): Collection> { + return ImageResourcesTestBase.collectBadIcons(false) + } + } +} + +@Ignore +class AllImageResourcesTest : ImageResourcesTestBase() { + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun data(): Collection> { + return ImageResourcesTestBase.collectBadIcons(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 home = PathManager.getHomePath() + val model = JpsElementFactory.getInstance().createModel() + + val pathVariables = JpsModelSerializationDataService.computeAllPathVariables(model.global) + JpsProjectLoader.loadProject(model.project, pathVariables, home) + + val modules = model.project.modules + + val checker = MyChecker(File(home), ignoreSkipTag) + modules.forEach { + checker.check(it) + } + return checker.collectFailures() + .sortedWith(compareBy { it.module }.thenBy { it.id }.thenBy { it.message }) + .map { arrayOf(it.getTestName(), it.getException()) } + } + } +} + +private class MyChecker(projectHome: File, ignoreSkipTag: Boolean) : ImageSanityCheckerBase(projectHome, ignoreSkipTag) { + private 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.name, message, image.first, image.second.path)) + } + } + + fun collectFailures(): Collection { + return failures + } +} + +class FailedTest(val module: String, val message: String, val id: String, val path: String) { + 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 436bec2d501c..122b8972021a 100644 --- a/platform/build-scripts/platform-build-scripts.iml +++ b/platform/build-scripts/platform-build-scripts.iml @@ -4,7 +4,8 @@ - + + @@ -34,5 +35,6 @@ + \ No newline at end of file From 4b26ecc16f34ac42472cdc936f8f4c278324e0da Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 16 Jan 2017 16:23:22 +0300 Subject: [PATCH 05/25] icons.gant: test that all icons are optimized --- .../intellij/build/scripts/ImageCollector.kt | 1 + .../build/scripts/ImageSanityChecker.kt | 19 ++-- .../build/scripts/ImageSizeOptimizer.kt | 58 +++++++----- .../build/scripts/ImageResourcesTest.kt | 88 +++++++++++++++---- 4 files changed, 114 insertions(+), 52 deletions(-) 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 index 02669bda64ba..7b3ad9825f20 100644 --- a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt +++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageCollector.kt @@ -32,6 +32,7 @@ internal class ImagePaths(val id: String, val sourceRoot: JpsModuleSourceRoot, v 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) { 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 index 4b5de4136a34..442bcceb27cf 100644 --- a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt +++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSanityChecker.kt @@ -28,7 +28,7 @@ abstract class ImageSanityCheckerBase(val projectHome: File, val ignoreSkipTag: val allImages = ImageCollector(projectHome, false, ignoreSkipTag).collect(module) val (images, broken) = allImages.partition { it.file != null } - logErrors(Severity.ERROR, "image without base version", module, broken) + log(Severity.ERROR, "image without base version", module, broken) checkHaveRetinaVersion(images, module) checkHaveCompleteIconSet(images, module) @@ -95,17 +95,10 @@ abstract class ImageSanityCheckerBase(val projectHome: File, val ignoreSkipTag: images.forEach { if (!processor(it)) result.add(it) } - logErrors(severity, message, module, result) + log(severity, message, module, result) } - private fun logErrors(severity: Severity, message: String, module: JpsModule, images: Collection) { - log(severity, message, module, images.map { - val path = it.file ?: it.files.values.first() - Pair(it.id, path) - }) - } - - abstract fun log(severity: Severity, message: String, module: JpsModule, images: Collection>) + internal abstract fun log(severity: Severity, message: String, module: JpsModule, images: Collection) enum class Severity { INFO, WARNING, ERROR } } @@ -128,7 +121,7 @@ class ImageSanityChecker(projectHome: File) : ImageSanityCheckerBase(projectHome } } - override fun log(severity: Severity, message: String, module: JpsModule, images: Collection>) { + override fun log(severity: Severity, message: String, module: JpsModule, images: Collection) { val logger = when (severity) { ERROR -> warnings WARNING -> warnings @@ -142,8 +135,8 @@ class ImageSanityChecker(projectHome: File) : ImageSanityCheckerBase(projectHome if (images.isEmpty()) return logger.append("$prefix $message found in module '${module.name}'\n") - images.sortedBy { it.first }.forEach { - logger.append(" ${it.first} - ${it.second}\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 index 4ee1053e1ff9..0864d3b8e649 100644 --- a/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt +++ b/platform/build-scripts/icons/src/com/intellij/build/scripts/ImageSizeOptimizer.kt @@ -17,13 +17,13 @@ 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 initialTotal: Long = 0 private var optimizedTotal: Long = 0 fun optimizeIcons(module: JpsModule) { @@ -48,36 +48,50 @@ class ImageSizeOptimizer(val projectHome: File) { fun printStats() { println("") - println("PNG size optimization: ${initialTotal - optimizedTotal} bytes in total") + println("PNG size optimization: $optimizedTotal bytes in total") } private fun tryToReduceSize(file: File) { - if (!file.name.endsWith(".png")) return - val image = ImageIO.read(file) - if (image == null) { - println(file.absolutePath + " loading failed") - return - } - val byteArrayOutputStream = ByteArrayOutputStream() - ImageIO.write(image, "png", byteArrayOutputStream) + val image = optimizeImage(file) ?: return - val byteArray = byteArrayOutputStream.toByteArray() + if (image.hasOptimumSize) return - val initialSize = file.length() - initialTotal += initialSize - if (initialSize <= byteArray.size) { - optimizedTotal += initialSize - return - } - - optimizedTotal += byteArray.size try { - FileUtil.writeToFile(file, byteArray) + FileUtil.writeToFile(file, image.optimizedArray) + optimizedTotal += image.sizeBefore - image.sizeAfter } catch (e: IOException) { throw Exception("Cannot optimize " + file.absolutePath) } - val compression = (initialSize - byteArray.size) * 100 / initialSize - println(file.absolutePath + " $compression% optimized ($initialSize->${byteArray.size} bytes)") + 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/tests/com/intellij/build/scripts/ImageResourcesTest.kt b/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt index 165d16fc7209..754d23b401d1 100644 --- a/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt +++ b/platform/build-scripts/icons/tests/com/intellij/build/scripts/ImageResourcesTest.kt @@ -31,7 +31,7 @@ import java.util.* import kotlin.comparisons.compareBy import kotlin.comparisons.thenBy -class CommunityImageResourcesTest : ImageResourcesTestBase() { +class CommunityImageResourcesSanityTest : ImageResourcesTestBase() { companion object { @JvmStatic @Parameters(name = "{0}") @@ -41,8 +41,18 @@ class CommunityImageResourcesTest : ImageResourcesTestBase() { } } +class CommunityImageResourcesOptimumSizeTest : ImageResourcesTestBase() { + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun data(): Collection> { + return ImageResourcesTestBase.collectIconsWithNonOptimumSize(false) + } + } +} + @Ignore -class AllImageResourcesTest : ImageResourcesTestBase() { +class AllImageResourcesSanityTest : ImageResourcesTestBase() { companion object { @JvmStatic @Parameters(name = "{0}") @@ -52,6 +62,18 @@ class AllImageResourcesTest : ImageResourcesTestBase() { } } +@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 @@ -65,44 +87,76 @@ abstract class ImageResourcesTestBase { 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) - val modules = model.project.modules - - val checker = MyChecker(File(home), ignoreSkipTag) - modules.forEach { - checker.check(it) - } - return checker.collectFailures() - .sortedWith(compareBy { it.module }.thenBy { it.id }.thenBy { it.message }) - .map { arrayOf(it.getTestName(), it.getException()) } + model.project.modules.forEach(action) } } } -private class MyChecker(projectHome: File, ignoreSkipTag: Boolean) : ImageSanityCheckerBase(projectHome, ignoreSkipTag) { - private val failures = ArrayList() +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>) { + images: Collection) { if (severity == Severity.INFO) return images.forEach { image -> - failures.add(FailedTest(module.name, message, image.first, image.second.path)) + failures.add(FailedTest(module, message, image)) } } +} - fun collectFailures(): Collection { - return failures +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 From 16173b30c0f6fa24b11c78b2201eaed84f809863 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 23 Jan 2017 15:25:05 +0300 Subject: [PATCH 06/25] run icons.gant --- platform/icons/src/fileTypes/jsonSchema.png | Bin 354 -> 219 bytes platform/icons/src/fileTypes/jsonSchema@2x.png | Bin 579 -> 363 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/platform/icons/src/fileTypes/jsonSchema.png b/platform/icons/src/fileTypes/jsonSchema.png index 7494963c3c3708a6ee74b0edd86e3677f5096f58..e32706ba7309a831b18363ed0a1519a02767ce84 100644 GIT binary patch delta 191 zcmV;w06_oZ0^0$QB!8kwL_t(|+GAkA0%k4WkPoFdVZ#jg07x@T5y=L?G?QfjvSzYf zfW=UfT(F5)JV*_o2smteunC3@)2pxOMP=Pa7Q=@z4A64&d11Hq%2srR7Tqfk>jo#? z!)t(9^Za8vzR?e1e2@mynyIHiY>+rX13*rnUl1Rp;p?%j5+L@ML!0FAMkCmk>!(F|-6HP8_{_Wl zKffN^F8Am4Gp>JMKeF??rQg8o0uD{n*PnN-P=xXMe3R}m3yOUMd5c|D{|&pc?kBtk za2Y$k0onBL`#=|Fuq4ncd>%3PK;GhYi+o5_AcAcAuy&dnjL#~q^i3qQ^&%`>&;dYz Z0RWG$V)efa@@xPA002ovPDHLkV1is;m9YQ- diff --git a/platform/icons/src/fileTypes/jsonSchema@2x.png b/platform/icons/src/fileTypes/jsonSchema@2x.png index 57d2586ffbadf32cb957a98299e14e967a78b3d6..b741ca9d9da67ebbcfddde8ecc892d579805f3e2 100644 GIT binary patch delta 337 zcmV-X0j~bT1nUBjBYy!hNkluf77Z1SE%Sk#2C(J&-uD@lVhJAcJ5IK=%>MG7t^oo7PM{1>&PS zmf|RY8H^kSAW96f1Qr#b5Wy8KG)aWWWdtnFNlADlIRI7&;R;q*A|)l^kyQ?WEW)J* j6a}OtyrEST(;5Q+3Han}*$@W000000NkvXXu0mjfy90s% delta 555 zcmV+`0@VHM0>cE5BYyxHbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUz z>e4eqlIke5+6JzL? zeQTsaG>=orQ>x_xR!N0##HjoG?j`&8E5{{3m@3(jMMMg0@t^mv*pYqsY5QV1Z0bMn zSt@#SUO%=lly}|Ns6K z9$NhVsrQ%##lC?;;luiws^G+WY^%iI_pi7>j^(oScz;2aq8OBlxlEj%{`>lo9XSnv zfM8V7bw)P!zaUFLZk(<3?aV$%%D3}-#Kg(-muhvv{~zC(v1$ZbA`qN?9US+!FN%Od zgGEB_GqG8fXb1dyc8?F74Uy6SJZnlTedBQqh9@>+6COzpU}EF=1EPOiJ1vM!p3B(r t4ZEuT8zfQ^9!Yfpt&8FTuZ#c!3;@x`%yX-Ii_ri8002ovPDHLkV1mOt2ao^& From 4d641af8bc1482be977cd8fe2e8f4f88d23c9a55 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 23 Jan 2017 15:31:09 +0300 Subject: [PATCH 07/25] icons: fix typos in file name --- ...menu@_dark.png => breakpoint_speedmenu_dark.png} | Bin ...menu@_dark.png => breakpoint_speedmenu_dark.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename python/educational-python/resources/tips/images/{breakpoint_speedmenu@_dark.png => breakpoint_speedmenu_dark.png} (100%) rename python/python-community-ide-resources/resources/tips/images/{breakpoint_speedmenu@_dark.png => breakpoint_speedmenu_dark.png} (100%) 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 From deee2a3fcb967c0d884f8333328c74a51fe591cb Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 23 Jan 2017 15:52:38 +0300 Subject: [PATCH 08/25] icons: remove @2x_dark icons that are equal to the @2x ones --- platform/icons/src/actions/help@2x_dark.png | Bin 1092 -> 0 bytes platform/icons/src/actions/menu-find@2x_dark.png | Bin 701 -> 0 bytes platform/icons/src/actions/menu-help@2x_dark.png | Bin 1092 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 platform/icons/src/actions/help@2x_dark.png delete mode 100644 platform/icons/src/actions/menu-find@2x_dark.png delete mode 100644 platform/icons/src/actions/menu-help@2x_dark.png 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 fef9b644ecdb949e285a32be36d93ae72fb5eecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1092 zcmV-K1iSl*P)*n* zq;kYsJ7$gBhxIAmAF7G;sell-f+N##l0?p`tL9XT|!W zC~x52etdSZF9K1lev5UqS?kCL2bm8j;5@9Yh!lBANjDWqlAQ&i+jz{wWf!5=@wuVl zfvJH34KfjSuds7Z;5k122XX&|OizT6UX>^csWlMO(C{F&T3X}YvA(+C>i(^B2zG`b zH}=`K&Yc1_8f({<#%u$2;QDjiJ8D@4ZfnS*B1IZRqDXD;y9@HXI0ML*p{hr1#l=GQ{iIp0Oy3O zq`wb4{1ODfk{Yc6j?GEI^1Jk1u)hsFtzqx$@pH~H#Kb?0XQUa7|!cj*ei z-gs|@C!Pd3F6}~bpz8g7XL_HomrlT2ob@?I!Fe?nTGUqKn4{ha$X?pe`9S4r2oQARY=js0S;MErUoxVQ=1y^{^^LSHtqK;KdGg zs6$AXJXxS%5S@Zdiw^y$Tgx7D2SjOKYfRyctD1`a9!k@*9a_$pmr7*0uPns zSfJiA8=M02^dDz|yDRMY7)Wy>*O*42)^czL zxeKMB8l*#@E*;?Fmn`?fY3(SU5`i>U&l{uoh!Cj(M~#Em2)>FOPonk%DL^;ecaea` z^EH}qVV4mXzW}~iF0ukoN0k84!|n&_YKf!*8dMUyCevO>AdxM$K$Jvw#~CQ1fK4L1>n$j< jfGLr+&Vl`6-v#~whI2};@t|Of00000NkvXXu0mjfZ;UsQ 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 fef9b644ecdb949e285a32be36d93ae72fb5eecd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1092 zcmV-K1iSl*P)*n* zq;kYsJ7$gBhxIAmAF7G;sell-f+N##l0?p`tL9XT|!W zC~x52etdSZF9K1lev5UqS?kCL2bm8j;5@9Yh!lBANjDWqlAQ&i+jz{wWf!5=@wuVl zfvJH34KfjSuds7Z;5k122XX&|OizT6UX>^csWlMO(C{F&T3X}YvA(+C>i(^B2zG`b zH}=`K&Yc1_8f({<#%u$2;QDjiJ8D@4ZfnS*B1IZRqDXD;y9@HXI0ML*p{hr1#l=GQ{iIp0Oy3O zq`wb4{1ODfk{Yc6j?GEI^1Jk1u)hsFtzqx$@pH~H#Kb?0XQUa7|!cj*ei z-gs|@C!Pd3F6}~bpz8g7XL_HomrlT2ob@?I!Fe?nTGUqKn4{ha$X?pe`9S4r Date: Mon, 23 Jan 2017 16:03:07 +0300 Subject: [PATCH 09/25] vcs: remove unused icon --- .../dvcs-impl/resources/icons/loading_old@2x.gif | Bin 6488 -> 0 bytes .../resources/icons/loading_old@2x_dark.gif | Bin 6585 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 platform/dvcs-impl/resources/icons/loading_old@2x.gif delete mode 100644 platform/dvcs-impl/resources/icons/loading_old@2x_dark.gif 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 7e6d0c3899eba293379d244f3f9d31a97c90d34d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6488 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 F{{vVJ@-P4Z From 5a4d2b160ff867a36bcfc634f3fdf29e6b5e7351 Mon Sep 17 00:00:00 2001 From: Anton Makeev Date: Mon, 23 Jan 2017 14:09:21 +0100 Subject: [PATCH 10/25] EmptyProgressIndicator.notNullize doesn't log info --- .../com/intellij/openapi/progress/EmptyProgressIndicator.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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(); } } From 3a219b6c0109da0da25e3ab852aa9b91ab2c5331 Mon Sep 17 00:00:00 2001 From: "Egor.Ushakov" Date: Mon, 23 Jan 2017 16:01:27 +0300 Subject: [PATCH 11/25] fixed popup borders with new hidpi --- platform/util/src/com/intellij/ui/PopupBorder.java | 5 +++-- .../src/com/intellij/ui/paint/RectanglePainter.java | 2 +- .../src/com/intellij/util/ui/RegionPainter.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) rename platform/{platform-api => util}/src/com/intellij/ui/paint/RectanglePainter.java (98%) rename platform/{platform-api => util}/src/com/intellij/util/ui/RegionPainter.java (99%) 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. From edac6e487445ddaa36b927ad7ca1e32cf27f2bc6 Mon Sep 17 00:00:00 2001 From: Anna Kozlova Date: Mon, 23 Jan 2017 16:20:58 +0300 Subject: [PATCH 12/25] make static: fix inner class check (IDEA-166983) --- .../makeStatic/MakeStaticJavaCallerChooser.java | 5 +++-- .../testData/refactoring/makeMethodStatic/afterDeep.java | 9 +++++++++ .../refactoring/makeMethodStatic/beforeDeep.java | 9 +++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) 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 From 1ebf5bca4896208347e5ac1492e88f6552363ed3 Mon Sep 17 00:00:00 2001 From: "Vassiliy.Kudryashov" Date: Mon, 23 Jan 2017 16:32:49 +0300 Subject: [PATCH 13/25] IDEA-166759 The cursor while over one ToolWindow sees another border toolwindow, located behind --- .../src/com/intellij/openapi/ui/ThreeComponentsSplitter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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; From cfe73f8927d34488c8edd49447d164b9c4daca42 Mon Sep 17 00:00:00 2001 From: "Egor.Ushakov" Date: Mon, 23 Jan 2017 17:07:00 +0300 Subject: [PATCH 14/25] honor capture point enabled state --- .../com/intellij/debugger/engine/DebugProcessEvents.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); + } } } } From d92daf36b2a73ba812c88afc36548a5a39326b37 Mon Sep 17 00:00:00 2001 From: Dmitry Avdeev Date: Mon, 23 Jan 2017 17:05:29 +0300 Subject: [PATCH 15/25] IDEA-166807 IntelliJ does not recognize and deletes project shared file templates --- .../ide/fileTemplates/impl/FTManager.java | 13 +++++- .../impl/FileTemplateManagerImpl.java | 1 - .../impl/LightFileTemplatesTest.java | 44 ++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) 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-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); From a924224414597076fdab32eb412d126c36e596e7 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Mon, 23 Jan 2017 17:10:01 +0300 Subject: [PATCH 16/25] IDEA-166179 [followup] Font size is not preserved after updating to EAP build JBUI.scaleFontSize shouldn't be used to restore user's font settings as it may screw it. --- .../openapi/editor/colors/impl/AbstractColorsScheme.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 657c0f69819168e2e3e0bd9be95f0a40ec9957ea Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 17 Jan 2017 17:02:55 +0300 Subject: [PATCH 17/25] PY-10182 Add examples for "Hang closing brackets" in code style preview --- .../PyLanguageCodeStyleSettingsProvider.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index 310d9b24df82..47b61fa9394a 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -178,15 +178,29 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin " 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" + + " baz)\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" + "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" + + " items]\n" + + "\n" + + "ingredients = [\n" + + " 'green',\n" + + " 'eggs',\n" + + "]\n" + + "\n" + "if True: pass\n\n" + "try: pass\n" + "finally: pass\n"; From 2e90002d699962323a2cc50aecd83ede26f6e796 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 17 Jan 2017 18:14:20 +0300 Subject: [PATCH 18/25] PY-20633 Allow to configure wrapping policy for names in a "from" import --- .../com/jetbrains/python/PyBundle.properties | 2 +- .../jetbrains/python/formatter/PyBlock.java | 37 ++++++++++++------- .../python/formatter/PyCodeStyleSettings.java | 5 +++ .../PyLanguageCodeStyleSettingsProvider.java | 13 ++++--- .../fromImportsWrappingChopDownIfLong.py | 4 ++ ...fromImportsWrappingChopDownIfLong_after.py | 8 ++++ .../com/jetbrains/python/PyFormatterTest.java | 7 ++++ 7 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 python/testData/formatter/fromImportsWrappingChopDownIfLong.py create mode 100644 python/testData/formatter/fromImportsWrappingChopDownIfLong_after.py diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 2faa0aa3aa90..97e301e7dd52 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -1033,7 +1033,7 @@ 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.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..46810209418a 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(); @@ -370,7 +379,7 @@ public class PyBlock implements ASTBlock { } } if (childType == PyElementTypes.KEY_VALUE_EXPRESSION && isChildOfDictLiteral(child)) { - wrap = myDictWrapping; + childWrap = myDictWrapping; childIndent = Indent.getNormalIndent(); } @@ -406,7 +415,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) { diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index 26133f0842ce..e103352a5cd5 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -93,6 +93,11 @@ 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. + */ + public int FROM_IMPORT_WRAPPING = WrapType.NORMAL.getLegacyRepresentation(); + /** * 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/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index 47b61fa9394a..94c2037903e1 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -119,9 +119,11 @@ 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, "DICT_WRAPPING", PyBundle.message("formatter.dictionary.literals"), null, WRAP_OPTIONS, WRAP_VALUES); @@ -177,14 +179,14 @@ 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" + + 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" + @@ -201,7 +203,8 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin " 'eggs',\n" + "]\n" + "\n" + - "if True: pass\n\n" + + "if True: pass\n" + + "\n" + "try: pass\n" + "finally: pass\n"; @SuppressWarnings("FieldCanBeLocal") diff --git a/python/testData/formatter/fromImportsWrappingChopDownIfLong.py b/python/testData/formatter/fromImportsWrappingChopDownIfLong.py new file mode 100644 index 000000000000..3d1efc572df2 --- /dev/null +++ b/python/testData/formatter/fromImportsWrappingChopDownIfLong.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/fromImportsWrappingChopDownIfLong_after.py b/python/testData/formatter/fromImportsWrappingChopDownIfLong_after.py new file mode 100644 index 000000000000..7fb0c1208f6e --- /dev/null +++ b/python/testData/formatter/fromImportsWrappingChopDownIfLong_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/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 0f217ea665d6..551b5297ca12 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -733,6 +733,13 @@ 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(); + } + public void testVariableAnnotations() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } From 17c24b9daf4ae08e6f477927d90801917ce3f4d8 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 18 Jan 2017 18:29:23 +0300 Subject: [PATCH 19/25] PY-20633 Options to put open/close parentheses in "from" imports on their own lines --- .../python/formatter/PyCodeStyleSettings.java | 2 ++ .../PyLanguageCodeStyleSettingsProvider.java | 7 +++++++ .../formatter/PythonFormattingModelBuilder.java | 13 ++++++++----- .../formatter/fromImportParenthesesPlacement.py | 11 +++++++++++ .../fromImportParenthesesPlacement_after.py | 17 +++++++++++++++++ ...g.py => fromImportWrappingChopDownIfLong.py} | 0 ...> fromImportWrappingChopDownIfLong_after.py} | 0 .../com/jetbrains/python/PyFormatterTest.java | 10 ++++++++++ 8 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 python/testData/formatter/fromImportParenthesesPlacement.py create mode 100644 python/testData/formatter/fromImportParenthesesPlacement_after.py rename python/testData/formatter/{fromImportsWrappingChopDownIfLong.py => fromImportWrappingChopDownIfLong.py} (100%) rename python/testData/formatter/{fromImportsWrappingChopDownIfLong_after.py => fromImportWrappingChopDownIfLong_after.py} (100%) diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index e103352a5cd5..64ca601e0d03 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -97,6 +97,8 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { * Affects wrapping of multiple imported names in a single "from" import. */ 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; /** * Corresponds to the option of pycodestyle.py "--hang-closing". Basically, it means that the closing brace of a collection literal, diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index 94c2037903e1..5e7ade520905 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -119,11 +119,18 @@ 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.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, "DICT_WRAPPING", PyBundle.message("formatter.dictionary.literals"), null, WRAP_OPTIONS, WRAP_VALUES); 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/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/fromImportParenthesesPlacement_after.py b/python/testData/formatter/fromImportParenthesesPlacement_after.py new file mode 100644 index 000000000000..fbf0e7a00dc0 --- /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/fromImportsWrappingChopDownIfLong.py b/python/testData/formatter/fromImportWrappingChopDownIfLong.py similarity index 100% rename from python/testData/formatter/fromImportsWrappingChopDownIfLong.py rename to python/testData/formatter/fromImportWrappingChopDownIfLong.py diff --git a/python/testData/formatter/fromImportsWrappingChopDownIfLong_after.py b/python/testData/formatter/fromImportWrappingChopDownIfLong_after.py similarity index 100% rename from python/testData/formatter/fromImportsWrappingChopDownIfLong_after.py rename to python/testData/formatter/fromImportWrappingChopDownIfLong_after.py diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 551b5297ca12..8843ae87bb97 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -740,6 +740,16 @@ public class PyFormatterTest extends PyTestCase { doTest(); } + // PY-20633 + // XXX causes jumping closing parenthesis, because of the hanging indent detection + 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(); + } + public void testVariableAnnotations() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } From c5a51bf66597c00ad284acc63a798e7f65a4061d Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 18 Jan 2017 19:32:05 +0300 Subject: [PATCH 20/25] PY-20633 Don't need to call reformat twice to move right parenthesis in from imports sometimes --- .../com/jetbrains/python/formatter/PyBlock.java | 3 ++- ...arenthesesPlacementHangClosingParenthesis.py | 11 +++++++++++ ...esesPlacementHangClosingParenthesis_after.py | 17 +++++++++++++++++ .../fromImportParenthesesPlacement_after.py | 6 +++--- .../com/jetbrains/python/PyFormatterTest.java | 11 ++++++++++- 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis.py create mode 100644 python/testData/formatter/fromImportParenthesesPlacementHangClosingParenthesis_after.py diff --git a/python/src/com/jetbrains/python/formatter/PyBlock.java b/python/src/com/jetbrains/python/formatter/PyBlock.java index 46810209418a..6f554254bf7b 100644 --- a/python/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/src/com/jetbrains/python/formatter/PyBlock.java @@ -295,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) { 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 index fbf0e7a00dc0..ae41cfed13cc 100644 --- a/python/testData/formatter/fromImportParenthesesPlacement_after.py +++ b/python/testData/formatter/fromImportParenthesesPlacement_after.py @@ -2,15 +2,15 @@ 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/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 8843ae87bb97..02544de4aca9 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -741,7 +741,6 @@ public class PyFormatterTest extends PyTestCase { } // PY-20633 - // XXX causes jumping closing parenthesis, because of the hanging indent detection public void testFromImportParenthesesPlacement() { getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_AFTER_LEFT_PARENTHESIS = true; getPythonCodeStyleSettings().FROM_IMPORT_NEW_LINE_BEFORE_RIGHT_PARENTHESIS = true; @@ -749,6 +748,16 @@ public class PyFormatterTest extends PyTestCase { 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(); + } public void testVariableAnnotations() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); From 329c03abd9a8f0e5cbb9eb02e20710c7eff7950c Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 18 Jan 2017 19:53:16 +0300 Subject: [PATCH 21/25] Consider option about new line after opening brace in dicts when aligning closing one Because this setting causes hanging indent to appear on reformat. --- python/src/com/jetbrains/python/formatter/PyBlock.java | 3 ++- ...ingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine.py | 3 +++ ...ceInDictLiteralWhenOpeningBraceIsForcedOnNewLine_after.py | 4 ++++ python/testSrc/com/jetbrains/python/PyFormatterTest.java | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine.py create mode 100644 python/testData/formatter/noAlignmentClosingBraceInDictLiteralWhenOpeningBraceIsForcedOnNewLine_after.py diff --git a/python/src/com/jetbrains/python/formatter/PyBlock.java b/python/src/com/jetbrains/python/formatter/PyBlock.java index 6f554254bf7b..e3427af2cc5b 100644 --- a/python/src/com/jetbrains/python/formatter/PyBlock.java +++ b/python/src/com/jetbrains/python/formatter/PyBlock.java @@ -617,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/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/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 02544de4aca9..831363ec3d21 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() { From 6f7faffcc317dab74d3c644e3ff7a40c34a52068 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Wed, 18 Jan 2017 22:08:03 +0300 Subject: [PATCH 22/25] PY-20633 Allow to forcibly surround "from" imports with parentheses As in case with similar options for braces in Java and other languages there are three variants of behavior: don't force, force always, and force if the corresponding statement spans multiple lines. In either way, the option is applied only if "from" import contains more than one imported name. PY-20633 Allow to wrap only multiline "from" imports in parentheses --- python/src/META-INF/python-core-common.xml | 1 + .../com/jetbrains/python/PyBundle.properties | 1 + .../python/formatter/PyCodeStyleSettings.java | 21 +++ .../PyFromImportPostFormatProcessor.java | 128 ++++++++++++++++++ .../PyLanguageCodeStyleSettingsProvider.java | 4 + .../fromImportForceParenthesesAlways.py | 12 ++ .../fromImportForceParenthesesAlways_after.py | 13 ++ .../fromImportForceParenthesesIfMultiline.py | 12 ++ ...ImportForceParenthesesIfMultiline_after.py | 13 ++ .../formatter/pyramidFromImportFormatting.py | 10 ++ .../pyramidFromImportFormatting_after.py | 18 +++ .../com/jetbrains/python/PyFormatterTest.java | 26 ++++ 12 files changed, 259 insertions(+) create mode 100644 python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java create mode 100644 python/testData/formatter/fromImportForceParenthesesAlways.py create mode 100644 python/testData/formatter/fromImportForceParenthesesAlways_after.py create mode 100644 python/testData/formatter/fromImportForceParenthesesIfMultiline.py create mode 100644 python/testData/formatter/fromImportForceParenthesesIfMultiline_after.py create mode 100644 python/testData/formatter/pyramidFromImportFormatting.py create mode 100644 python/testData/formatter/pyramidFromImportFormatting_after.py 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 97e301e7dd52..0ab87e4aafc0 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -1034,6 +1034,7 @@ 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.from.import.statements="From" Import Statements +formatter.from.import.statements.force.parentheses=Force parentheses formatter.dictionary.literals=Dictionary literals formatter.hang.closing.brackets=Hang closing brackets diff --git a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index 64ca601e0d03..21ec073e7662 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; @@ -96,9 +104,22 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { /** * 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 int FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE; /** * Corresponds to the option of pycodestyle.py "--hang-closing". Basically, it means that the closing brace of a collection literal, 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..7b0132861177 --- /dev/null +++ b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java @@ -0,0 +1,128 @@ +/* + * 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.jetbrains.python.psi.*; +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)) { + final PyImportElement[] importedNames = node.getImportElements(); + final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class); + final PsiElement leftParen = node.getLeftParen(); + final boolean enabledInSettings = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS || + pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE && + PostFormatProcessorHelper.isMultiline(node); + if (enabledInSettings && importedNames.length > 1 && leftParen == 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 firstName = fromImport.getImportElements()[0]; + final String beforeFirstName = fromImport.getText().substring(0, firstName.getStartOffsetInParent()); + final StringBuilder newStatementText = new StringBuilder(beforeFirstName); + newStatementText.append("("); + boolean lastElementWasComment = false; + for (PsiElement cur = firstName; cur != null; cur = cur.getNextSibling()) { + if (cur instanceof PsiWhiteSpace) { + newStatementText.append(cur.getText().replace("\\", "")); + } + else { + newStatementText.append(cur.getText()); + } + lastElementWasComment = cur instanceof PsiComment; + } + if (lastElementWasComment) { + newStatementText.append("\n"); + } + newStatementText.append(")"); + + final PyElementGenerator generator = PyElementGenerator.getInstance(fromImport.getProject()); + final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(fromImport.getProject()); + + 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); + myHelper.updateResultRange(fromImport.getTextLength(), newFromImport.getTextLength()); + return newFromImport; + } + } + +} diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index 5e7ade520905..e7d004a91991 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -131,6 +131,10 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin 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", + PyBundle.message("formatter.from.import.statements.force.parentheses"), + PyBundle.message("formatter.from.import.statements"), + BRACE_OPTIONS, BRACE_VALUES); consumer.showCustomOption(PyCodeStyleSettings.class, "DICT_WRAPPING", PyBundle.message("formatter.dictionary.literals"), null, WRAP_OPTIONS, WRAP_VALUES); diff --git a/python/testData/formatter/fromImportForceParenthesesAlways.py b/python/testData/formatter/fromImportForceParenthesesAlways.py new file mode 100644 index 000000000000..4367abfb876b --- /dev/null +++ b/python/testData/formatter/fromImportForceParenthesesAlways.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/fromImportForceParenthesesAlways_after.py b/python/testData/formatter/fromImportForceParenthesesAlways_after.py new file mode 100644 index 000000000000..8f29c68abdd7 --- /dev/null +++ b/python/testData/formatter/fromImportForceParenthesesAlways_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/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/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..da4948c9daca --- /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 831363ec3d21..264d7e4b9dfb 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -20,6 +20,7 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.formatter.PyCodeStyleSettings; @@ -764,6 +765,31 @@ public class PyFormatterTest extends PyTestCase { doTest(); } + // PY-20633 + public void testFromImportForceParenthesesAlways() { + getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; + doTest(); + } + + // PY-20633 + public void testFromImportForceParenthesesIfMultiline() { + getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE; + doTest(); + } + + // PY-20633 + // See http://docs.pylonsproject.org/en/latest/community/codestyle.html + public void testPyramidFromImportFormatting() { + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; + 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().HANG_CLOSING_BRACKETS = true; + doTest(); + } + public void testVariableAnnotations() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } From 466af873732503963c1d77e415ba5cc7b35d9dfb Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Fri, 20 Jan 2017 18:53:30 +0300 Subject: [PATCH 23/25] PY-9764 Allow to force trailing comma in multiline "from" import statements --- .../com/jetbrains/python/PyBundle.properties | 1 + .../python/formatter/PyCodeStyleSettings.java | 1 + .../PyFromImportPostFormatProcessor.java | 102 ++++++++++++------ .../PyLanguageCodeStyleSettingsProvider.java | 3 + .../fromImportTrailingCommaWithParentheses.py | 23 ++++ ...mportTrailingCommaWithParentheses_after.py | 27 +++++ ...omImportTrailingCommaWithoutParentheses.py | 23 ++++ ...rtTrailingCommaWithoutParentheses_after.py | 24 +++++ .../pyramidFromImportFormatting_after.py | 6 +- .../com/jetbrains/python/PyFormatterTest.java | 17 +++ 10 files changed, 190 insertions(+), 37 deletions(-) create mode 100644 python/testData/formatter/fromImportTrailingCommaWithParentheses.py create mode 100644 python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py create mode 100644 python/testData/formatter/fromImportTrailingCommaWithoutParentheses.py create mode 100644 python/testData/formatter/fromImportTrailingCommaWithoutParentheses_after.py diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 0ab87e4aafc0..3820a1a69a5b 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -1035,6 +1035,7 @@ formatter.align.when.multiline=Align when multiline formatter.collections.and.comprehensions=Collections and Comprehensions formatter.from.import.statements="From" Import Statements formatter.from.import.statements.force.parentheses=Force parentheses +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/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index 21ec073e7662..1c48174e4a43 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -120,6 +120,7 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { CommonCodeStyleSettings.FORCE_BRACES_ALWAYS} ) public int FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE; + 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, diff --git a/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java index 7b0132861177..ab87b880cb09 100644 --- a/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java +++ b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java @@ -25,7 +25,10 @@ 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; @@ -57,14 +60,24 @@ public class PyFromImportPostFormatProcessor implements PostFormatProcessor { @Override public void visitPyFromImportStatement(PyFromImportStatement node) { if (myHelper.isElementFullyInRange(node)) { - final PyImportElement[] importedNames = node.getImportElements(); - final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class); - final PsiElement leftParen = node.getLeftParen(); - final boolean enabledInSettings = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS || - pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE && - PostFormatProcessorHelper.isMultiline(node); - if (enabledInSettings && importedNames.length > 1 && leftParen == null) { - myImportStatements.add(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 forcedParentheses = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS || + pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_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 (forcedParentheses && openingParen == null || forcedComma && missingComma && openingParen != null) { + myImportStatements.add(node); + } } } } @@ -94,34 +107,55 @@ public class PyFromImportPostFormatProcessor implements PostFormatProcessor { @NotNull private PyFromImportStatement replaceFromImport(@NotNull PyFromImportStatement fromImport) { - final PyImportElement firstName = fromImport.getImportElements()[0]; - final String beforeFirstName = fromImport.getText().substring(0, firstName.getStartOffsetInParent()); - final StringBuilder newStatementText = new StringBuilder(beforeFirstName); - newStatementText.append("("); - boolean lastElementWasComment = false; - for (PsiElement cur = firstName; cur != null; cur = cur.getNextSibling()) { - if (cur instanceof PsiWhiteSpace) { - newStatementText.append(cur.getText().replace("\\", "")); - } - else { - newStatementText.append(cur.getText()); - } - lastElementWasComment = cur instanceof PsiComment; - } - if (lastElementWasComment) { - newStatementText.append("\n"); - } - newStatementText.append(")"); - + final PyImportElement[] allNames = fromImport.getImportElements(); + final PyImportElement firstName = allNames[0]; final PyElementGenerator generator = PyElementGenerator.getInstance(fromImport.getProject()); final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(fromImport.getProject()); - - 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); - myHelper.updateResultRange(fromImport.getTextLength(), newFromImport.getTextLength()); - return newFromImport; + + 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 e7d004a91991..bdd2ce2a88da 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -135,6 +135,9 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin PyBundle.message("formatter.from.import.statements.force.parentheses"), PyBundle.message("formatter.from.import.statements"), BRACE_OPTIONS, BRACE_VALUES); + 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); 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..1857e3d30298 --- /dev/null +++ b/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py @@ -0,0 +1,27 @@ +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/pyramidFromImportFormatting_after.py b/python/testData/formatter/pyramidFromImportFormatting_after.py index da4948c9daca..efa762b318ae 100644 --- a/python/testData/formatter/pyramidFromImportFormatting_after.py +++ b/python/testData/formatter/pyramidFromImportFormatting_after.py @@ -1,15 +1,15 @@ from module import foo from module import ( foo, - bar + bar, ) from module import ( foo, - bar + bar, ) from module import ( foo, - bar # comment + bar, # comment ) from module import ( foo, diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index 264d7e4b9dfb..dd6048fa5525 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -786,10 +786,27 @@ public class PyFormatterTest extends PyTestCase { 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 = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; + getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true; + doTest(); + } + + // PY-9764 + public void testFromImportTrailingCommaWithoutParentheses() { + getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE; + getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true; + doTest(); + } + public void testVariableAnnotations() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } From c0880210abf1bf9016e8280f0f29a745ec0c6a4a Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 23 Jan 2017 16:08:14 +0300 Subject: [PATCH 24/25] PY-20633 Wrap only multiline "from" imports in parentheses Effectively, I've removed "Always" variant of "Force parentheses" option. It doesn't seem that people ever need to wrap one line "from" imports in parentheses, so there is no point in making code style settings more complicated. We can return combo box with "Always" variant later if someone specifically asks for it. --- .../src/com/jetbrains/python/PyBundle.properties | 2 +- .../python/formatter/PyCodeStyleSettings.java | 2 +- .../PyFromImportPostFormatProcessor.java | 6 ++---- .../PyLanguageCodeStyleSettingsProvider.java | 7 +++---- .../fromImportForceParenthesesAlways.py | 12 ------------ .../fromImportForceParenthesesAlways_after.py | 13 ------------- ...omImportTrailingCommaWithParentheses_after.py | 6 ++---- .../com/jetbrains/python/PyFormatterTest.java | 16 ++++------------ 8 files changed, 13 insertions(+), 51 deletions(-) delete mode 100644 python/testData/formatter/fromImportForceParenthesesAlways.py delete mode 100644 python/testData/formatter/fromImportForceParenthesesAlways_after.py diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 3820a1a69a5b..8070a9e5bd51 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -1034,7 +1034,7 @@ 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.from.import.statements="From" Import Statements -formatter.from.import.statements.force.parentheses=Force parentheses +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/PyCodeStyleSettings.java b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java index 1c48174e4a43..a8d81a9de18d 100644 --- a/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java +++ b/python/src/com/jetbrains/python/formatter/PyCodeStyleSettings.java @@ -119,7 +119,7 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings { CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE, CommonCodeStyleSettings.FORCE_BRACES_ALWAYS} ) - public int FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE; + public boolean FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = false; public boolean FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = false; /** diff --git a/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java index ab87b880cb09..825fb2153188 100644 --- a/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java +++ b/python/src/com/jetbrains/python/formatter/PyFromImportPostFormatProcessor.java @@ -66,16 +66,14 @@ public class PyFromImportPostFormatProcessor implements PostFormatProcessor { if (importedNames.size() > 1) { final PyCodeStyleSettings pySettings = ((CodeStyleSettings)myHelper.getSettings()).getCustomSettings(PyCodeStyleSettings.class); - final boolean forcedParentheses = pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_ALWAYS || - pySettings.FROM_IMPORT_PARENTHESES_FORCE == CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE && - PostFormatProcessorHelper.isMultiline(node); + 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 (forcedParentheses && openingParen == null || forcedComma && missingComma && openingParen != null) { + if (forcedParens && openingParen == null || forcedComma && missingComma && openingParen != null) { myImportStatements.add(node); } } diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index bdd2ce2a88da..4d9bf537c737 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -131,10 +131,9 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin 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", - PyBundle.message("formatter.from.import.statements.force.parentheses"), - PyBundle.message("formatter.from.import.statements"), - BRACE_OPTIONS, BRACE_VALUES); + 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")); diff --git a/python/testData/formatter/fromImportForceParenthesesAlways.py b/python/testData/formatter/fromImportForceParenthesesAlways.py deleted file mode 100644 index 4367abfb876b..000000000000 --- a/python/testData/formatter/fromImportForceParenthesesAlways.py +++ /dev/null @@ -1,12 +0,0 @@ -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/fromImportForceParenthesesAlways_after.py b/python/testData/formatter/fromImportForceParenthesesAlways_after.py deleted file mode 100644 index 8f29c68abdd7..000000000000 --- a/python/testData/formatter/fromImportForceParenthesesAlways_after.py +++ /dev/null @@ -1,13 +0,0 @@ -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/fromImportTrailingCommaWithParentheses_after.py b/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py index 1857e3d30298..0940bfab0ebb 100644 --- a/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py +++ b/python/testData/formatter/fromImportTrailingCommaWithParentheses_after.py @@ -1,8 +1,6 @@ from module import foo -from module import (foo, - bar, ) -from module import (foo, - bar, ) +from module import foo, bar +from module import foo, bar, # | margin from module import (foo, bar, baz, ) diff --git a/python/testSrc/com/jetbrains/python/PyFormatterTest.java b/python/testSrc/com/jetbrains/python/PyFormatterTest.java index dd6048fa5525..8f48d4970e9e 100644 --- a/python/testSrc/com/jetbrains/python/PyFormatterTest.java +++ b/python/testSrc/com/jetbrains/python/PyFormatterTest.java @@ -20,7 +20,6 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleManager; -import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.formatter.PyCodeStyleSettings; @@ -765,24 +764,17 @@ public class PyFormatterTest extends PyTestCase { doTest(); } - // PY-20633 - public void testFromImportForceParenthesesAlways() { - getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); - getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; - doTest(); - } - // PY-20633 public void testFromImportForceParenthesesIfMultiline() { getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); - getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_IF_MULTILINE; + 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 = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; + 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(); @@ -794,7 +786,7 @@ public class PyFormatterTest extends PyTestCase { // PY-9764 public void testFromImportTrailingCommaWithParentheses() { getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); - getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.FORCE_BRACES_ALWAYS; + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = true; getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true; doTest(); } @@ -802,7 +794,7 @@ public class PyFormatterTest extends PyTestCase { // PY-9764 public void testFromImportTrailingCommaWithoutParentheses() { getCodeStyleSettings().setRightMargin(PythonLanguage.INSTANCE, 30); - getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE = CommonCodeStyleSettings.DO_NOT_FORCE; + getPythonCodeStyleSettings().FROM_IMPORT_PARENTHESES_FORCE_IF_MULTILINE = false; getPythonCodeStyleSettings().FROM_IMPORT_TRAILING_COMMA_IF_MULTILINE = true; doTest(); } From e2d64f0cd8b22fe25a86ca242dbeda3123445a18 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Mon, 23 Jan 2017 17:11:47 +0300 Subject: [PATCH 25/25] PY-20633 PY-9764 Change the code sample to better show effect of the new options --- .../python/formatter/PyLanguageCodeStyleSettingsProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java index 4d9bf537c737..511cca5aafed 100644 --- a/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java +++ b/python/src/com/jetbrains/python/formatter/PyLanguageCodeStyleSettingsProvider.java @@ -192,7 +192,7 @@ public class PyLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettin " def foo(self):\n" + " pass"; @SuppressWarnings("FieldCanBeLocal") - private static String WRAP_SETTINGS_PREVIEW = "from module import (foo, bar, baz, quux,)\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" +