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
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> {
init {
if (first == null) {
@@ -17,13 +17,18 @@
package org.jetbrains.kotlin.idea.core
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementVisitor
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.psiUtil.*
import java.util.*
import java.util.ArrayList
import java.util.HashMap
import java.util.LinkedHashMap
public class CommentSaver(originalElements: PsiChildRange) {
public constructor(originalElement: PsiElement) : this(PsiChildRange.singleElement(originalElement))
@@ -60,6 +65,8 @@ public class CommentSaver(originalElements: PsiChildRange) {
val nextElements: Sequence<TreeElement>
get() = nextSiblings.flatMap { it.withDescendants(leftToRight = true) }
// var debugText: String? = null
}
private data class CommentData(
@@ -98,7 +105,8 @@ public class CommentSaver(originalElements: PsiChildRange) {
assert(child.savedTreeElement == null)
val savedChild = TreeElement(parent = parentTreeElement, prev = last)
child.putCopyableUserData(SAVED_TREE_KEY, savedChild)
// savedChild.debugText = child.getText()
child.savedTreeElement = savedChild
last?.next = savedChild
last = savedChild
@@ -116,8 +124,9 @@ public class CommentSaver(originalElements: PsiChildRange) {
private val PsiElement.shouldSave: Boolean
get() = this !is PsiWhiteSpace
private val PsiElement.savedTreeElement: TreeElement?
private var PsiElement.savedTreeElement: TreeElement?
get() = getCopyableUserData(SAVED_TREE_KEY)
set(value) = putCopyableUserData(SAVED_TREE_KEY, value)
public fun deleteCommentsInside(element: PsiElement) {
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) {
restoreComments(PsiChildRange.singleElement(resultElement))
}
@@ -141,7 +176,7 @@ public class CommentSaver(originalElements: PsiChildRange) {
resultElements.forEach {
it.accept(object : PsiRecursiveElementVisitor() {
override fun visitElement(element: PsiElement) {
element.putCopyableUserData(SAVED_TREE_KEY, null)
element.savedTreeElement = null
super.visitElement(element)
}
})
@@ -55,7 +55,7 @@ public class InvertIfConditionIntention : JetSelfTargetingIntention<JetIfExpress
val elseBranch = ifExpression.getElse() ?: psiFactory.createEmptyBody()
val newThen = if (elseBranch is JetIfExpression)
psiFactory.wrapInABlock(elseBranch)
psiFactory.createSingleStatementBlock(elseBranch)
else
elseBranch
@@ -17,10 +17,16 @@
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import com.intellij.openapi.util.TextRange
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.psiUtil.PsiChildRange
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.lastBlockStatementOrThis
import org.jetbrains.kotlin.psi.psiUtil.startOffset
@@ -40,8 +46,11 @@ public class SplitIfIntention : JetSelfTargetingIntention<JetExpression>(javaCla
}
val ifExpression = operator.getNonStrictParentOfType<JetIfExpression>()
val commentSaver = CommentSaver(ifExpression!!)
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 thenBranch = ifExpression.getThen()!!
val elseBranch = ifExpression.getElse()
@@ -50,32 +59,41 @@ public class SplitIfIntention : JetSelfTargetingIntention<JetExpression>(javaCla
val innerIf = psiFactory.createIf(rightExpression, thenBranch, elseBranch)
when (operator.getReferencedNameElementType()) {
JetTokens.ANDAND -> ifExpression.replace(psiFactory.createIf(leftExpression, psiFactory.wrapInABlock(innerIf), elseBranch))
val newIf = when (operator.getReferencedNameElementType()) {
JetTokens.ANDAND -> psiFactory.createIf(leftExpression, psiFactory.createSingleStatementBlock(innerIf), elseBranch)
JetTokens.OROR -> {
val container = ifExpression.getParent()
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)
ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch))
val firstIf = ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch))
commentSaver.restoreComments(PsiChildRange(firstIf, secondIf))
return
}
ifExpression.replace(psiFactory.createIf(leftExpression, thenBranch, innerIf))
else {
psiFactory.createIf(leftExpression, thenBranch, innerIf)
}
}
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
val startOffset = element.getRight()!!.startOffset - condition.startOffset
val rightString = condition.getText()!!.substring(startOffset, condition.getTextLength())
val conditionRange = condition.range
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? {
@@ -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 {
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");
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")