KT-13220: Add completion for variable names

Using parameter name completion

#KT-13220 fixed
This commit is contained in:
Simon Ogorodnik
2017-10-17 00:17:27 +03:00
parent e65adeb029
commit 7a991ddc25
8 changed files with 134 additions and 17 deletions
@@ -195,6 +195,10 @@ class BasicCompletionSession(
if (declaration != null) { if (declaration != null) {
completeDeclarationNameFromUnresolvedOrOverride(declaration) completeDeclarationNameFromUnresolvedOrOverride(declaration)
if (declaration is KtProperty) {
completeVariableName(declaration.modifierList?.hasModifier(KtTokens.LATEINIT_KEYWORD) == true)
}
// no auto-popup on typing after "val", "var" and "fun" because it's likely the name of the declaration which is being typed by user // no auto-popup on typing after "val", "var" and "fun" because it's likely the name of the declaration which is being typed by user
if (parameters.invocationCount == 0) { if (parameters.invocationCount == 0) {
val suppressOtherCompletion = when (declaration) { val suppressOtherCompletion = when (declaration) {
@@ -475,6 +479,29 @@ class BasicCompletionSession(
else -> null else -> null
} }
} }
private fun completeVariableName(withType: Boolean) {
val variableNameAndTypeCompletion = VariableOrParameterNameWithTypeCompletion(collector, basicLookupElementFactory, prefixMatcher, resolutionFacade, withType)
// if we are typing parameter name, restart completion each time we type an upper case letter because new suggestions will appear (previous words can be used as user prefix)
val prefixPattern = StandardPatterns.string().with(object : PatternCondition<String>("Prefix ends with uppercase letter") {
override fun accepts(prefix: String, context: ProcessingContext?) = prefix.isNotEmpty() && prefix.last().isUpperCase()
})
collector.restartCompletionOnPrefixChange(prefixPattern)
collector.addLookupElementPostProcessor { lookupElement ->
lookupElement.putUserData(KotlinCompletionCharFilter.HIDE_LOOKUP_ON_COLON, Unit)
lookupElement
}
variableNameAndTypeCompletion.addFromParametersInFile(position, resolutionFacade, isVisibleFilterCheckAlways)
flushToResultSet()
variableNameAndTypeCompletion.addFromImportedClasses(position, bindingContext, isVisibleFilterCheckAlways)
flushToResultSet()
variableNameAndTypeCompletion.addFromAllClasses(parameters, indicesHelper(false))
}
} }
private val KEYWORDS_ONLY = object : CompletionKind { private val KEYWORDS_ONLY = object : CompletionKind {
@@ -641,13 +668,13 @@ class BasicCompletionSession(
override fun addWeighers(sorter: CompletionSorter): CompletionSorter { override fun addWeighers(sorter: CompletionSorter): CompletionSorter {
if (shouldCompleteParameterNameAndType()) { if (shouldCompleteParameterNameAndType()) {
return sorter.weighBefore("prefix", ParameterNameAndTypeCompletion.Weigher) return sorter.weighBefore("prefix", VariableOrParameterNameWithTypeCompletion.Weigher)
} }
return sorter return sorter
} }
private fun completeParameterNameAndType() { private fun completeParameterNameAndType() {
val parameterNameAndTypeCompletion = ParameterNameAndTypeCompletion(collector, basicLookupElementFactory, prefixMatcher, resolutionFacade) val parameterNameAndTypeCompletion = VariableOrParameterNameWithTypeCompletion(collector, basicLookupElementFactory, prefixMatcher, resolutionFacade, true)
// if we are typing parameter name, restart completion each time we type an upper case letter because new suggestions will appear (previous words can be used as user prefix) // if we are typing parameter name, restart completion each time we type an upper case letter because new suggestions will appear (previous words can be used as user prefix)
val prefixPattern = StandardPatterns.string().with(object : PatternCondition<String>("Prefix ends with uppercase letter") { val prefixPattern = StandardPatterns.string().with(object : PatternCondition<String>("Prefix ends with uppercase letter") {
@@ -134,7 +134,7 @@ class KotlinCompletionContributor : CompletionContributor() {
if (tokenAt.node.elementType == KtTokens.IDENTIFIER) { if (tokenAt.node.elementType == KtTokens.IDENTIFIER) {
val parameter = tokenAt.parent as? KtParameter val parameter = tokenAt.parent as? KtParameter
if (parameter != null) { if (parameter != null) {
context.offsetMap.addOffset(ParameterNameAndTypeCompletion.REPLACEMENT_OFFSET, parameter.endOffset) context.offsetMap.addOffset(VariableOrParameterNameWithTypeCompletion.REPLACEMENT_OFFSET, parameter.endOffset)
} }
} }
} }
@@ -47,11 +47,12 @@ import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.isError import org.jetbrains.kotlin.types.isError
import java.util.* import java.util.*
class ParameterNameAndTypeCompletion( class VariableOrParameterNameWithTypeCompletion(
private val collector: LookupElementsCollector, private val collector: LookupElementsCollector,
private val lookupElementFactory: BasicLookupElementFactory, private val lookupElementFactory: BasicLookupElementFactory,
private val prefixMatcher: PrefixMatcher, private val prefixMatcher: PrefixMatcher,
private val resolutionFacade: ResolutionFacade private val resolutionFacade: ResolutionFacade,
private val withType: Boolean
) { ) {
private val userPrefixes: List<String> private val userPrefixes: List<String>
private val classNamePrefixMatchers: List<PrefixMatcher> private val classNamePrefixMatchers: List<PrefixMatcher>
@@ -114,7 +115,7 @@ class ParameterNameAndTypeCompletion(
if (descriptor != null) { if (descriptor != null) {
val parameterType = descriptor.type val parameterType = descriptor.type
if (parameterType.isVisible(visibilityFilter)) { if (parameterType.isVisible(visibilityFilter)) {
val lookupElement = MyLookupElement.create(name, ArbitraryType(parameterType), lookupElementFactory)!! val lookupElement = MyLookupElement.create(name, ArbitraryType(parameterType), withType, lookupElementFactory)!!
val count = lookupElementToCount[lookupElement] ?: 0 val count = lookupElementToCount[lookupElement] ?: 0
lookupElementToCount[lookupElement] = count + 1 lookupElementToCount[lookupElement] = count + 1
} }
@@ -144,7 +145,7 @@ class ParameterNameAndTypeCompletion(
for (name in nameSuggestions) { for (name in nameSuggestions) {
val parameterName = userPrefix + name val parameterName = userPrefix + name
if (prefixMatcher.isStartMatch(parameterName)) { if (prefixMatcher.isStartMatch(parameterName)) {
val lookupElement = MyLookupElement.create(parameterName, type, lookupElementFactory) val lookupElement = MyLookupElement.create(parameterName, type, withType, lookupElementFactory)
if (lookupElement != null) { if (lookupElement != null) {
lookupElement.putUserData(PRIORITY_KEY, userPrefix.length) // suggestions with longer user prefix get lower priority lookupElement.putUserData(PRIORITY_KEY, userPrefix.length) // suggestions with longer user prefix get lower priority
collector.addElement(lookupElement, notImported) collector.addElement(lookupElement, notImported)
@@ -185,13 +186,14 @@ class ParameterNameAndTypeCompletion(
private class MyLookupElement private constructor( private class MyLookupElement private constructor(
private val parameterName: String, private val parameterName: String,
private val type: Type, private val type: Type,
typeLookupElement: LookupElement typeLookupElement: LookupElement,
private val shouldInsertType: Boolean
) : LookupElementDecorator<LookupElement>(typeLookupElement) { ) : LookupElementDecorator<LookupElement>(typeLookupElement) {
companion object { companion object {
fun create(parameterName: String, type: Type, factory: BasicLookupElementFactory): LookupElement? { fun create(parameterName: String, type: Type, shouldInsertType: Boolean, factory: BasicLookupElementFactory): LookupElement? {
val typeLookupElement = type.createTypeLookupElement(factory) ?: return null val typeLookupElement = type.createTypeLookupElement(factory) ?: return null
val lookupElement = MyLookupElement(parameterName, type, typeLookupElement) val lookupElement = MyLookupElement(parameterName, type, typeLookupElement, shouldInsertType)
return lookupElement.suppressAutoInsertion() return lookupElement.suppressAutoInsertion()
} }
} }
@@ -204,7 +206,13 @@ class ParameterNameAndTypeCompletion(
override fun renderElement(presentation: LookupElementPresentation) { override fun renderElement(presentation: LookupElementPresentation) {
super.renderElement(presentation) super.renderElement(presentation)
presentation.itemText = parameterName + ": " + presentation.itemText if (shouldInsertType) {
presentation.itemText = parameterName + ": " + presentation.itemText
}
else {
presentation.prependTailText(": " + presentation.itemText, true)
presentation.itemText = parameterName
}
} }
override fun handleInsert(context: InsertionContext) { override fun handleInsert(context: InsertionContext) {
@@ -227,18 +235,26 @@ class ParameterNameAndTypeCompletion(
val settings = CodeStyleSettingsManager.getInstance(context.project).currentSettings.getCustomSettings(KotlinCodeStyleSettings::class.java) val settings = CodeStyleSettingsManager.getInstance(context.project).currentSettings.getCustomSettings(KotlinCodeStyleSettings::class.java)
val spaceBefore = if (settings.SPACE_BEFORE_TYPE_COLON) " " else "" val spaceBefore = if (settings.SPACE_BEFORE_TYPE_COLON) " " else ""
val spaceAfter = if (settings.SPACE_AFTER_TYPE_COLON) " " else "" val spaceAfter = if (settings.SPACE_AFTER_TYPE_COLON) " " else ""
val text = parameterName + spaceBefore + ":" + spaceAfter
val startOffset = context.startOffset val startOffset = context.startOffset
context.document.insertString(startOffset, text) if (shouldInsertType) {
val text = parameterName + spaceBefore + ":" + spaceAfter
context.document.insertString(startOffset, text)
// update start offset so that it does not include the text we inserted // update start offset so that it does not include the text we inserted
context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, startOffset + text.length) context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, startOffset + text.length)
super.handleInsert(context) super.handleInsert(context)
}
else {
context.document.replaceString(startOffset, context.tailOffset, parameterName)
context.commitDocument()
}
} }
override fun equals(other: Any?) override fun equals(other: Any?)
= other is MyLookupElement && parameterName == other.parameterName && type == other.type = other is MyLookupElement && parameterName == other.parameterName && type == other.type && shouldInsertType == other.shouldInsertType
override fun hashCode() = parameterName.hashCode() override fun hashCode() = parameterName.hashCode()
} }
@@ -0,0 +1,6 @@
class Foo
lateinit var f<caret>
// EXIST: { itemText: "foo: Foo" }
@@ -0,0 +1,8 @@
class Foo
fun test() {
var f<caret>
}
// EXIST: { itemText: "foo", tailText: ": Foo (<root>)" }
@@ -0,0 +1,6 @@
class Foo
val f<caret>
// EXIST: { itemText: "foo", tailText: ": Foo (<root>)" }
@@ -2789,6 +2789,33 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
} }
} }
@TestMetadata("idea/idea-completion/testData/basic/common/variableNameAndType")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class VariableNameAndType extends AbstractJSBasicCompletionTest {
public void testAllFilesPresentInVariableNameAndType() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/variableNameAndType"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("Lateinit.kt")
public void testLateinit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/Lateinit.kt");
doTest(fileName);
}
@TestMetadata("Local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/Local.kt");
doTest(fileName);
}
@TestMetadata("TopLevel.kt")
public void testTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/TopLevel.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/visibility") @TestMetadata("idea/idea-completion/testData/basic/common/visibility")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)
@@ -2789,6 +2789,33 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
} }
} }
@TestMetadata("idea/idea-completion/testData/basic/common/variableNameAndType")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class VariableNameAndType extends AbstractJvmBasicCompletionTest {
public void testAllFilesPresentInVariableNameAndType() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/variableNameAndType"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("Lateinit.kt")
public void testLateinit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/Lateinit.kt");
doTest(fileName);
}
@TestMetadata("Local.kt")
public void testLocal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/Local.kt");
doTest(fileName);
}
@TestMetadata("TopLevel.kt")
public void testTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/variableNameAndType/TopLevel.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/visibility") @TestMetadata("idea/idea-completion/testData/basic/common/visibility")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)