KT-11784 Completion for 'if' statement should add parentheses automatically

#KT-11784 Fixed
This commit is contained in:
Valentin Kipyatkov
2016-04-18 21:44:40 +03:00
parent bf4a376831
commit 2a3b87f783
37 changed files with 378 additions and 91 deletions
@@ -26,18 +26,17 @@ import com.intellij.codeInsight.template.TemplateManager
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.StandardPatterns
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.idea.completion.handlers.createKeywordConstructLookupElement
import org.jetbrains.kotlin.idea.completion.smart.ExpectedInfoMatch
import org.jetbrains.kotlin.idea.completion.smart.SMART_COMPLETION_ITEM_PRIORITY_KEY
import org.jetbrains.kotlin.idea.completion.smart.SmartCompletion
import org.jetbrains.kotlin.idea.completion.smart.SmartCompletionItemPriority
import org.jetbrains.kotlin.idea.core.ExpectedInfo
import org.jetbrains.kotlin.idea.project.ProjectStructureUtil
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.stubindex.PackageIndexUtil
import org.jetbrains.kotlin.idea.util.CallTypeAndReceiver
import org.jetbrains.kotlin.lexer.KtTokens
@@ -376,8 +375,8 @@ class BasicCompletionSession(
collector.addElement(lookupElement)
if (!isUseSiteAnnotationTarget) {
collector.addElement(createKeywordConstructLookupElement(keyword, "val v:Int get()=caret", "caret"))
collector.addElement(createKeywordConstructLookupElement(keyword, "val v:Int get(){caret}", "caret", trimSpacesAroundCaret = true))
collector.addElement(createKeywordConstructLookupElement(project, keyword, "val v:Int get()=caret"))
collector.addElement(createKeywordConstructLookupElement(project, keyword, "val v:Int get(){caret}", trimSpacesAroundCaret = true))
}
}
@@ -385,8 +384,8 @@ class BasicCompletionSession(
collector.addElement(lookupElement)
if (!isUseSiteAnnotationTarget) {
collector.addElement(createKeywordConstructLookupElement(keyword, "var v:Int set(value)=caret", "caret"))
collector.addElement(createKeywordConstructLookupElement(keyword, "var v:Int set(value){caret}", "caret", trimSpacesAroundCaret = true))
collector.addElement(createKeywordConstructLookupElement(project, keyword, "var v:Int set(value)=caret"))
collector.addElement(createKeywordConstructLookupElement(project, keyword, "var v:Int set(value){caret}", trimSpacesAroundCaret = true))
}
}
@@ -394,41 +393,6 @@ class BasicCompletionSession(
}
}
}
private fun createKeywordConstructLookupElement(
keyword: String,
fileTextToReformat: String,
caretPlaceHolder: String,
trimSpacesAroundCaret: Boolean = false
): LookupElement {
val file = KtPsiFactory(project).createFile(fileTextToReformat)
CodeStyleManager.getInstance(project).reformat(file)
val newFileText = file.text
val keywordOffset = newFileText.indexOf(keyword)
assert(keywordOffset >= 0)
val keywordEndOffset = keywordOffset + keyword.length
val caretOffset = newFileText.indexOf(caretPlaceHolder)
assert(caretOffset >= 0)
assert(caretOffset >= keywordEndOffset)
var tailBeforeCaret = newFileText.substring(keywordEndOffset, caretOffset)
var tailAfterCaret = newFileText.substring(caretOffset + caretPlaceHolder.length)
if (trimSpacesAroundCaret) {
tailBeforeCaret = tailBeforeCaret.trimEnd()
tailAfterCaret = tailAfterCaret.trimStart()
}
return LookupElementBuilder.create(KeywordLookupObject(), keyword + tailBeforeCaret + tailAfterCaret)
.withPresentableText(keyword)
.bold()
.withTailText(tailBeforeCaret + tailAfterCaret)
.withInsertHandler { insertionContext, lookupElement ->
insertionContext.editor.moveCaret(insertionContext.editor.caretModel.offset - tailAfterCaret.length)
}
}
}
private val NAMED_ARGUMENTS_ONLY = object : CompletionKind {
@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.descriptors.annotations.KotlinTarget.*
import org.jetbrains.kotlin.idea.completion.handlers.KotlinFunctionInsertHandler
import org.jetbrains.kotlin.idea.completion.handlers.KotlinKeywordInsertHandler
import org.jetbrains.kotlin.idea.completion.handlers.UseSiteAnnotationTargetInsertHandler
import org.jetbrains.kotlin.idea.completion.handlers.createKeywordConstructLookupElement
import org.jetbrains.kotlin.lexer.KtKeywordToken
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens
@@ -58,6 +59,19 @@ object KeywordCompletion {
ANNOTATION_KEYWORD to CLASS_KEYWORD
)
private val KEYWORD_CONSTRUCTS = mapOf<KtKeywordToken, String>(
IF_KEYWORD to "fun foo() { if (caret)",
WHILE_KEYWORD to "fun foo() { while(caret)",
FOR_KEYWORD to "fun foo() { for(caret)",
TRY_KEYWORD to "fun foo() { try {\ncaret\n}",
/* TODO!
CATCH_KEYWORD to "fun foo() { try {} catch (<caret>)",
*/
DO_KEYWORD to "fun foo() { do {\ncaret\n}",
INIT_KEYWORD to "class C { init {\ncaret\n}",
CONSTRUCTOR_KEYWORD to "class C { constructor(caret)"
)
fun complete(position: PsiElement, prefix: String, isJvmModule: Boolean, consumer: (LookupElement) -> Unit) {
if (!GENERAL_FILTER.isAcceptable(position, position)) return
@@ -85,23 +99,30 @@ object KeywordCompletion {
if (!parserFilter(keywordToken)) continue
var element = LookupElementBuilder.create(KeywordLookupObject(), keyword).bold()
val isUseSiteAnnotationTarget = position.prevLeaf()?.node?.elementType == KtTokens.AT
val insertHandler = if (isUseSiteAnnotationTarget)
UseSiteAnnotationTargetInsertHandler
else if (keywordToken !in FUNCTION_KEYWORDS)
KotlinKeywordInsertHandler
else
KotlinFunctionInsertHandler.Normal(inputTypeArguments = false, inputValueArguments = false)
element = element.withInsertHandler(insertHandler)
if (isUseSiteAnnotationTarget) {
element = element.withPresentableText(keyword + ":")
val constructText = KEYWORD_CONSTRUCTS[keywordToken]
if (constructText != null) {
val element = createKeywordConstructLookupElement(position.project, keyword, constructText, showConstructInLookup = false)
consumer(element)
}
else {
var element = LookupElementBuilder.create(KeywordLookupObject(), keyword).bold()
consumer(element)
val isUseSiteAnnotationTarget = position.prevLeaf()?.node?.elementType == KtTokens.AT
val insertHandler = if (isUseSiteAnnotationTarget)
UseSiteAnnotationTargetInsertHandler
else if (keywordToken !in FUNCTION_KEYWORDS)
KotlinKeywordInsertHandler
else
KotlinFunctionInsertHandler.Normal(inputTypeArguments = false, inputValueArguments = false)
element = element.withInsertHandler(insertHandler)
if (isUseSiteAnnotationTarget) {
element = element.withPresentableText(keyword + ":")
}
consumer(element)
}
}
}
@@ -18,8 +18,15 @@ package org.jetbrains.kotlin.idea.completion.handlers
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.Lookup
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.CodeStyleManager
import org.jetbrains.kotlin.idea.completion.KeywordLookupObject
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.lexer.KtTokens.*
import org.jetbrains.kotlin.psi.KtPsiFactory
object KotlinKeywordInsertHandler : InsertHandler<LookupElement> {
private val NO_SPACE_AFTER = listOf(THIS_KEYWORD,
@@ -51,6 +58,103 @@ object KotlinKeywordInsertHandler : InsertHandler<LookupElement> {
}
}
fun createKeywordConstructLookupElement(
project: Project,
keyword: String,
fileTextToReformat: String,
trimSpacesAroundCaret: Boolean = false,
showConstructInLookup: Boolean = true
): LookupElement {
val file = KtPsiFactory(project).createFile(fileTextToReformat)
CodeStyleManager.getInstance(project).reformat(file)
val newFileText = file.text
val keywordOffset = newFileText.indexOf(keyword)
assert(keywordOffset >= 0)
val keywordEndOffset = keywordOffset + keyword.length
val caretPlaceHolder = "caret"
val caretOffset = newFileText.indexOf(caretPlaceHolder)
assert(caretOffset >= 0)
assert(caretOffset >= keywordEndOffset)
var tailBeforeCaret = newFileText.substring(keywordEndOffset, caretOffset)
var tailAfterCaret = newFileText.substring(caretOffset + caretPlaceHolder.length)
if (trimSpacesAroundCaret) {
tailBeforeCaret = tailBeforeCaret.trimEnd()
tailAfterCaret = tailAfterCaret.trimStart()
}
val indent = detectIndent(newFileText, keywordOffset)
if (indent != null) {
tailBeforeCaret = tailBeforeCaret.unindent(indent)
tailAfterCaret = tailAfterCaret.unindent(indent)
}
var lookupElementBuilder = LookupElementBuilder.create(KeywordLookupObject(), keyword)
.bold()
.withInsertHandler { insertionContext, lookupElement ->
if (insertionContext.completionChar == Lookup.NORMAL_SELECT_CHAR || insertionContext.completionChar == Lookup.REPLACE_SELECT_CHAR) {
val offset = insertionContext.tailOffset
val newIndent = detectIndent(insertionContext.document.charsSequence, offset - keyword.length)
var beforeCaret = tailBeforeCaret
var afterCaret = tailAfterCaret
if (newIndent != null) {
beforeCaret = beforeCaret.indentLinesAfterFirst(newIndent)
afterCaret = afterCaret.indentLinesAfterFirst(newIndent)
}
insertionContext.document.insertString(offset, beforeCaret + afterCaret)
insertionContext.editor.moveCaret(offset + beforeCaret.length)
}
}
if (showConstructInLookup) {
lookupElementBuilder = lookupElementBuilder.withTailText(tailBeforeCaret + tailAfterCaret)
}
return lookupElementBuilder
}
private fun detectIndent(text: CharSequence, offset: Int): String? {
val reversedIndent = buildString {
var index = offset - 1
Loop@
while (index >= 0) {
val c = text[index]
when (c) {
' ', '\t' -> append(c)
'\r', '\n' -> break@Loop
else -> return null
}
index--
}
}
return reversedIndent.reversed()
}
private fun String.indentLinesAfterFirst(indent: String): String {
val text = this
return buildString {
val lines = text.lines()
for ((index, line) in lines.withIndex()) {
if (index > 0) append(indent)
append(line)
if (index != lines.lastIndex) append('\n')
}
}
}
private fun String.unindent(indent: String): String {
val text = this
return buildString {
val lines = text.lines()
for ((index, line) in lines.withIndex()) {
append(line.removePrefix(indent))
if (index != lines.lastIndex) append('\n')
}
}
}
object UseSiteAnnotationTargetInsertHandler : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
WithTailInsertHandler(":", spaceBefore = false, spaceAfter = false).postHandleInsert(context, item)
@@ -0,0 +1,10 @@
fun t() {
while (true) {
try {
}
catch (<caret>)
}
}
// ELEMENT: catch
@@ -0,0 +1,10 @@
fun t() {
while (true) {
try {
}
c<caret>
}
}
// ELEMENT: catch
@@ -0,0 +1,5 @@
class C {
c<caret>
}
// ELEMENT: constructor
@@ -0,0 +1,5 @@
class C {
constructor(<caret>)
}
// ELEMENT: constructor
+5
View File
@@ -0,0 +1,5 @@
fun t() {
d<caret>
}
// ELEMENT: do
@@ -0,0 +1,7 @@
fun t() {
do {
<caret>
}
}
// ELEMENT: do
@@ -0,0 +1,5 @@
fun t() {
f<caret>
}
// ELEMENT: for
@@ -0,0 +1,5 @@
fun t() {
for (<caret>)
}
// ELEMENT: for
+2 -1
View File
@@ -1,4 +1,5 @@
val v: Int
g<caret>
// ELEMENT: "get() = "
// ELEMENT: "get"
// TAIL_TEXT: "() = "
@@ -1,4 +1,5 @@
val v: Int
get() = <caret>
// ELEMENT: "get() = "
// ELEMENT: "get"
// TAIL_TEXT: "() = "
+2 -1
View File
@@ -1,4 +1,5 @@
val v: Int
g<caret>
// ELEMENT: "get() {}"
// ELEMENT: "get"
// TAIL_TEXT: "() {}"
@@ -1,4 +1,5 @@
val v: Int
get() {<caret>}
// ELEMENT: "get() {}"
// ELEMENT: "get"
// TAIL_TEXT: "() {}"
+7
View File
@@ -0,0 +1,7 @@
fun t() {
while (true) {
i<caret>
}
}
// ELEMENT: if
@@ -0,0 +1,7 @@
fun t() {
while (true) {
if (<caret>)
}
}
// ELEMENT: if
@@ -0,0 +1,8 @@
fun t() {
while (true) {
i<caret>
}
}
// ELEMENT: if
// CHAR: '('
@@ -0,0 +1,8 @@
fun t() {
while (true) {
if(<caret>)
}
}
// ELEMENT: if
// CHAR: '('
@@ -0,0 +1,8 @@
fun t() {
while (true) {
i<caret>
}
}
// ELEMENT: if
// CHAR: ' '
@@ -0,0 +1,8 @@
fun t() {
while (true) {
if <caret>
}
}
// ELEMENT: if
// CHAR: ' '
@@ -0,0 +1,5 @@
class C {
i<caret>
}
// ELEMENT: init
@@ -0,0 +1,7 @@
class C {
init {
<caret>
}
}
// ELEMENT: init
+2 -1
View File
@@ -2,4 +2,5 @@ var v: Int
get() = 0
s<caret>
// ELEMENT: "set(value) = "
// ELEMENT: "set"
// TAIL_TEXT: "(value) = "
@@ -2,4 +2,5 @@ var v: Int
get() = 0
set(value) = <caret>
// ELEMENT: "set(value) = "
// ELEMENT: "set"
// TAIL_TEXT: "(value) = "
+2 -1
View File
@@ -2,4 +2,5 @@ var v: Int
get() = 0
s<caret>
// ELEMENT: "set(value) {}"
// ELEMENT: "set"
// TAIL_TEXT: "(value) {}"
@@ -2,4 +2,5 @@ var v: Int
get() = 0
set(value) {<caret>}
// ELEMENT: "set(value) {}"
// ELEMENT: "set"
// TAIL_TEXT: "(value) {}"
@@ -0,0 +1,7 @@
fun t() {
while (true) {
t<caret>
}
}
// ELEMENT: try
@@ -0,0 +1,9 @@
fun t() {
while (true) {
try {
<caret>
}
}
}
// ELEMENT: try
@@ -0,0 +1,5 @@
fun t() {
w<caret>
}
// ELEMENT: while
@@ -0,0 +1,5 @@
fun t() {
while (<caret>)
}
// ELEMENT: while
@@ -10,9 +10,9 @@ class MouseMovedEventArgs
// EXIST: enum class
// EXIST: final
// EXIST: fun
// EXIST: get
// EXIST: "get() = "
// EXIST: "get() {}"
// EXIST: { itemText: "get", tailText: null }
// EXIST: { itemText: "get", tailText: "() = " }
// EXIST: { itemText: "get", tailText: "() {}" }
// EXIST: inner
// EXIST: internal
// EXIST: object
@@ -21,9 +21,9 @@ class MouseMovedEventArgs
// EXIST: private
// EXIST: protected
// EXIST: public
// EXIST: set
// EXIST: "set(value) = "
// EXIST: "set(value) {}"
// EXIST: { itemText: "set", tailText: null }
// EXIST: { itemText: "set", tailText: "(value) = " }
// EXIST: { itemText: "set", tailText: "(value) {}" }
// EXIST: interface
// EXIST: val
// EXIST: var
@@ -15,17 +15,17 @@ var a : Int
// EXIST: enum class
// EXIST: final
// EXIST: fun
// EXIST: get
// EXIST: "get() = "
// EXIST: "get() {}"
// EXIST: { itemText: "get", tailText: null }
// EXIST: { itemText: "get", tailText: "() = " }
// EXIST: { itemText: "get", tailText: "() {}" }
// EXIST: internal
// EXIST: object
// EXIST: open
// EXIST: private
// EXIST: public
// EXIST: set
// EXIST: "set(value) = "
// EXIST: "set(value) {}"
// EXIST: { itemText: "set", tailText: null }
// EXIST: { itemText: "set", tailText: "(value) = " }
// EXIST: { itemText: "set", tailText: "(value) {}" }
// EXIST: interface
// EXIST: val
// EXIST: var
@@ -9,9 +9,9 @@ class Some {
// EXIST: enum class
// EXIST: final
// EXIST: fun
// EXIST: get
// EXIST: "get() = "
// EXIST: "get() {}"
// EXIST: { itemText: "get", tailText: null }
// EXIST: { itemText: "get", tailText: "() = " }
// EXIST: { itemText: "get", tailText: "() {}" }
// EXIST: inner
// EXIST: internal
// EXIST: object
@@ -20,9 +20,9 @@ class Some {
// EXIST: private
// EXIST: protected
// EXIST: public
// EXIST: set
// EXIST: "set(value) = "
// EXIST: "set(value) {}"
// EXIST: { itemText: "set", tailText: null }
// EXIST: { itemText: "set", tailText: "(value) = " }
// EXIST: { itemText: "set", tailText: "(value) {}" }
// EXIST: interface
// EXIST: val
// EXIST: var
@@ -9,9 +9,9 @@ class Some {
// EXIST: enum class
// EXIST: final
// EXIST: fun
// EXIST: get
// EXIST: "get() = "
// EXIST: "get() {}"
// EXIST: { itemText: "get", tailText: null }
// EXIST: { itemText: "get", tailText: "() = " }
// EXIST: { itemText: "get", tailText: "() {}" }
// EXIST: inner
// EXIST: internal
// EXIST: object
@@ -20,9 +20,9 @@ class Some {
// EXIST: private
// EXIST: protected
// EXIST: public
// EXIST: set
// EXIST: "set(value) = "
// EXIST: "set(value) {}"
// EXIST: { itemText: "set", tailText: null }
// EXIST: { itemText: "set", tailText: "(value) = " }
// EXIST: { itemText: "set", tailText: "(value) {}" }
// EXIST: interface
// EXIST: val
// EXIST: var
+3 -3
View File
@@ -18,9 +18,9 @@ class Some {
// EXIST: private
// EXIST: protected
// EXIST: public
// EXIST: set
// EXIST: "set(value) = "
// EXIST: "set(value) {}"
// EXIST: { itemText: "set", tailText: null }
// EXIST: { itemText: "set", tailText: "(value) = " }
// EXIST: { itemText: "set", tailText: "(value) {}" }
// EXIST: interface
// EXIST: val
// EXIST: var
@@ -53,12 +53,30 @@ public class KeywordCompletionHandlerTestGenerated extends AbstractKeywordComple
doTest(fileName);
}
@TestMetadata("Constructor.kt")
public void testConstructor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/Constructor.kt");
doTest(fileName);
}
@TestMetadata("Do.kt")
public void testDo() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/Do.kt");
doTest(fileName);
}
@TestMetadata("FileKeyword.kt")
public void testFileKeyword() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/FileKeyword.kt");
doTest(fileName);
}
@TestMetadata("For.kt")
public void testFor() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/For.kt");
doTest(fileName);
}
@TestMetadata("Getter1.kt")
public void testGetter1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/Getter1.kt");
@@ -71,6 +89,30 @@ public class KeywordCompletionHandlerTestGenerated extends AbstractKeywordComple
doTest(fileName);
}
@TestMetadata("If.kt")
public void testIf() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/If.kt");
doTest(fileName);
}
@TestMetadata("IfLParenth.kt")
public void testIfLParenth() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/IfLParenth.kt");
doTest(fileName);
}
@TestMetadata("IfSpace.kt")
public void testIfSpace() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/IfSpace.kt");
doTest(fileName);
}
@TestMetadata("Init.kt")
public void testInit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/Init.kt");
doTest(fileName);
}
@TestMetadata("NoSpaceAfterNull.kt")
public void testNoSpaceAfterNull() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/NoSpaceAfterNull.kt");
@@ -149,6 +191,12 @@ public class KeywordCompletionHandlerTestGenerated extends AbstractKeywordComple
doTest(fileName);
}
@TestMetadata("Try.kt")
public void testTry() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/Try.kt");
doTest(fileName);
}
@TestMetadata("UseSiteAnnotationTarget1.kt")
public void testUseSiteAnnotationTarget1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/UseSiteAnnotationTarget1.kt");
@@ -166,4 +214,10 @@ public class KeywordCompletionHandlerTestGenerated extends AbstractKeywordComple
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/UseSiteAnnotationTarget3.kt");
doTest(fileName);
}
@TestMetadata("While.kt")
public void testWhile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/keywords/While.kt");
doTest(fileName);
}
}