Add feature for data and inline class parameters smart typing

When one typing data/inline class primary ctor this feature adds missing val keyword for parameters
i.e.
data class xxx(x: Int<caret>)
when typing comma symbol convert code to
data class xxx(val x: Int,<caret>)

Fixed #KT-34567
This commit is contained in:
Igor Yakovlev
2019-10-23 21:54:44 +03:00
parent 0708f574fc
commit 63e687f67e
4 changed files with 432 additions and 286 deletions
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
public class KotlinEditorOptions implements PersistentStateComponent<KotlinEditorOptions> { public class KotlinEditorOptions implements PersistentStateComponent<KotlinEditorOptions> {
private boolean donTShowConversionDialog = false; private boolean donTShowConversionDialog = false;
private boolean enableJavaToKotlinConversion = true; private boolean enableJavaToKotlinConversion = true;
private boolean autoAddValKeywordToDataClassParameters = true;
public boolean isDonTShowConversionDialog() { public boolean isDonTShowConversionDialog() {
return donTShowConversionDialog; return donTShowConversionDialog;
@@ -42,6 +43,14 @@ public class KotlinEditorOptions implements PersistentStateComponent<KotlinEdito
this.donTShowConversionDialog = donTShowConversionDialog; this.donTShowConversionDialog = donTShowConversionDialog;
} }
public boolean isAutoAddValKeywordToDataClassParameters() {
return autoAddValKeywordToDataClassParameters;
}
public void setAutoAddValKeywordToDataClassParameters(boolean autoAddValKeywordToDataClassParameters) {
this.autoAddValKeywordToDataClassParameters = autoAddValKeywordToDataClassParameters;
}
public boolean isEnableJavaToKotlinConversion() { public boolean isEnableJavaToKotlinConversion() {
return enableJavaToKotlinConversion; return enableJavaToKotlinConversion;
} }
@@ -21,6 +21,9 @@ public class KotlinEditorOptionsConfigurable extends BeanConfigurable<KotlinEdit
checkBox("Don't show Java to Kotlin conversion dialog on paste", checkBox("Don't show Java to Kotlin conversion dialog on paste",
instance::isDonTShowConversionDialog, instance::isDonTShowConversionDialog,
instance::setDonTShowConversionDialog); instance::setDonTShowConversionDialog);
checkBox("Auto add val keyword to data/inline class constructor parameters",
instance::isAutoAddValKeywordToDataClassParameters,
instance::setAutoAddValKeywordToDataClassParameters);
} }
@Override @Override
@@ -21,6 +21,7 @@ import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
import com.intellij.codeInsight.highlighting.BraceMatcher; import com.intellij.codeInsight.highlighting.BraceMatcher;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil; import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.ide.PowerSaveMode;
import com.intellij.lang.ASTNode; import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Document;
@@ -82,6 +83,9 @@ public class KotlinTypedHandler extends TypedHandlerDelegate {
} }
switch (c) { switch (c) {
case ')':
dataClassValParameterInsert(project, editor, file, /*beforeType = */ true);
break;
case '<': case '<':
kotlinLTTyped = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET && kotlinLTTyped = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET &&
LtGtTypingUtils.shouldAutoCloseAngleBracket(editor.getCaretModel().getOffset(), editor); LtGtTypingUtils.shouldAutoCloseAngleBracket(editor.getCaretModel().getOffset(), editor);
@@ -263,6 +267,9 @@ public class KotlinTypedHandler extends TypedHandlerDelegate {
LtGtTypingUtils.handleKotlinAutoCloseLT(editor); LtGtTypingUtils.handleKotlinAutoCloseLT(editor);
return Result.STOP; return Result.STOP;
} }
else if (c == ',' || c == ')') {
dataClassValParameterInsert(project, editor, file, /*beforeType = */ false);
}
else if (c == '{' && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) { else if (c == '{' && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) {
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
@@ -328,6 +335,46 @@ public class KotlinTypedHandler extends TypedHandlerDelegate {
return Result.CONTINUE; return Result.CONTINUE;
} }
private static void dataClassValParameterInsert(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file, boolean beforeType) {
if (!KotlinEditorOptions.getInstance().isAutoAddValKeywordToDataClassParameters()) return;
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
int commaOffset = editor.getCaretModel().getOffset();
if (!beforeType) commaOffset--;
if (commaOffset < 1) return;
PsiElement elementOnCaret = file.findElementAt(commaOffset);
if (elementOnCaret == null) return;
boolean contextMatched = false;
PsiElement parentElement = elementOnCaret.getParent();
if (parentElement instanceof KtParameterList) {
parentElement = parentElement.getParent();
if (parentElement instanceof KtPrimaryConstructor) {
parentElement = parentElement.getParent();
if (parentElement instanceof KtClass) {
KtClass klassElement = ((KtClass)parentElement);
contextMatched = klassElement.isData() || klassElement.hasModifier(KtTokens.INLINE_KEYWORD);
}
}
}
if (!contextMatched) return;
PsiElement leftElement = PsiTreeUtil.skipWhitespacesAndCommentsBackward(elementOnCaret);
if (!(leftElement instanceof KtParameter)) return;
KtParameter ktParameter = (KtParameter)leftElement;
if (ktParameter.hasValOrVar()) return;
KtTypeReference typeReference = ktParameter.getTypeReference();
if (typeReference == null) return;
if (typeReference.getTextLength() == 0) return;
editor.getDocument().insertString(leftElement.getTextOffset(), "val ");
}
/** /**
* Copied from * Copied from
* *
@@ -503,7 +503,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
|open class A |open class A
|class B |class B
| :<caret> | :<caret>
""") """
)
} }
fun testColonOfSuperTypeListInObject() { fun testColonOfSuperTypeListInObject() {
@@ -518,7 +519,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
|interface A |interface A
|object B |object B
| :<caret> | :<caret>
""") """
)
} }
fun testColonOfSuperTypeListInCompanionObject() { fun testColonOfSuperTypeListInCompanionObject() {
@@ -537,7 +539,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
| companion object | companion object
| :<caret> | :<caret>
|} |}
""") """
)
} }
fun testColonOfSuperTypeListBeforeBody() { fun testColonOfSuperTypeListBeforeBody() {
@@ -554,7 +557,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
|class B |class B
| :<caret> { | :<caret> {
|} |}
""") """
)
} }
fun testColonOfSuperTypeListNotNullIndent() { fun testColonOfSuperTypeListNotNullIndent() {
@@ -573,7 +577,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
| class B | class B
| :<caret> | :<caret>
|} |}
""") """
)
} }
fun testChainCallContinueWithDot() { fun testChainCallContinueWithDot() {
@@ -592,7 +597,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
| Test() | Test()
| .<caret> | .<caret>
|} |}
""") """
)
} }
fun testChainCallContinueWithSafeCall() { fun testChainCallContinueWithSafeCall() {
@@ -611,7 +617,8 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
| Test() | Test()
| ?.<caret> | ?.<caret>
|} |}
""") """
)
} }
fun testContinueWithElvis() { fun testContinueWithElvis() {
@@ -821,6 +828,75 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
) )
} }
fun testValInserterOnClass() =
testValInserter(',', """data class xxx(val x: Int<caret>)""", """data class xxx(val x: Int,<caret>)""")
fun testValInserterOnSimpleDataClass() =
testValInserter(',', """data class xxx(x: Int<caret>)""", """data class xxx(val x: Int,<caret>)""")
fun testValInserterOnValWithComment() =
testValInserter(',', """data class xxx(x: Int /*comment*/ <caret>)""", """data class xxx(val x: Int /*comment*/ ,<caret>)""")
fun testValInserterOnValWithInitializer() =
testValInserter(',', """data class xxx(x: Int = 2<caret>)""", """data class xxx(val x: Int = 2,<caret>)""")
fun testValInserterOnValWithInitializerWithOutType() =
testValInserter(',', """data class xxx(x = 2<caret>)""", """data class xxx(x = 2,<caret>)""")
fun testValInserterOnValWithGenericType() =
testValInserter(',', """data class xxx(x: A<B><caret>)""", """data class xxx(val x: A<B>,<caret>)""")
fun testValInserterOnValWithNoType() =
testValInserter(',', """data class xxx(x<caret>)""", """data class xxx(x,<caret>)""")
fun testValInserterOnValWithIncompleteGenericType() =
testValInserter(',', """data class xxx(x: A<B,C<caret>)""", """data class xxx(x: A<B,C,<caret>)""")
fun testValInserterOnValWithInvalidComma() =
testValInserter(',', """data class xxx(x:<caret> A<B>)""", """data class xxx(x:,<caret> A<B>)""")
fun testValInserterOnValWithInvalidGenericType() =
testValInserter(',', """data class xxx(x: A><caret>)""", """data class xxx(x: A>,<caret>)""")
fun testValInserterOnInMultiline() =
testValInserter(
',',
"""
|data class xxx(
| val a: A,
| b: B<caret>
| val c: C
|)
""",
"""
|data class xxx(
| val a: A,
| val b: B,<caret>
| val c: C
|)
"""
)
fun testValInserterOnValInsertedInsideOtherParameters() =
testValInserter(
',',
"""data class xxx(val a: A, b: A<caret>val c: A)""",
"""data class xxx(val a: A, val b: A,<caret>val c: A)"""
)
fun testValInserterOnSimpleInlineClass() =
testValInserter(')', """inline class xxx(a: A<caret>)""", """inline class xxx(val a: A)<caret>""")
fun testValInserterOnValInsertedWithSquare() =
testValInserter(')', """data class xxx(val a: A, b: A<caret>)""", """data class xxx(val a: A, val b: A)<caret>""")
fun testValInserterOnTypingMissedSquare() =
testValInserter(')', """data class xxx(val a: A, b: A<caret>""", """data class xxx(val a: A, val b: A)<caret>""")
fun testValInserterWithDisabledSetting() =
testValInserter(',', """data class xxx(x: Int<caret>)""", """data class xxx(x: Int,<caret>)""", inserterEnabled = false)
fun testEnterInFunctionWithExpressionBody() { fun testEnterInFunctionWithExpressionBody() {
doTypeTest( doTypeTest(
'\n', '\n',
@@ -908,6 +984,17 @@ class TypedHandlerTest : LightCodeInsightTestCase() {
} }
} }
private fun testValInserter(ch: Char, beforeText: String, afterText: String, inserterEnabled: Boolean = true) {
val editorOptions = KotlinEditorOptions.getInstance()
val wasEnabled = editorOptions.isAutoAddValKeywordToDataClassParameters
try {
editorOptions.isAutoAddValKeywordToDataClassParameters = inserterEnabled
doTypeTest(ch, beforeText, afterText)
} finally {
editorOptions.isAutoAddValKeywordToDataClassParameters = wasEnabled
}
}
private fun doLtGtTestNoAutoClose(initText: String) { private fun doLtGtTestNoAutoClose(initText: String) {
doLtGtTest(initText, false) doLtGtTest(initText, false)