From 5587af95dea6a81f55f647e32428bfb3d6ca60c7 Mon Sep 17 00:00:00 2001 From: Nikolay Krasko Date: Fri, 18 Jan 2019 12:39:09 +0300 Subject: [PATCH] Auto-close `{}`` before identifiers in string only after typed '$' (KT-11143) #KT-11143 Fixed --- .../idea/editor/KotlinTypedHandler.java | 35 ++++- .../kotlin/idea/editor/TypedHandlerTest.kt | 144 ++++++++++-------- 2 files changed, 111 insertions(+), 68 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java index a7a48cfdf4a..f879df04000 100644 --- a/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java +++ b/idea/src/org/jetbrains/kotlin/idea/editor/KotlinTypedHandler.java @@ -33,6 +33,7 @@ import com.intellij.openapi.editor.highlighter.HighlighterIterator; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.Key; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.formatter.FormatterUtil; @@ -63,6 +64,9 @@ public class KotlinTypedHandler extends TypedHandlerDelegate { private boolean kotlinLTTyped; + private boolean isGlobalPreviousDollarInString; // Global flag for all editors + private static final Key PREVIOUS_IN_STRING_DOLLAR_TYPED_OFFSET_KEY = Key.create("PREVIOUS_IN_STRING_DOLLAR_TYPED_OFFSET_KEY"); + @Override public Result beforeCharTyped(char c, Project project, Editor editor, PsiFile file, FileType fileType) { if (!(file instanceof KtFile)) { @@ -238,22 +242,35 @@ public class KotlinTypedHandler extends TypedHandlerDelegate { return Result.CONTINUE; } + Integer previousDollarInStringOffset = null; + if (isGlobalPreviousDollarInString) { + isGlobalPreviousDollarInString = false; + previousDollarInStringOffset = editor.getUserData(PREVIOUS_IN_STRING_DOLLAR_TYPED_OFFSET_KEY); + } + editor.putUserData(PREVIOUS_IN_STRING_DOLLAR_TYPED_OFFSET_KEY, null); + if (kotlinLTTyped) { kotlinLTTyped = false; LtGtTypingUtils.handleKotlinAutoCloseLT(editor); return Result.STOP; } else if (c == '{' && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) { - PsiDocumentManager.getInstance(project).commitAllDocuments(); + PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); int offset = editor.getCaretModel().getOffset(); PsiElement previousElement = file.findElementAt(offset - 1); if (previousElement instanceof LeafPsiElement - && ((LeafPsiElement) previousElement).getElementType() == KtTokens.LONG_TEMPLATE_ENTRY_START) { - PsiElement currentElement = file.findElementAt(offset); + && ((LeafPsiElement) previousElement).getElementType() == KtTokens.LONG_TEMPLATE_ENTRY_START + ) { + if (previousDollarInStringOffset != null && previousDollarInStringOffset.intValue() == offset - 1) { + editor.getDocument().insertString(offset, "}"); + return Result.STOP; + } - if (currentElement instanceof LeafPsiElement - && ((LeafPsiElement) currentElement).getElementType() != KtTokens.IDENTIFIER) { + PsiElement currentElement = file.findElementAt(offset); + boolean isNextTokenIsIdentifier = currentElement instanceof LeafPsiElement && + ((LeafPsiElement) currentElement).getElementType() != KtTokens.IDENTIFIER; + if (isNextTokenIsIdentifier) { editor.getDocument().insertString(offset, "}"); return Result.STOP; } @@ -280,6 +297,14 @@ public class KotlinTypedHandler extends TypedHandlerDelegate { return Result.STOP; } } + else if (c == '$') { + int offset = editor.getCaretModel().getOffset(); + PsiElement element = file.findElementAt(offset); + if (element instanceof LeafPsiElement && ((LeafPsiElement) element).getElementType() == KtTokens.REGULAR_STRING_PART) { + editor.putUserData(PREVIOUS_IN_STRING_DOLLAR_TYPED_OFFSET_KEY, offset); + isGlobalPreviousDollarInString = true; + } + } return Result.CONTINUE; } diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/TypedHandlerTest.kt b/idea/tests/org/jetbrains/kotlin/idea/editor/TypedHandlerTest.kt index c5a2604f150..afdddc919db 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/editor/TypedHandlerTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/TypedHandlerTest.kt @@ -26,13 +26,13 @@ import org.jetbrains.kotlin.idea.KotlinFileType class TypedHandlerTest : LightCodeInsightTestCase() { private val dollar = '$' - fun testTypeStringTemplateStart() = doCharTypeTest( + fun testTypeStringTemplateStart() = doTypeTest( '{', """val x = "$" """, """val x = "$dollar{}" """ ) - fun testAutoIndentRightOpenBrace() = doCharTypeTest( + fun testAutoIndentRightOpenBrace() = doTypeTest( '{', "fun test() {\n" + @@ -44,7 +44,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoIndentLeftOpenBrace() = doCharTypeTest( + fun testAutoIndentLeftOpenBrace() = doTypeTest( '{', "fun test() {\n" + @@ -56,37 +56,49 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testTypeStringTemplateStartWithCloseBraceAfter() = doCharTypeTest( + fun testTypeStringTemplateStartWithCloseBraceAfter() = doTypeTest( '{', """fun foo() { "$" }""", """fun foo() { "$dollar{}" }""" ) - fun testTypeStringTemplateStartBeforeString() = doCharTypeTest( + fun testTypeStringTemplateStartBeforeStringWithExistingDollar() = doTypeTest( '{', """fun foo() { "$something" }""", """fun foo() { "$dollar{something" }""" ) - fun testKT3575() = doCharTypeTest( + fun testTypeStringTemplateStartBeforeStringWithNoDollar() = doTypeTest( + "$dollar{", + """fun foo() { "something" }""", + """fun foo() { "$dollar{}something" }""" + ) + + fun testTypeStringTemplateStartInEmptyString() = doTypeTest( + '{', + """fun foo() { "$" }""", + """fun foo() { "$dollar{}" }""" + ) + + fun testKT3575() = doTypeTest( '{', """val x = "$]" """, """val x = "$dollar{}]" """ ) - fun testAutoCloseRawStringInEnd() = doCharTypeTest( + fun testAutoCloseRawStringInEnd() = doTypeTest( '"', """val x = """"", """val x = ""${'"'}""${'"'}""" ) - fun testNoAutoCloseRawStringInEnd() = doCharTypeTest( + fun testNoAutoCloseRawStringInEnd() = doTypeTest( '"', """val x = ""${'"'}""", """val x = ""${'"'}"""" ) - fun testAutoCloseRawStringInMiddle() = doCharTypeTest( + fun testAutoCloseRawStringInMiddle() = doTypeTest( '"', """ val x = "" @@ -98,31 +110,31 @@ class TypedHandlerTest : LightCodeInsightTestCase() { """.trimIndent() ) - fun testNoAutoCloseBetweenMultiQuotes() = doCharTypeTest( + fun testNoAutoCloseBetweenMultiQuotes() = doTypeTest( '"', """val x = ""${'"'}${'"'}""/**/""", """val x = ""${'"'}${'"'}""/**/""" ) - fun testNoAutoCloseBetweenMultiQuotes1() = doCharTypeTest( + fun testNoAutoCloseBetweenMultiQuotes1() = doTypeTest( '"', """val x = ""${'"'}""${'"'}/**/""", """val x = ""${'"'}""${'"'}/**/""" ) - fun testNoAutoCloseAfterEscape() = doCharTypeTest( + fun testNoAutoCloseAfterEscape() = doTypeTest( '"', """val x = "\""""", """val x = "\""${'"'}"""" ) - fun testAutoCloseBraceInFunctionDeclaration() = doCharTypeTest( + fun testAutoCloseBraceInFunctionDeclaration() = doTypeTest( '{', "fun foo() ", "fun foo() {}" ) - fun testAutoCloseBraceInLocalFunctionDeclaration() = doCharTypeTest( + fun testAutoCloseBraceInLocalFunctionDeclaration() = doTypeTest( '{', "fun foo() {\n" + @@ -134,7 +146,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoCloseBraceInAssignment() = doCharTypeTest( + fun testAutoCloseBraceInAssignment() = doTypeTest( '{', "fun foo() {\n" + " val a = \n" + @@ -145,7 +157,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -157,7 +169,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -169,7 +181,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedTryOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedTryOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -181,7 +193,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedCatchOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedCatchOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -193,7 +205,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedFinallyOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedFinallyOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -205,7 +217,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedWhileSurroundOnSameLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedWhileSurroundOnSameLine() = doTypeTest( '{', "fun foo() {\n" + @@ -217,7 +229,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedWhileSurroundOnNewLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedWhileSurroundOnNewLine() = doTypeTest( '{', "fun foo() {\n" + @@ -233,7 +245,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnOtherLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnOtherLine() = doTypeTest( '{', "fun foo() {\n" + @@ -247,7 +259,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnOtherLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnOtherLine() = doTypeTest( '{', "fun foo() {\n" + @@ -261,7 +273,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedTryOnOtherLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedTryOnOtherLine() = doTypeTest( '{', "fun foo() {\n" + @@ -275,7 +287,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnNewLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedIfSurroundOnNewLine() = doTypeTest( '{', "fun foo() {\n" + @@ -291,7 +303,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnNewLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedElseSurroundOnNewLine() = doTypeTest( '{', "fun foo() {\n" + @@ -307,7 +319,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testDoNotAutoCloseBraceInUnfinishedTryOnNewLine() = doCharTypeTest( + fun testDoNotAutoCloseBraceInUnfinishedTryOnNewLine() = doTypeTest( '{', "fun foo() {\n" + @@ -323,7 +335,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoCloseBraceInsideFor() = doCharTypeTest( + fun testAutoCloseBraceInsideFor() = doTypeTest( '{', "fun foo() {\n" + @@ -337,7 +349,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoCloseBraceInsideForAfterCloseParen() = doCharTypeTest( + fun testAutoCloseBraceInsideForAfterCloseParen() = doTypeTest( '{', "fun foo() {\n" + @@ -351,7 +363,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoCloseBraceBeforeIf() = doCharTypeTest( + fun testAutoCloseBraceBeforeIf() = doTypeTest( '{', "fun foo() {\n" + @@ -363,7 +375,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoCloseBraceInIfCondition() = doCharTypeTest( + fun testAutoCloseBraceInIfCondition() = doTypeTest( '{', "fun foo() {\n" + @@ -375,54 +387,54 @@ class TypedHandlerTest : LightCodeInsightTestCase() { "}" ) - fun testAutoInsertParenInStringLiteral() = doCharTypeTest( + fun testAutoInsertParenInStringLiteral() = doTypeTest( '(', """fun f() { println("$dollar{f}") }""", """fun f() { println("$dollar{f()}") }""" ) - fun testAutoInsertParenInCode() = doCharTypeTest( + fun testAutoInsertParenInCode() = doTypeTest( '(', """fun f() { val a = f }""", """fun f() { val a = f() }""" ) - fun testSplitStringByEnter() = doCharTypeTest( + fun testSplitStringByEnter() = doTypeTest( '\n', """val s = "foobar"""", "val s = \"foo\" +\n" + " \"bar\"" ) - fun testSplitStringByEnterEmpty() = doCharTypeTest( + fun testSplitStringByEnterEmpty() = doTypeTest( '\n', """val s = """"", "val s = \"\" +\n" + " \"\"" ) - fun testSplitStringByEnterBeforeEscapeSequence() = doCharTypeTest( + fun testSplitStringByEnterBeforeEscapeSequence() = doTypeTest( '\n', """val s = "foo\nbar"""", "val s = \"foo\" +\n" + " \"\\nbar\"" ) - fun testSplitStringByEnterBeforeSubstitution() = doCharTypeTest( + fun testSplitStringByEnterBeforeSubstitution() = doTypeTest( '\n', """val s = "foo${dollar}bar"""", "val s = \"foo\" +\n" + " \"${dollar}bar\"" ) - fun testSplitStringByEnterAddParentheses() = doCharTypeTest( + fun testSplitStringByEnterAddParentheses() = doTypeTest( '\n', """val l = "foobar".length()""", "val l = (\"foo\" +\n" + " \"bar\").length()" ) - fun testSplitStringByEnterExistingParentheses() = doCharTypeTest( + fun testSplitStringByEnterExistingParentheses() = doTypeTest( '\n', """val l = ("asdf" + "foobar").length()""", "val l = (\"asdf\" + \"foo\" +\n" + @@ -462,7 +474,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testColonOfSuperTypeList() { - doCharTypeTest( + doTypeTest( ':', """ |open class A @@ -477,7 +489,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testColonOfSuperTypeListInObject() { - doCharTypeTest( + doTypeTest( ':', """ |interface A @@ -492,7 +504,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testColonOfSuperTypeListInCompanionObject() { - doCharTypeTest( + doTypeTest( ':', """ |interface A @@ -511,7 +523,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testColonOfSuperTypeListBeforeBody() { - doCharTypeTest( + doTypeTest( ':', """ |open class A @@ -528,7 +540,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testColonOfSuperTypeListNotNullIndent() { - doCharTypeTest( + doTypeTest( ':', """ |fun test() { @@ -547,7 +559,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testChainCallContinueWithDot() { - doCharTypeTest( + doTypeTest( '.', """ |class Test{ fun test() = this } @@ -566,7 +578,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testChainCallContinueWithSafeCall() { - doCharTypeTest( + doTypeTest( '.', """ |class Test{ fun test() = this } @@ -585,7 +597,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testContinueWithElvis() { - doCharTypeTest( + doTypeTest( ':', """ |fun test(): Any? = null @@ -605,7 +617,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testContinueWithOr() { - doCharTypeTest( + doTypeTest( '|', """ |fun some() { @@ -623,7 +635,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testContinueWithAnd() { - doCharTypeTest( + doTypeTest( '&', """ |fun some() { @@ -641,7 +653,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testSpaceAroundRange() { - doCharTypeTest( + doTypeTest( '.', """ | val test = 1 @@ -653,7 +665,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testIndentBeforeElseWithBlock() { - doCharTypeTest( + doTypeTest( '\n', """ |fun test(b: Boolean) { @@ -676,7 +688,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testIndentBeforeElseWithoutBlock() { - doCharTypeTest( + doTypeTest( '\n', """ |fun test(b: Boolean) { @@ -699,7 +711,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testIndentOnFinishedVariableEndAfterEquals() { - doCharTypeTest( + doTypeTest( '\n', """ |fun test() { @@ -718,7 +730,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testIndentNotFinishedVariableEndAfterEquals() { - doCharTypeTest( + doTypeTest( '\n', """ |fun test() { @@ -735,7 +747,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testSmartEnterWithTabsOnConstructorParameters() { - doCharTypeTest( + doTypeTest( '\n', """ |class A( @@ -753,7 +765,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testSmartEnterWithTabsInMethodParameters() { - doCharTypeTest( + doTypeTest( '\n', """ |fun method( @@ -771,7 +783,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testAutoIndentInWhenClause() { - doCharTypeTest( + doTypeTest( '\n', """ |fun test() { @@ -800,7 +812,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } fun testCharClosingQuote() { - doCharTypeTest('\'', "val c = ", "val c = ''") + doTypeTest('\'', "val c = ", "val c = ''") } private fun enableSmartEnterWithTabs(): () -> Unit = { @@ -810,17 +822,22 @@ class TypedHandlerTest : LightCodeInsightTestCase() { indentOptions.SMART_TABS = true } - private fun doCharTypeTest(ch: Char, beforeText: String, afterText: String, settingsModifier: (() -> Unit)? = null) { + private fun doTypeTest(ch: Char, beforeText: String, afterText: String, settingsModifier: (() -> Unit)? = null) { + doTypeTest(ch.toString(), beforeText, afterText, settingsModifier) + } + + private fun doTypeTest(text: String, beforeText: String, afterText: String, settingsModifier: (() -> Unit)? = null) { try { if (settingsModifier != null) { settingsModifier() } LightPlatformCodeInsightTestCase.configureFromFileText("a.kt", beforeText.trimMargin()) - EditorTestUtil.performTypingAction(LightPlatformCodeInsightTestCase.getEditor(), ch) + for (ch in text) { + EditorTestUtil.performTypingAction(LightPlatformCodeInsightTestCase.getEditor(), ch) + } checkResultByText(afterText.trimMargin()) - } - finally { + } finally { if (settingsModifier != null) { val project = LightPlatformTestCase.getProject() CodeStyleSettingsManager.getSettings(project).clearCodeStyleSettings() @@ -828,6 +845,7 @@ class TypedHandlerTest : LightCodeInsightTestCase() { } } + private fun doLtGtTestNoAutoClose(initText: String) { doLtGtTest(initText, false) }