diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java index 342ad7b094f..b13fe38bd00 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java @@ -48,7 +48,6 @@ import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils; import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde; import org.jetbrains.kotlin.idea.refactoring.introduce.IntroduceUtilKt; import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers; -import org.jetbrains.kotlin.idea.util.string.StringUtilKt; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt; import org.jetbrains.kotlin.renderer.DescriptorRenderer; @@ -378,7 +377,10 @@ public class KotlinRefactoringUtil { if (element instanceof KtExpression && !(element instanceof KtStatementExpression)) { boolean addExpression = true; - if (KtPsiUtil.isLabelIdentifierExpression(element)) { + if (element instanceof KtParenthesizedExpression) { + addExpression = false; + } + else if (KtPsiUtil.isLabelIdentifierExpression(element)) { addExpression = false; } else if (element.getParent() instanceof KtQualifiedExpression) { @@ -481,8 +483,12 @@ public class KotlinRefactoringUtil { } - public static String getExpressionShortText(@NotNull KtElement element) { //todo: write appropriate implementation - return StringUtilKt.collapseSpaces(StringUtil.shortenTextWithEllipsis(element.getText(), 53, 0)); + public static String getExpressionShortText(@NotNull KtElement element) { + String text = ElementRenderingUtilsKt.renderTrimmed(element); + int firstNewLinePos = text.indexOf('\n'); + String trimmedText = text.substring(0, firstNewLinePos != -1 ? firstNewLinePos : Math.min(100, text.length())); + if (trimmedText.length() != text.length()) trimmedText += " ..."; + return trimmedText; } @Nullable diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/elementRenderingUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/elementRenderingUtils.kt new file mode 100644 index 00000000000..2138035ca96 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/elementRenderingUtils.kt @@ -0,0 +1,325 @@ +/* + * 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.idea.refactoring + +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.utils.ifEmpty + +fun KtElement.renderTrimmed(): String { + class Renderer : KtTreeVisitorVoid() { + val builder = StringBuilder() + + fun render(element: KtElement): String { + element.accept(this) + return builder.toString() + } + + private fun Iterable.join(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "") { + builder.append(prefix) + var count = 0 + for (element in this) { + if (++count > 1) builder.append(separator) + element.accept(this@Renderer) + } + builder.append(postfix) + } + + // Whitespace and comments + + override fun visitWhiteSpace(space: PsiWhiteSpace) { + val text = space.text + val newLine = text.indexOf('\n') + if (newLine != 0) { + builder.append(' ') + } + if (newLine >= 0) { + builder.append(text.substring(newLine)) + } + } + + override fun visitComment(comment: PsiComment) { + + } + + // Basic expressions + + override fun visitParenthesizedExpression(expression: KtParenthesizedExpression) { + builder.append('(') + expression.expression?.accept(this) + builder.append(')') + } + + override fun visitLabeledExpression(expression: KtLabeledExpression) { + expression.baseExpression?.accept(this) + } + + override fun visitAnnotatedExpression(expression: KtAnnotatedExpression) { + expression.baseExpression?.accept(this) + } + + override fun visitPrefixExpression(expression: KtPrefixExpression) { + builder.append("${expression.operationReference.getReferencedName()}") + expression.baseExpression?.accept(this) + } + + override fun visitPostfixExpression(expression: KtPostfixExpression) { + expression.baseExpression?.accept(this) + builder.append("${expression.operationReference.getReferencedName()}") + } + + override fun visitBinaryExpression(expression: KtBinaryExpression) { + expression.left?.accept(this) + builder.append(" ${expression.operationReference.getReferencedName()} ") + expression.right?.accept(this) + } + + override fun visitBinaryWithTypeRHSExpression(expression: KtBinaryExpressionWithTypeRHS) { + expression.left.accept(this) + builder.append(" ${expression.operationReference.getReferencedName()} ") + expression.right?.accept(this) + } + + override fun visitIsExpression(expression: KtIsExpression) { + expression.leftHandSide.accept(this) + builder.append(" is ") + expression.typeReference?.accept(this) + } + + override fun visitArrayAccessExpression(expression: KtArrayAccessExpression) { + expression.arrayExpression?.accept(this) + expression.indexExpressions.join(builder, prefix = "[", postfix = "]") + } + + override fun visitCallExpression(expression: KtCallExpression) { + expression.calleeExpression?.accept(this) + expression.valueArgumentList?.accept(this) + expression.functionLiteralArguments.forEach { builder.append("{...}") } + } + + override fun visitValueArgumentList(list: KtValueArgumentList) { + builder.append(if (list.arguments.isEmpty()) "()" else "(...)") + } + + override fun visitQualifiedExpression(expression: KtQualifiedExpression) { + expression.receiverExpression.accept(this) + builder.append(expression.operationTokenNode.text) + expression.selectorExpression?.accept(this) + } + + override fun visitClassLiteralExpression(expression: KtClassLiteralExpression) { + expression.typeReference?.accept(this) + builder.append("::class") + } + + override fun visitCallableReferenceExpression(expression: KtCallableReferenceExpression) { + expression.typeReference?.accept(this) + builder.append("::") + builder.append(expression.callableReference.getReferencedName()) + } + + override fun visitThisExpression(expression: KtThisExpression) { + builder.append("this") + expression.getLabelName()?.let { builder.append("@$it") } + } + + override fun visitSuperExpression(expression: KtSuperExpression) { + builder.append("super") + expression.superTypeQualifier?.let { + builder.append("<") + it.accept(this) + builder.append(">") + } + expression.getLabelName()?.let { builder.append("@$it") } + } + + // Control structures + + override fun visitBreakExpression(expression: KtBreakExpression) { + builder.append("break") + expression.getLabelName()?.let { builder.append("@$it") } + } + + override fun visitContinueExpression(expression: KtContinueExpression) { + builder.append("continue") + expression.getLabelName()?.let { builder.append("@$it") } + } + + override fun visitThrowExpression(expression: KtThrowExpression) { + builder.append("throw ") + expression.thrownExpression?.accept(this) + } + + override fun visitReturnExpression(expression: KtReturnExpression) { + builder.append("return") + expression.getLabelName()?.let { builder.append("@$it") } + builder.append(' ') + expression.returnedExpression?.accept(this) + } + + override fun visitBlockExpression(expression: KtBlockExpression) { + if (expression.parent is KtFunctionLiteral) { + super.visitBlockExpression(expression) + } + else { + builder.append("{...}") + } + } + + override fun visitIfExpression(expression: KtIfExpression) { + builder.append("if (") + expression.condition?.accept(this) + builder.append(")") + expression.then?.let { + builder.append(' ') + it.accept(this) + } + expression.`else`?.let { + builder.append(" else ") + it.accept(this) + } + } + + override fun visitWhenExpression(expression: KtWhenExpression) { + builder.append("when") + expression.subjectExpression?.let { + builder.append('(') + it.accept(this) + builder.append(')') + } + builder.append(" {...}") + } + + override fun visitForExpression(expression: KtForExpression) { + builder.append("for (") + (expression.loopParameter ?: expression.multiParameter)?.accept(this) + builder.append(" in ") + expression.loopRange?.accept(this) + builder.append(")") + expression.body?.let { + builder.append(' ') + it.accept(this) + } + } + + override fun visitWhileExpression(expression: KtWhileExpression) { + builder.append("while (") + expression.condition?.accept(this) + builder.append(")") + expression.body?.let { + builder.append(' ') + it.accept(this) + } + } + + override fun visitDoWhileExpression(expression: KtDoWhileExpression) { + builder.append("do") + expression.body?.let { + builder.append(' ') + it.accept(this) + } + builder.append(" while (") + expression.condition?.accept(this) + builder.append(")") + } + + override fun visitTryExpression(expression: KtTryExpression) { + builder.append("try {...}") + } + + // Declarations + + override fun visitNamedFunction(function: KtNamedFunction) { + builder.append("fun") + function.receiverTypeReference?.let { + builder.append('.') + it.accept(this) + } + function.name?.let { builder.append(" $it") } + function.valueParameters.mapNotNull { it.typeReference }.joinTo(builder, prefix = "(", postfix = ")") + function.equalsToken?.let { builder.append(" = ") } + function.bodyExpression?.accept(this) + } + + override fun visitPropertyAccessor(accessor: KtPropertyAccessor) { + builder.append(if(accessor.isGetter) "get" else "set") + builder.append("()") + accessor.equalsToken?.let { builder.append(" = ") } + accessor.bodyExpression?.accept(this) + } + + override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) { + constructor.valueParameters.mapNotNull { it.typeReference }.joinTo(builder, prefix = "(", postfix = ")") + } + + override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor) { + builder.append("constructor") + constructor.valueParameters.mapNotNull { it.typeReference }.joinTo(builder, prefix = "(", postfix = ")") + constructor.bodyExpression?.accept(this) + } + + override fun visitClassOrObject(classOrObject: KtClassOrObject) { + val keyword = when (classOrObject) { + is KtClass -> classOrObject.getClassOrInterfaceKeyword() + is KtObjectDeclaration -> classOrObject.getObjectKeyword() + else -> return + } + keyword?.accept(this) + + classOrObject.name?.let { builder.append(" $it") } + classOrObject.getDelegationSpecifierList()?.accept(this) + classOrObject.getBody()?.let { builder.append(" {...}") } + } + + override fun visitDelegationSpecifierList(list: KtDelegationSpecifierList) { + list.delegationSpecifiers.ifEmpty { return }.join(builder, prefix = " : ") + } + + override fun visitDelegationByExpressionSpecifier(specifier: KtDelegatorByExpressionSpecifier) { + specifier.typeReference?.accept(this) + specifier.delegateExpression?.let { + builder.append(" by ") + it.accept(this) + } + } + + override fun visitDelegationToSuperCallSpecifier(specifier: KtDelegatorToSuperCall) { + specifier.typeReference?.accept(this) + specifier.valueArgumentList?.accept(this) + } + + override fun visitDelegationToSuperClassSpecifier(specifier: KtDelegatorToSuperClass) { + specifier.typeReference?.accept(this) + } + + // Default + + override fun visitElement(element: PsiElement) { + if (element is LeafPsiElement) { + builder.append(element.text) + } + else { + super.visitElement(element) + } + } + } + + return Renderer().render(this) +} \ No newline at end of file diff --git a/idea/testData/smartSelection/commentsAndExtraSpaces.kt b/idea/testData/smartSelection/commentsAndExtraSpaces.kt new file mode 100644 index 00000000000..51e4a77193a --- /dev/null +++ b/idea/testData/smartSelection/commentsAndExtraSpaces.kt @@ -0,0 +1,10 @@ +fun foo() { + (1 + // abc + 2 /*def*/) * 3 +} + +/* +1 +1 + 2 +(1 + 2) * 3 +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/lambdaCalls.kt b/idea/testData/smartSelection/lambdaCalls.kt new file mode 100644 index 00000000000..4793432ab38 --- /dev/null +++ b/idea/testData/smartSelection/lambdaCalls.kt @@ -0,0 +1,17 @@ +interface T { + fun foo(a: () -> Any): T = this +} + +fun foo(t: T) { + t.foo { 1 } + .foo {2} + .foo({ 3 }) +} + +/* +1 +{ 1 } +t.foo{...} +t.foo{...}.foo{...} +t.foo{...}.foo{...}.foo(...) +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/multilineCalls.kt b/idea/testData/smartSelection/multilineCalls.kt new file mode 100644 index 00000000000..6a4220f0f9f --- /dev/null +++ b/idea/testData/smartSelection/multilineCalls.kt @@ -0,0 +1,18 @@ +interface T { + fun foo(vararg a: Any): T = this +} + +fun foo(t: T) { + t.foo( + 1 + 2, + 2, 3 + ).foo() + .foo(4) +} + +/* +2 +t.foo(...) +t.foo(...).foo() +t.foo(...).foo().foo(...) +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/multilineOperations.kt b/idea/testData/smartSelection/multilineOperations.kt new file mode 100644 index 00000000000..39f1ddf7f1a --- /dev/null +++ b/idea/testData/smartSelection/multilineOperations.kt @@ -0,0 +1,13 @@ +fun foo() { + (1 + + 2 - + 3)*(- + 4) +} + +/* +1 +1 + 2 +1 + 2 - 3 +(1 + 2 - 3) * (-4) +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/objectLiteral.kt b/idea/testData/smartSelection/objectLiteral.kt new file mode 100644 index 00000000000..fdda337c9e6 --- /dev/null +++ b/idea/testData/smartSelection/objectLiteral.kt @@ -0,0 +1,16 @@ +interface T { + fun foo(t: T) = t +} + +fun foo() { + object : T { + val x = 1 + }.foo(object : T { + val x = 2 + }) +} + +/* +object : T {...} +object : T {...}.foo(...) +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/parenthesized.kt b/idea/testData/smartSelection/parenthesized.kt new file mode 100644 index 00000000000..8f6e626fa42 --- /dev/null +++ b/idea/testData/smartSelection/parenthesized.kt @@ -0,0 +1,10 @@ +fun foo() { + val t = ((1 + 2) + 3) * 4 +} + +/* +1 +1 + 2 +(1 + 2) + 3 +((1 + 2) + 3) * 4 +*/ \ No newline at end of file diff --git a/idea/testData/smartSelection/simple.kt b/idea/testData/smartSelection/simple.kt index b8d82baaafe..c1942c64cc5 100644 --- a/idea/testData/smartSelection/simple.kt +++ b/idea/testData/smartSelection/simple.kt @@ -4,6 +4,6 @@ fun main(args: Array) { /* 1.0 1.0 + 1 -Math.pow(1.0 + 1, Math.abs(-42.0)) -print(Math.pow(1.0 + 1, Math.abs(-42.0))) +Math.pow(...) +print(...) */ diff --git a/idea/tests/org/jetbrains/kotlin/idea/AbstractSmartSelectionTest.java b/idea/tests/org/jetbrains/kotlin/idea/AbstractSmartSelectionTest.java index c7d2a7b8a15..f9218bf37ea 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/AbstractSmartSelectionTest.java +++ b/idea/tests/org/jetbrains/kotlin/idea/AbstractSmartSelectionTest.java @@ -38,7 +38,7 @@ public abstract class AbstractSmartSelectionTest extends LightCodeInsightTestCas List textualExpressions = new ArrayList(); for (KtExpression expression : expressions) { - textualExpressions.add(expression.getText()); + textualExpressions.add(KotlinRefactoringUtil.getExpressionShortText(expression)); } assertEquals(expectedResultText, StringUtil.join(textualExpressions, "\n")); } diff --git a/idea/tests/org/jetbrains/kotlin/idea/SmartSelectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/SmartSelectionTestGenerated.java index 6b993fed8a6..9456e438663 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/SmartSelectionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/SmartSelectionTestGenerated.java @@ -35,6 +35,12 @@ public class SmartSelectionTestGenerated extends AbstractSmartSelectionTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/smartSelection"), Pattern.compile("^([^\\.]+)\\.kt$"), true); } + @TestMetadata("commentsAndExtraSpaces.kt") + public void testCommentsAndExtraSpaces() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/commentsAndExtraSpaces.kt"); + doTestSmartSelection(fileName); + } + @TestMetadata("labelledStatement.kt") public void testLabelledStatement() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/labelledStatement.kt"); @@ -47,6 +53,36 @@ public class SmartSelectionTestGenerated extends AbstractSmartSelectionTest { doTestSmartSelection(fileName); } + @TestMetadata("lambdaCalls.kt") + public void testLambdaCalls() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/lambdaCalls.kt"); + doTestSmartSelection(fileName); + } + + @TestMetadata("multilineCalls.kt") + public void testMultilineCalls() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/multilineCalls.kt"); + doTestSmartSelection(fileName); + } + + @TestMetadata("multilineOperations.kt") + public void testMultilineOperations() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/multilineOperations.kt"); + doTestSmartSelection(fileName); + } + + @TestMetadata("objectLiteral.kt") + public void testObjectLiteral() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/objectLiteral.kt"); + doTestSmartSelection(fileName); + } + + @TestMetadata("parenthesized.kt") + public void testParenthesized() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/parenthesized.kt"); + doTestSmartSelection(fileName); + } + @TestMetadata("simple.kt") public void testSimple() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/smartSelection/simple.kt");