diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImpl.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImpl.java index 5f25f52a344..ac177196a05 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImpl.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImpl.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.JetNodeType; import static org.jetbrains.kotlin.psi.PsiPackage.JetPsiFactory; +import static org.jetbrains.kotlin.psi.PsiPackage.createExpressionByPattern; public abstract class JetExpressionImpl extends JetElementImpl implements JetExpression { public JetExpressionImpl(@NotNull ASTNode node) { @@ -48,7 +49,7 @@ public abstract class JetExpressionImpl extends JetElementImpl implements JetExp PsiElement parent = getParent(); if (parent instanceof JetExpression && newElement instanceof JetExpression && JetPsiUtil.areParenthesesNecessary((JetExpression) newElement, this, (JetExpression) parent)) { - return super.replace(JetPsiFactory(this).createExpression("(" + newElement.getText() + ")")); + return super.replace(createExpressionByPattern(JetPsiFactory(this), "($0)", newElement)); } return super.replace(newElement); } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImplStub.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImplStub.java index 74e309ac814..53cd6eaba3d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImplStub.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetExpressionImplStub.java @@ -24,6 +24,7 @@ import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import static org.jetbrains.kotlin.psi.PsiPackage.JetPsiFactory; +import static org.jetbrains.kotlin.psi.PsiPackage.createExpressionByPattern; public abstract class JetExpressionImplStub extends JetElementImplStub implements JetExpression { public JetExpressionImplStub(@NotNull T stub, @NotNull IStubElementType nodeType) { @@ -46,7 +47,7 @@ public abstract class JetExpressionImplStub extends JetEl PsiElement parent = getParent(); if (parent instanceof JetExpression && newElement instanceof JetExpression && JetPsiUtil.areParenthesesNecessary((JetExpression) newElement, this, (JetExpression) parent)) { - return super.replace(JetPsiFactory(this).createExpression("(" + newElement.getText() + ")")); + return super.replace(createExpressionByPattern(JetPsiFactory(this), "($0)", newElement)); } return super.replace(newElement); } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/createByPattern.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/createByPattern.kt new file mode 100644 index 00000000000..dc8cc27dd7b --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/createByPattern.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2010-2015 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 org.jetbrains.kotlin.psi + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.SmartPointerManager +import com.intellij.psi.SmartPsiElementPointer +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.impl.source.codeStyle.CodeEditUtil +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import java.util.ArrayList +import java.util.HashMap +import java.util.LinkedHashMap + +public fun JetPsiFactory.createExpressionByPattern(pattern: String, vararg args: Any): JetExpression { + val (processedText, allPlaceholders) = processPattern(pattern, args) + + var expression = createExpression(processedText) + val project = expression.getProject() + + val start = expression.getTextRange().getStartOffset() + + val pointerManager = SmartPointerManager.getInstance(project) + + val pointers = HashMap, Int>() + for ((n, placeholders) in allPlaceholders) { + if (args[n] is String) continue // already in the text + + for ((range, text) in placeholders) { + val token = expression.findElementAt(range.getStartOffset())!! + for (element in token.parents()) { + val elementRange = element.getTextRange().shiftRight(-start) + if (elementRange == range) { + val elementToUse = element.parents(withItself = false).firstIsInstanceOrNull() + ?: error("Invalid pattern - no JetExpression found for $n") + val pointer = pointerManager.createSmartPsiElementPointer(elementToUse) + pointers.put(pointer, n) + break + } + else if (!range.contains(elementRange)) { + throw IllegalArgumentException("Invalid pattern - no PsiElement found for $$n") + } + } + } + } + + val codeStyleManager = CodeStyleManager.getInstance(project) + + val stringPlaceholderRanges = allPlaceholders + .filter { args[it.getKey()] is String } + .flatMap { it.getValue() } + .map { it.range } + .sortBy { -it.getStartOffset() } + + // reformat whole text except for String arguments (as they can contain user's formatting to be preserved) + if (stringPlaceholderRanges.none()) { + expression = codeStyleManager.reformat(expression, true) as JetExpression + } + else { + var bound = expression.getTextRange().getEndOffset() - 1 + for (range in stringPlaceholderRanges) { + // we extend reformatting range by 1 to the right because otherwise some of spaces are not reformatted + expression = codeStyleManager.reformatRange(expression, range.getEndOffset() + start, bound + 1, true) as JetExpression + bound = range.getStartOffset() + start + } + expression = codeStyleManager.reformatRange(expression, start, bound + 1, true) as JetExpression + + // we need to adjust indent of all lines within expression + codeStyleManager.adjustLineIndent(expression.getContainingFile(), expression.getTextRange()) + } + + // do not reformat the whole expression in PostprocessReformattingAspect + CodeEditUtil.setNodeGeneratedRecursively(expression.getNode(), false) + + for ((pointer, n) in pointers) { + val element = pointer.getElement()!! + val arg = args[n] + if (arg !is JetExpression) { + throw IllegalArgumentException("Unknown argument $arg - should be JetExpression or String") + } + element.replace(arg) + } + + return expression +} + +private data class Placeholder(val range: TextRange, val text: String) + +private data class PatternData(val processedText: String, val placeholders: Map>) + +private fun processPattern(pattern: String, args: Array): PatternData { + val ranges = LinkedHashMap>() + + fun charOrNull(i: Int) = if (i < pattern.length()) pattern[i] else null + + fun check(condition: Boolean, message: String) { + if (!condition) { + throw IllegalArgumentException("Invalid pattern '$pattern' - $message") + } + } + + val text = StringBuilder { + var i = 0 + while (i < pattern.length()) { + var c = pattern[i] + + if (c == '$') { + val nextChar = charOrNull(++i) + if (nextChar == '$') { + append(nextChar) + } + else { + check(nextChar?.isDigit() ?: false, "unclosed '$'") + + val lastIndex = (i..pattern.length() - 1).firstOrNull { !pattern[it].isDigit() } ?: pattern.length() + val n = pattern.substring(i, lastIndex).toInt() + check(n >= 0, "invalid placeholder number: $n") + i = lastIndex + + val arg: Any? = if (n < args.size()) args[n] else null /* report wrong number of arguments later */ + val placeholderText = if (charOrNull(i) != '=') { + arg as? String ?: "xyz" + } + else { + check(arg !is String, "do not specify placeholder text for $$n - String argument passed") + i++ + val endIndex = pattern.indexOf('$', i) + check(endIndex >= 0, "unclosed placeholder text") + check(endIndex > i, "empty placeholder text") + val text = pattern.substring(i, endIndex) + i = endIndex + 1 + text + } + + append(placeholderText) + val range = TextRange(length() - placeholderText.length(), length()) + ranges.getOrPut(n, { ArrayList() }).add(Placeholder(range, placeholderText)) + continue + } + } + else { + append(c) + } + i++ + } + }.toString() + + check(!ranges.isEmpty(), "no placeholders found") + + val max = ranges.keySet().max()!! + for (i in 0..max) { + check(ranges.contains(i), "no '$$i' placeholder") + } + + if (args.size() != ranges.size()) { + throw IllegalArgumentException("Wrong number of arguments, expected: ${ranges.size()}, passed: ${args.size()}") + } + + return PatternData(text, ranges) +} diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertForEachToForLoopIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertForEachToForLoopIntention.kt index 6bb42fd09b5..e25e0b0c507 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertForEachToForLoopIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertForEachToForLoopIntention.kt @@ -37,8 +37,8 @@ public class ConvertForEachToForLoopIntention : JetSelfTargetingOffsetIndependen override fun applyTo(element: JetSimpleNameExpression, editor: Editor) { val (expressionToReplace, receiver, functionLiteral) = extractData(element)!! - val loopText = generateLoopText(functionLiteral, receiver) - expressionToReplace.replace(JetPsiFactory(element).createExpression(loopText)) + val loop = generateLoop(functionLiteral, receiver) + expressionToReplace.replace(loop) } private data class Data( @@ -64,10 +64,11 @@ public class ConvertForEachToForLoopIntention : JetSelfTargetingOffsetIndependen return Data(expression, receiver.getExpression(), functionLiteral) } - private fun generateLoopText(functionLiteral: JetFunctionLiteralExpression, receiver: JetExpression): String { - val loopRangeText = JetPsiUtil.safeDeparenthesize(receiver).getText() - val bodyText = functionLiteral.getBodyExpression()!!.getText() - val varText = functionLiteral.getValueParameters().singleOrNull()?.getText() ?: "it" - return "for ($varText in $loopRangeText) { $bodyText }" + private fun generateLoop(functionLiteral: JetFunctionLiteralExpression, receiver: JetExpression): JetExpression { + val factory = JetPsiFactory(functionLiteral) + val loopRange = JetPsiUtil.safeDeparenthesize(receiver) + val body = functionLiteral.getBodyExpression()!! + val parameter = functionLiteral.getValueParameters().singleOrNull() + return factory.createExpressionByPattern("for($0 in $1){ $2 }", parameter ?: "it", loopRange, body) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertPropertyToFunctionIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertPropertyToFunctionIntention.kt index d3bb6149ddf..0dec646e764 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertPropertyToFunctionIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertPropertyToFunctionIntention.kt @@ -17,13 +17,11 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.openapi.editor.Editor -import org.jetbrains.kotlin.psi.JetProperty import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.descriptors.CallableDescriptor import com.intellij.openapi.project.Project import org.jetbrains.kotlin.idea.refactoring.CallableRefactoring -import org.jetbrains.kotlin.psi.JetPsiFactory import com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.psi.psiUtil.siblings import com.intellij.util.containers.MultiMap @@ -39,24 +37,22 @@ import org.jetbrains.kotlin.idea.search.usagesSearch.search import org.jetbrains.kotlin.idea.references.JetSimpleNameReference import com.intellij.openapi.util.text.StringUtil import org.jetbrains.kotlin.resolve.calls.callUtil.getCall -import org.jetbrains.kotlin.psi.JetSimpleNameExpression import org.jetbrains.kotlin.idea.references.JetReference import com.intellij.psi.PsiMethod import com.intellij.psi.PsiField import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiElementFactory import org.jetbrains.kotlin.idea.util.application.executeWriteCommand -import org.jetbrains.kotlin.psi.JetCallableReferenceExpression import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType import com.intellij.psi.PsiJavaReference import org.jetbrains.kotlin.psi.psiUtil.isAncestor -import org.jetbrains.kotlin.psi.JetExpressionImpl import org.jetbrains.kotlin.codegen.PropertyCodegen import org.jetbrains.kotlin.asJava.namedUnwrappedElement import org.jetbrains.kotlin.idea.refactoring.getContainingScope import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.core.refactoring.checkConflictsInteractively import org.jetbrains.kotlin.idea.core.refactoring.reportDeclarationConflict +import org.jetbrains.kotlin.psi.* public class ConvertPropertyToFunctionIntention : JetSelfTargetingIntention(javaClass(), "Convert property to function") { private inner class Converter( @@ -172,7 +168,7 @@ public class ConvertPropertyToFunctionIntention : JetSelfTargetingIntention it.setName(propertyName) } } - kotlinRefs.forEach { it.replace(kotlinPsiFactory.createExpression(it.getText() + "()")) } + kotlinRefs.forEach { it.replace(kotlinPsiFactory.createExpressionByPattern("$0()", it)) } foreignRefsToRename.forEach { it.handleElementRename(propertyName) } javaRefsToReplaceWithCall.forEach { it.replace(javaPsiFactory.createExpressionFromText(it.getText() + "()", null)) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertToForEachFunctionCallIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertToForEachFunctionCallIntention.kt index 6549009f932..fd468beb005 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertToForEachFunctionCallIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertToForEachFunctionCallIntention.kt @@ -17,7 +17,10 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.openapi.editor.Editor -import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.JetBlockExpression +import org.jetbrains.kotlin.psi.JetForExpression +import org.jetbrains.kotlin.psi.JetPsiFactory +import org.jetbrains.kotlin.psi.createExpressionByPattern public class ConvertToForEachFunctionCallIntention : JetSelfTargetingIntention(javaClass(), "Replace with a forEach function call") { override fun isApplicableTo(element: JetForExpression, caretOffset: Int): Boolean { @@ -29,16 +32,14 @@ public class ConvertToForEachFunctionCallIntention : JetSelfTargetingIntention body.getStatements().map { it.getText() }.joinToString("\n") else -> body.getText() } - val bodyText = "${loopParameter.getText()} -> $functionBodyText" - val foreachExpression = factory.createExpression("x.forEach { $bodyText }") as JetDotQualifiedExpression - foreachExpression.getReceiverExpression().replace(element.getLoopRange()!!) + val foreachExpression = JetPsiFactory(element).createExpressionByPattern( + "$0.forEach{$1->$2}", element.getLoopRange()!!, loopParameter, functionBodyText) element.replace(foreachExpression) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt index 02daae7d62b..afea5de6149 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/IfNullToElvisIntention.kt @@ -62,9 +62,7 @@ public class IfNullToElvisIntention : JetSelfTargetingOffsetIndependentIntention declaration.add(comment) } - val elvis = factory.createExpression("a ?: b") as JetBinaryExpression - elvis.getLeft()!!.replace(initializer) - elvis.getRight()!!.replace(ifNullExpr) + val elvis = factory.createExpressionByPattern("$0 ?: $1", initializer, ifNullExpr) as JetBinaryExpression val newElvis = initializer.replaced(elvis) element.delete() diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/InfixCallToOrdinaryIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/InfixCallToOrdinaryIntention.kt index f76f3066541..eeb4dea3bf8 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/InfixCallToOrdinaryIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/InfixCallToOrdinaryIntention.kt @@ -17,11 +17,8 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.openapi.editor.Editor -import org.jetbrains.kotlin.psi.JetBinaryExpression -import org.jetbrains.kotlin.psi.JetPsiFactory -import org.jetbrains.kotlin.psi.JetFunctionLiteralExpression -import org.jetbrains.kotlin.psi.JetParenthesizedExpression import org.jetbrains.kotlin.lexer.JetTokens +import org.jetbrains.kotlin.psi.* public class InfixCallToOrdinaryIntention : JetSelfTargetingIntention(javaClass(), "Replace infix call with ordinary call") { override fun isApplicableTo(element: JetBinaryExpression, caretOffset: Int): Boolean { @@ -30,21 +27,12 @@ public class InfixCallToOrdinaryIntention : JetSelfTargetingIntention " $argumentText" - is JetParenthesizedExpression -> argumentText - else -> "($argumentText)" - }) - }.toString() - - element.replace(JetPsiFactory(element).createExpression(text)) + val argument = JetPsiUtil.safeDeparenthesize(element.getRight()!!) + val pattern = "$0.$1" + when (argument) { + is JetFunctionLiteralExpression -> " $2={}$" + else -> "($2)" + } + val replacement = JetPsiFactory(element).createExpressionByPattern(pattern, element.getLeft()!!, element.getOperationReference().getText(), argument) + element.replace(replacement) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/InvertIfConditionIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/InvertIfConditionIntention.kt index 53828a3d44b..2b7ceb44b22 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/InvertIfConditionIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/InvertIfConditionIntention.kt @@ -75,10 +75,7 @@ public class InvertIfConditionIntention : JetSelfTargetingIntention { diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ToInfixCallIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ToInfixCallIntention.kt index dd70ce5f567..191312b6a21 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ToInfixCallIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ToInfixCallIntention.kt @@ -18,14 +18,11 @@ package org.jetbrains.kotlin.idea.intentions import com.intellij.openapi.editor.Editor import org.jetbrains.kotlin.idea.caches.resolve.analyze -import org.jetbrains.kotlin.psi.JetBinaryExpression -import org.jetbrains.kotlin.psi.JetCallExpression -import org.jetbrains.kotlin.psi.JetDotQualifiedExpression -import org.jetbrains.kotlin.psi.JetPsiFactory +import org.jetbrains.kotlin.psi.* public class ToInfixCallIntention : JetSelfTargetingIntention(javaClass(), "Replace with infix function call") { override fun isApplicableTo(element: JetCallExpression, caretOffset: Int): Boolean { - val calleeExpr = element.getCalleeExpression() ?: return false + val calleeExpr = element.getCalleeExpression() as? JetSimpleNameExpression ?: return false if (!calleeExpr.getTextRange().containsOffset(caretOffset)) return false val dotQualified = element.getParent() as? JetDotQualifiedExpression ?: return false @@ -46,13 +43,10 @@ public class ToInfixCallIntention : JetSelfTargetingIntention override fun applyTo(element: JetCallExpression, editor: Editor) { val dotQualified = element.getParent() as JetDotQualifiedExpression val receiver = dotQualified.getReceiverExpression() + val argument = element.getValueArguments().single().getArgumentExpression()!! + val name = element.getCalleeExpression()!!.getText() - val operatorText = element.getCalleeExpression()!!.getText() - - val newCall = JetPsiFactory(element).createExpression("${receiver.getText()} $operatorText x") as JetBinaryExpression - val argument = element.getValueArguments().single() - newCall.getRight()!!.replace(argument.getArgumentExpression()!!) - + val newCall = JetPsiFactory(element).createExpressionByPattern("$0 $1 $2", receiver, name, argument) dotQualified.replace(newCall) } } diff --git a/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt b/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt index 2302a930a8e..283ec93a309 100644 --- a/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt +++ b/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt @@ -1,8 +1,7 @@ fun foo() { - val list = 1..4 - - for (x in list) { - x + // check that original formatting of "x+1" and "1 .. 4" is preserved + for (x in 1 .. 4) { x + x+1 } } \ No newline at end of file diff --git a/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt.after b/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt.after index d9117f3c55b..d45aba4571c 100644 --- a/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt.after +++ b/idea/testData/intentions/convertToForEachFunctionCall/blockBodyExpression.kt.after @@ -1,8 +1,7 @@ fun foo() { - val list = 1..4 - - list.forEach { x -> - x + // check that original formatting of "x+1" and "1 .. 4" is preserved + (1 .. 4).forEach { x -> x + x+1 } } \ No newline at end of file