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