Split if intention preserves comments

This commit is contained in:
Valentin Kipyatkov
2015-05-23 22:55:43 +03:00
parent 4a2f2b62ec
commit df0d27b8b2
13 changed files with 178 additions and 19 deletions
@@ -27,6 +27,16 @@ public val PsiElement.startOffset: Int
public val PsiElement.endOffset: Int public val PsiElement.endOffset: Int
get() = getTextRange().getEndOffset() get() = getTextRange().getEndOffset()
public fun PsiElement.getStartOffsetIn(ancestor: PsiElement): Int {
var offset = 0
var parent = this
while (parent != this) {
offset += parent.getStartOffsetInParent()
parent = parent.getParent()
}
return offset
}
public data class PsiChildRange(public val first: PsiElement?, public val last: PsiElement?) : Sequence<PsiElement> { public data class PsiChildRange(public val first: PsiElement?, public val last: PsiElement?) : Sequence<PsiElement> {
init { init {
if (first == null) { if (first == null) {
@@ -17,13 +17,18 @@
package org.jetbrains.kotlin.idea.core package org.jetbrains.kotlin.idea.core
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementVisitor import com.intellij.psi.PsiRecursiveElementVisitor
import com.intellij.psi.PsiWhiteSpace import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.JetFile
import org.jetbrains.kotlin.psi.JetPsiFactory import org.jetbrains.kotlin.psi.JetPsiFactory
import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.psi.psiUtil.*
import java.util.* import java.util.ArrayList
import java.util.HashMap
import java.util.LinkedHashMap
public class CommentSaver(originalElements: PsiChildRange) { public class CommentSaver(originalElements: PsiChildRange) {
public constructor(originalElement: PsiElement) : this(PsiChildRange.singleElement(originalElement)) public constructor(originalElement: PsiElement) : this(PsiChildRange.singleElement(originalElement))
@@ -60,6 +65,8 @@ public class CommentSaver(originalElements: PsiChildRange) {
val nextElements: Sequence<TreeElement> val nextElements: Sequence<TreeElement>
get() = nextSiblings.flatMap { it.withDescendants(leftToRight = true) } get() = nextSiblings.flatMap { it.withDescendants(leftToRight = true) }
// var debugText: String? = null
} }
private data class CommentData( private data class CommentData(
@@ -98,7 +105,8 @@ public class CommentSaver(originalElements: PsiChildRange) {
assert(child.savedTreeElement == null) assert(child.savedTreeElement == null)
val savedChild = TreeElement(parent = parentTreeElement, prev = last) val savedChild = TreeElement(parent = parentTreeElement, prev = last)
child.putCopyableUserData(SAVED_TREE_KEY, savedChild) // savedChild.debugText = child.getText()
child.savedTreeElement = savedChild
last?.next = savedChild last?.next = savedChild
last = savedChild last = savedChild
@@ -116,8 +124,9 @@ public class CommentSaver(originalElements: PsiChildRange) {
private val PsiElement.shouldSave: Boolean private val PsiElement.shouldSave: Boolean
get() = this !is PsiWhiteSpace get() = this !is PsiWhiteSpace
private val PsiElement.savedTreeElement: TreeElement? private var PsiElement.savedTreeElement: TreeElement?
get() = getCopyableUserData(SAVED_TREE_KEY) get() = getCopyableUserData(SAVED_TREE_KEY)
set(value) = putCopyableUserData(SAVED_TREE_KEY, value)
public fun deleteCommentsInside(element: PsiElement) { public fun deleteCommentsInside(element: PsiElement) {
element.accept(object : PsiRecursiveElementVisitor() { element.accept(object : PsiRecursiveElementVisitor() {
@@ -130,6 +139,32 @@ public class CommentSaver(originalElements: PsiChildRange) {
}) })
} }
public fun elementCreatedByText(createdElement: PsiElement, original: PsiElement, rangeInOriginal: TextRange) {
assert(createdElement.getTextLength() == rangeInOriginal.getLength())
assert(createdElement.getText() == original.getText().substring(rangeInOriginal.getStartOffset(), rangeInOriginal.getEndOffset()))
createdElement.accept(object : PsiRecursiveElementVisitor() {
override fun visitElement(element: PsiElement) {
if (!element.shouldSave) return
val token = original.findElementAt(element.getStartOffsetIn(createdElement) + rangeInOriginal.getStartOffset())
if (token != null) {
val elementLength = element.getTextLength()
for (originalElement in token.parents()) {
val length = originalElement.getTextLength()
if (length < elementLength) continue
if (length == elementLength) {
element.savedTreeElement = originalElement.savedTreeElement
}
break
}
}
super.visitElement(element)
}
})
}
public fun restoreComments(resultElement: PsiElement) { public fun restoreComments(resultElement: PsiElement) {
restoreComments(PsiChildRange.singleElement(resultElement)) restoreComments(PsiChildRange.singleElement(resultElement))
} }
@@ -141,7 +176,7 @@ public class CommentSaver(originalElements: PsiChildRange) {
resultElements.forEach { resultElements.forEach {
it.accept(object : PsiRecursiveElementVisitor() { it.accept(object : PsiRecursiveElementVisitor() {
override fun visitElement(element: PsiElement) { override fun visitElement(element: PsiElement) {
element.putCopyableUserData(SAVED_TREE_KEY, null) element.savedTreeElement = null
super.visitElement(element) super.visitElement(element)
} }
}) })
@@ -55,7 +55,7 @@ public class InvertIfConditionIntention : JetSelfTargetingIntention<JetIfExpress
val elseBranch = ifExpression.getElse() ?: psiFactory.createEmptyBody() val elseBranch = ifExpression.getElse() ?: psiFactory.createEmptyBody()
val newThen = if (elseBranch is JetIfExpression) val newThen = if (elseBranch is JetIfExpression)
psiFactory.wrapInABlock(elseBranch) psiFactory.createSingleStatementBlock(elseBranch)
else else
elseBranch elseBranch
@@ -17,10 +17,16 @@
package org.jetbrains.kotlin.idea.intentions package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.lexer.JetTokens import com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.idea.conversion.copy.length
import org.jetbrains.kotlin.idea.conversion.copy.range
import org.jetbrains.kotlin.idea.conversion.copy.start
import org.jetbrains.kotlin.idea.core.CommentSaver
import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis
import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.psiUtil.startOffset
@@ -40,8 +46,11 @@ public class SplitIfIntention : JetSelfTargetingIntention<JetExpression>(javaCla
} }
val ifExpression = operator.getNonStrictParentOfType<JetIfExpression>() val ifExpression = operator.getNonStrictParentOfType<JetIfExpression>()
val commentSaver = CommentSaver(ifExpression!!)
val expression = operator.getParent() as JetBinaryExpression val expression = operator.getParent() as JetBinaryExpression
val rightExpression = JetPsiUtil.safeDeparenthesize(getRight(expression, ifExpression!!.getCondition()!!)) val rightExpression = JetPsiUtil.safeDeparenthesize(getRight(expression, ifExpression.getCondition()!!, commentSaver))
val leftExpression = JetPsiUtil.safeDeparenthesize(expression.getLeft()!!) val leftExpression = JetPsiUtil.safeDeparenthesize(expression.getLeft()!!)
val thenBranch = ifExpression.getThen()!! val thenBranch = ifExpression.getThen()!!
val elseBranch = ifExpression.getElse() val elseBranch = ifExpression.getElse()
@@ -50,32 +59,41 @@ public class SplitIfIntention : JetSelfTargetingIntention<JetExpression>(javaCla
val innerIf = psiFactory.createIf(rightExpression, thenBranch, elseBranch) val innerIf = psiFactory.createIf(rightExpression, thenBranch, elseBranch)
when (operator.getReferencedNameElementType()) { val newIf = when (operator.getReferencedNameElementType()) {
JetTokens.ANDAND -> ifExpression.replace(psiFactory.createIf(leftExpression, psiFactory.wrapInABlock(innerIf), elseBranch)) JetTokens.ANDAND -> psiFactory.createIf(leftExpression, psiFactory.createSingleStatementBlock(innerIf), elseBranch)
JetTokens.OROR -> { JetTokens.OROR -> {
val container = ifExpression.getParent() val container = ifExpression.getParent()
if (container is JetBlockExpression && elseBranch == null && thenBranch.lastBlockStatementOrThis().isExitStatement()) { // special case if (container is JetBlockExpression && elseBranch == null && thenBranch.lastBlockStatementOrThis().isExitStatement()) { // special case
container.addAfter(innerIf, ifExpression) val secondIf = container.addAfter(innerIf, ifExpression)
container.addAfter(psiFactory.createNewLine(), ifExpression) container.addAfter(psiFactory.createNewLine(), ifExpression)
ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch)) val firstIf = ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch))
commentSaver.restoreComments(PsiChildRange(firstIf, secondIf))
return return
} }
else {
ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch, innerIf)) psiFactory.createIf(leftExpression, thenBranch, innerIf)
}
} }
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
val result = ifExpression.replace(newIf)
commentSaver.restoreComments(result)
} }
private fun getRight(element: JetBinaryExpression, condition: JetExpression): JetExpression { private fun getRight(element: JetBinaryExpression, condition: JetExpression, commentSaver: CommentSaver): JetExpression {
//gets the textOffset of the right side of the JetBinaryExpression in context to condition //gets the textOffset of the right side of the JetBinaryExpression in context to condition
val startOffset = element.getRight()!!.startOffset - condition.startOffset val conditionRange = condition.range
val rightString = condition.getText()!!.substring(startOffset, condition.getTextLength()) val startOffset = element.getRight()!!.startOffset - conditionRange.start
val endOffset = conditionRange.length
val rightString = condition.getText().substring(startOffset, endOffset)
return JetPsiFactory(element).createExpression(rightString) val expression = JetPsiFactory(element).createExpression(rightString)
commentSaver.elementCreatedByText(expression, condition, TextRange(startOffset, endOffset))
return expression
} }
private fun getFirstValidOperator(element: JetIfExpression): JetSimpleNameExpression? { private fun getFirstValidOperator(element: JetIfExpression): JetSimpleNameExpression? {
@@ -0,0 +1,4 @@
fun foo(p: Int): Boolean {
if (p < 0 /* p < 0 */ <caret>|| p == 5 /* or 5 */) return false
return true
}
@@ -0,0 +1,5 @@
fun foo(p: Int): Boolean {
if (p < 0 /* p < 0 */) return false
if (p == 5 /* or 5 */) return false
return true
}
@@ -0,0 +1,10 @@
fun doSomething<T>(a: T) {}
fun foo() {
val a = true
val b = false
val c = true
if (a /*a*/ && b /*b*/ &<caret>& c /*c*/) {
doSomething("test")
}
}
@@ -0,0 +1,12 @@
fun doSomething<T>(a: T) {}
fun foo() {
val a = true
val b = false
val c = true
if (a /*a*/ && b /*b*/) {
if (c /*c*/) {
doSomething("test")
}
}
}
@@ -0,0 +1,7 @@
fun doSomething<T>(a: T) {}
fun foo(p: Int) {
<caret>if (0 < p /* > 0 */ && p < 100 /* not too much */) {
doSomething("test")
}
}
@@ -0,0 +1,9 @@
fun doSomething<T>(a: T) {}
fun foo(p: Int) {
<caret>if (0 < p /* > 0 */) {
if (p < 100 /* not too much */) {
doSomething("test")
}
}
}
@@ -0,0 +1,7 @@
fun doSomething<T>(a: T) {}
fun foo(p: Int) {
<caret>if (p < 0 /* p < 0 */ || p > 100 /* too much */) {
doSomething("test")
}
}
@@ -0,0 +1,9 @@
fun doSomething<T>(a: T) {}
fun foo(p: Int) {
<caret>if (p < 0 /* p < 0 */) {
doSomething("test")
} else if (p > 100 /* too much */) {
doSomething("test")
}
}
@@ -3906,7 +3906,7 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
} }
public void testAllFilesPresentInKeepComments() throws Exception { public void testAllFilesPresentInKeepComments() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertToExpressionBody/keepComments"), Pattern.compile("^(.+)\\.kt$"), true); JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertToExpressionBody/keepComments"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
} }
} }
} }
@@ -6629,6 +6629,39 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/wrongCaretLocation.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/wrongCaretLocation.kt");
doTest(fileName); doTest(fileName);
} }
@TestMetadata("idea/testData/intentions/splitIf/keepComments")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class KeepComments extends AbstractIntentionTest {
public void testAllFilesPresentInKeepComments() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/splitIf/keepComments"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("ifOrReturn.kt")
public void testIfOrReturn() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/keepComments/ifOrReturn.kt");
doTest(fileName);
}
@TestMetadata("twoOperators.kt")
public void testTwoOperators() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/keepComments/twoOperators.kt");
doTest(fileName);
}
@TestMetadata("withAnd.kt")
public void testWithAnd() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/keepComments/withAnd.kt");
doTest(fileName);
}
@TestMetadata("withOR.kt")
public void testWithOR() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/intentions/splitIf/keepComments/withOR.kt");
doTest(fileName);
}
}
} }
@TestMetadata("idea/testData/intentions/swapBinaryExpression") @TestMetadata("idea/testData/intentions/swapBinaryExpression")