Smart completion: more correct Tab key handling

#KT-4954 Fixed
This commit is contained in:
Valentin Kipyatkov
2014-04-25 22:17:13 +04:00
parent 5f931be39b
commit 9ae59db329
22 changed files with 248 additions and 43 deletions
@@ -15,7 +15,17 @@
name='com.intellij.codeInsight.completion.InsertHandler void handleInsert(com.intellij.codeInsight.completion.InsertionContext, T) 1'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.codeInsight.completion.InsertionContext com.intellij.codeInsight.completion.OffsetMap getOffsetMap()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.codeInsight.completion.InsertionContext com.intellij.openapi.project.Project getProject()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.codeInsight.completion.OffsetKey com.intellij.codeInsight.completion.OffsetKey create(java.lang.String)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item
name='com.intellij.codeInsight.completion.OffsetKey com.intellij.codeInsight.completion.OffsetKey create(java.lang.String, boolean)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -20,19 +20,20 @@ import com.intellij.codeInsight.completion.*;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.JetNodeTypes;
import org.jetbrains.jet.lang.psi.*;
import org.jetbrains.jet.lang.psi.JetBlockExpression;
import org.jetbrains.jet.lang.psi.JetCallExpression;
import org.jetbrains.jet.lang.psi.JetExpression;
import org.jetbrains.jet.lang.psi.JetFile;
import org.jetbrains.jet.plugin.completion.smart.SmartCompletion;
import org.jetbrains.jet.plugin.references.JetSimpleNameReference;
public class JetCompletionContributor extends CompletionContributor {
public JetCompletionContributor() {
final CompletionProvider<CompletionParameters> provider = new CompletionProvider<CompletionParameters>() {
CompletionProvider<CompletionParameters> provider = new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(
@NotNull CompletionParameters parameters,
@@ -125,7 +126,7 @@ public class JetCompletionContributor extends CompletionContributor {
PsiElement tokenAt = context.getFile().findElementAt(Math.max(0, offset));
if (tokenAt != null) {
PsiElement parent = tokenAt.getParent();
if (parent instanceof JetExpression) {
if (parent instanceof JetExpression && !(parent instanceof JetBlockExpression)) {
// search expression to be replaced - go up while we are the first child of parent expression
JetExpression expression = (JetExpression) parent;
parent = expression.getParent();
@@ -133,7 +134,16 @@ public class JetCompletionContributor extends CompletionContributor {
expression = (JetExpression) parent;
parent = expression.getParent();
}
context.setReplacementOffset(expression.getTextRange().getEndOffset());
int expressionEnd = expression.getTextRange().getEndOffset();
if (expression instanceof JetCallExpression) {
JetExpression calleeExpression = ((JetCallExpression) expression).getCalleeExpression();
context.setReplacementOffset(calleeExpression != null ? calleeExpression.getTextRange().getEndOffset() : expressionEnd);
}
else{
context.setReplacementOffset(expressionEnd);
}
context.getOffsetMap().addOffset(SmartCompletion.OLD_ARGUMENTS_REPLACEMENT_OFFSET, expressionEnd);
}
}
}
@@ -18,43 +18,27 @@ package org.jetbrains.jet.plugin.completion.handlers
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.editor.event.*
import com.intellij.psi.PsiDocumentManager
import com.intellij.codeInsight.AutoPopupController
import com.intellij.codeInsight.lookup.Lookup
import org.jetbrains.jet.plugin.completion.smart.SmartCompletion
import org.jetbrains.jet.plugin.completion.smart.KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY
abstract class WithTailInsertHandlerBase : InsertHandler<LookupElement> {
protected abstract fun insertTail(context: InsertionContext, offset: Int, moveCaret: Boolean)
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val document = context.getDocument()
val caretModel = context.getEditor().getCaretModel()
var maxChangeOffset = caretModel.getOffset()
val documentListener = object: DocumentListener {
override fun documentChanged(event: DocumentEvent) {
val oldEndOffset = event.getOffset() + event.getOldLength()
if (oldEndOffset < maxChangeOffset) {
maxChangeOffset += event.getNewLength() - event.getOldLength()
}
else {
maxChangeOffset = event.getOffset() + event.getNewLength()
}
}
item.handleInsert(context)
PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document)
override fun beforeDocumentChange(event: DocumentEvent) {
}
var tailOffset = context.getTailOffset()
if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR && item.getUserData(KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY) != null) {
val offset = context.getOffsetMap().getOffset(SmartCompletion.OLD_ARGUMENTS_REPLACEMENT_OFFSET)
if (offset != -1) tailOffset = offset
}
document.addDocumentListener(documentListener)
try{
item.handleInsert(context)
PsiDocumentManager.getInstance(context.getProject()).doPostponedOperationsAndUnblockDocument(document)
}
finally {
document.removeDocumentListener(documentListener)
}
insertTail(context, maxChangeOffset, caretModel.getOffset() == maxChangeOffset)
insertTail(context, tailOffset, context.getEditor().getCaretModel().getOffset() == tailOffset)
}
}
@@ -37,6 +37,28 @@ class SmartCompletion(val expression: JetSimpleNameExpression,
private val project = expression.getProject()
public fun buildLookupElements(referenceVariants: Iterable<DeclarationDescriptor>): Collection<LookupElement>? {
return buildLookupElementsInternal(referenceVariants)?.map {
if (it.getUserData(KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY) == null) {
object : LookupElementDecorator<LookupElement>(it) {
override fun handleInsert(context: InsertionContext) {
if (context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR) {
val offset = context.getOffsetMap().getOffset(SmartCompletion.OLD_ARGUMENTS_REPLACEMENT_OFFSET)
if (offset != -1) {
context.getDocument().deleteString(context.getTailOffset(), offset)
}
}
super.handleInsert(context)
}
}
}
else {
it
}
}
}
private fun buildLookupElementsInternal(referenceVariants: Iterable<DeclarationDescriptor>): Collection<LookupElement>? {
val parent = expression.getParent()
val expressionWithType: JetExpression
val receiver: JetExpression?
@@ -73,7 +95,7 @@ class SmartCompletion(val expression: JetSimpleNameExpression,
else -> ExpectedInfoClassification.NOT_MATCHES
}
}
result.addLookupElements(expectedInfos, classifier, { DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, descriptor) })
result.addLookupElements(expectedInfos, classifier, { createLookupElement(descriptor, resolveSession, bindingContext) })
if (receiver == null) {
toFunctionReferenceLookupElement(descriptor, functionExpectedInfos)?.let { result.add(it) }
@@ -129,7 +151,7 @@ class SmartCompletion(val expression: JetSimpleNameExpression,
val matchedExpectedInfos = functionExpectedInfos.filter { functionType.isSubtypeOf(it.`type`) }
if (matchedExpectedInfos.isEmpty()) return null
var lookupElement = DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, descriptor)
var lookupElement = createLookupElement(descriptor, resolveSession, bindingContext)
val text = "::" + (if (descriptor is ConstructorDescriptor) descriptor.getContainingDeclaration().getName() else descriptor.getName())
lookupElement = object: LookupElementDecorator<LookupElement>(lookupElement) {
override fun getLookupString() = text
@@ -160,4 +182,8 @@ class SmartCompletion(val expression: JetSimpleNameExpression,
return null
}
class object {
public val OLD_ARGUMENTS_REPLACEMENT_OFFSET: OffsetKey = OffsetKey.create("nonFunctionReplacementOffset")
}
}
@@ -104,7 +104,7 @@ class StaticMembers(val bindingContext: BindingContext, val resolveSession: Reso
}
private fun createLookupElement(memberDescriptor: DeclarationDescriptor, classDescriptor: ClassDescriptor): LookupElement {
val lookupElement = DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, memberDescriptor)
val lookupElement = createLookupElement(memberDescriptor, resolveSession, bindingContext)
val qualifierPresentation = classDescriptor.getName().asString()
val lookupString = qualifierPresentation + "." + lookupElement.getLookupString()
val qualifierText = DescriptorUtils.getFqName(classDescriptor).asString() //TODO: escape keywords
@@ -21,14 +21,12 @@ import org.jetbrains.jet.lang.types.JetType
import org.jetbrains.jet.lang.types.TypeUtils
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns
import org.jetbrains.jet.lang.descriptors.ClassDescriptor
import org.jetbrains.jet.plugin.completion.DescriptorLookupConverter
import org.jetbrains.jet.renderer.DescriptorRenderer
import com.intellij.codeInsight.completion.InsertHandler
import org.jetbrains.jet.lang.resolve.DescriptorUtils
import org.jetbrains.jet.lang.descriptors.Modality
import org.jetbrains.jet.lang.descriptors.ClassKind
import org.jetbrains.jet.plugin.codeInsight.ImplementMethodsHandler
import org.jetbrains.jet.lang.descriptors.ConstructorDescriptor
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.completion.InsertionContext
@@ -36,6 +34,7 @@ import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.plugin.project.ResolveSessionForBodies
import org.jetbrains.jet.plugin.completion.handlers.JetFunctionInsertHandler
import org.jetbrains.jet.plugin.completion.*
import org.jetbrains.jet.plugin.completion.handlers.CaretPosition
class TypeInstantiationItems(val bindingContext: BindingContext, val resolveSession: ResolveSessionForBodies) {
public fun addToCollection(collection: MutableCollection<LookupElement>, expectedInfos: Collection<ExpectedInfo>) {
@@ -53,7 +52,7 @@ class TypeInstantiationItems(val bindingContext: BindingContext, val resolveSess
if (!(classifier is ClassDescriptor)) return
//TODO: check for constructor's visibility
var lookupElement = DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, classifier)
var lookupElement = createLookupElement(classifier, resolveSession, bindingContext)
var lookupString = lookupElement.getLookupString()
@@ -85,12 +84,12 @@ class TypeInstantiationItems(val bindingContext: BindingContext, val resolveSess
itemText += "()"
val constructors = classifier.getConstructors()
val baseInsertHandler =
if (constructors.size == 0)
(if (constructors.size == 0)
JetFunctionInsertHandler.NO_PARAMETERS_HANDLER
else if (constructors.size == 1)
DescriptorLookupConverter.getDefaultInsertHandler(constructors.first())!!
else
JetFunctionInsertHandler.WITH_PARAMETERS_HANDLER
JetFunctionInsertHandler.WITH_PARAMETERS_HANDLER) as JetFunctionInsertHandler
insertHandler = object : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
context.getDocument().replaceString(context.getStartOffset(), context.getTailOffset(), typeText)
@@ -101,7 +100,10 @@ class TypeInstantiationItems(val bindingContext: BindingContext, val resolveSess
shortenReferences(context, context.getStartOffset(), context.getTailOffset())
}
}
if ((baseInsertHandler as JetFunctionInsertHandler).lambdaInfo != null) {
if (baseInsertHandler.caretPosition == CaretPosition.IN_BRACKETS) {
lookupElement = lookupElement.keepOldArgumentListOnTab()
}
if (baseInsertHandler.lambdaInfo != null) {
lookupElement.putUserData(JetCompletionCharFilter.ACCEPT_OPENING_BRACE, true)
}
}
@@ -31,9 +31,13 @@ import org.jetbrains.jet.lang.types.JetType
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns
import org.jetbrains.jet.lang.types.checker.JetTypeChecker
import com.intellij.codeInsight.lookup.LookupElementPresentation
import org.jetbrains.jet.plugin.completion.handlers.WithTailStringInsertHandler
import java.util.ArrayList
import org.jetbrains.jet.plugin.completion.*
import com.intellij.openapi.util.Key
import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor
import org.jetbrains.jet.plugin.project.ResolveSessionForBodies
import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.plugin.completion.handlers.WithTailStringInsertHandler
class ArtificialElementInsertHandler(
val textBeforeCaret: String, val textAfterCaret: String, val shortenRefs: Boolean) : InsertHandler<LookupElement>{
@@ -83,6 +87,13 @@ fun LookupElement.addTail(matchedExpectedInfos: Collection<ExpectedInfo>): Looku
fun LookupElement.suppressAutoInsertion() = AutoCompletionPolicy.NEVER_AUTOCOMPLETE.applyPolicy(this)
val KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY = Key<Unit>("KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY")
fun LookupElement.keepOldArgumentListOnTab(): LookupElement {
putUserData(KEEP_OLD_ARGUMENT_LIST_ON_TAB_KEY, Unit.VALUE)
return this
}
enum class ExpectedInfoClassification {
MATCHES
MAKE_NOT_NULLABLE
@@ -151,6 +162,11 @@ fun functionType(function: FunctionDescriptor): JetType? {
function.getReturnType() ?: return null)
}
fun createLookupElement(descriptor: DeclarationDescriptor, resolveSession: ResolveSessionForBodies, bindingContext: BindingContext): LookupElement {
val element = DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, descriptor)
return if (descriptor is FunctionDescriptor && descriptor.getValueParameters().isNotEmpty()) element.keepOldArgumentListOnTab() else element
}
fun JetType.isSubtypeOf(expectedType: JetType) = !isError() && JetTypeChecker.INSTANCE.isSubtypeOf(this, expectedType)
fun <T : Any> T?.toList(): List<T> = if (this != null) listOf(this) else listOf()
@@ -0,0 +1,8 @@
package sample
fun foo(s: String){
val s1: String = <caret>
}
//ELEMENT: s
//CHAR: \t
@@ -0,0 +1,8 @@
package sample
fun foo(s: String){
val s1: String = s<caret>
}
//ELEMENT: s
//CHAR: \t
@@ -0,0 +1,7 @@
fun foo(p: Int): Int = p
fun f(p: Int): Int {
return <caret>foo(1)
}
//ELEMENT: p
@@ -0,0 +1,7 @@
fun foo(p: Int): Int = p
fun f(p: Int): Int {
return p<caret>foo(1)
}
//ELEMENT: p
@@ -0,0 +1,10 @@
fun foo(s: String, c: Char){ }
fun takeString(p: Int): String? = null
fun bar() {
foo(<caret>qqq(1, 2, 3))
}
// ELEMENT_TEXT: "!! takeString"
// CHAR: \t
@@ -0,0 +1,10 @@
fun foo(s: String, c: Char){ }
fun takeString(p: Int): String? = null
fun bar() {
foo(takeString(<caret>1, 2, 3)!!, )
}
// ELEMENT_TEXT: "!! takeString"
// CHAR: \t
@@ -0,0 +1,8 @@
fun foo(s: String){ }
fun bar(sss: String) {
foo(<caret>x())
}
//ELEMENT: sss
//CHAR: \t
@@ -0,0 +1,8 @@
fun foo(s: String){ }
fun bar(sss: String) {
foo(sss)<caret>
}
//ELEMENT: sss
//CHAR: \t
@@ -0,0 +1,9 @@
fun foo(p: Int): Int = p
fun bar(p: Int): Int = p
fun f(): Int {
return <caret>foo(1)
}
//ELEMENT: bar
//CHAR: \t
@@ -0,0 +1,9 @@
fun foo(p: Int): Int = p
fun bar(p: Int): Int = p
fun f(): Int {
return bar(<caret>1)
}
//ELEMENT: bar
//CHAR: \t
@@ -0,0 +1,9 @@
fun foo(p: Int): Int = p
fun bar(): Int = p
fun f(): Int {
return <caret>foo(1)
}
//ELEMENT: bar
//CHAR: \t
@@ -0,0 +1,9 @@
fun foo(p: Int): Int = p
fun bar(): Int = p
fun f(): Int {
return bar()<caret>
}
//ELEMENT: bar
//CHAR: \t
@@ -0,0 +1,10 @@
fun foo(s: String, i: Int){ }
fun takeString(p: Int): String = ""
fun bar() {
foo(<caret>x(1))
}
//ELEMENT: takeString
//CHAR: \t
@@ -0,0 +1,10 @@
fun foo(s: String, i: Int){ }
fun takeString(p: Int): String = ""
fun bar() {
foo(takeString(<caret>1), )
}
//ELEMENT: takeString
//CHAR: \t
@@ -176,6 +176,16 @@ public class SmartCompletionHandlerTestGenerated extends AbstractSmartCompletion
doTest("idea/testData/completion/handlers/smart/ConstructorWithParameters.kt");
}
@TestMetadata("DoNotEraseBraceOnTab.kt")
public void testDoNotEraseBraceOnTab() throws Exception {
doTest("idea/testData/completion/handlers/smart/DoNotEraseBraceOnTab.kt");
}
@TestMetadata("DoNotReplaceOnEnter.kt")
public void testDoNotReplaceOnEnter() throws Exception {
doTest("idea/testData/completion/handlers/smart/DoNotReplaceOnEnter.kt");
}
@TestMetadata("EnumMember.kt")
public void testEnumMember() throws Exception {
doTest("idea/testData/completion/handlers/smart/EnumMember.kt");
@@ -286,6 +296,11 @@ public class SmartCompletionHandlerTestGenerated extends AbstractSmartCompletion
doTest("idea/testData/completion/handlers/smart/NullableValue3.kt");
}
@TestMetadata("NullableValueKeepOldArguments.kt")
public void testNullableValueKeepOldArguments() throws Exception {
doTest("idea/testData/completion/handlers/smart/NullableValueKeepOldArguments.kt");
}
@TestMetadata("TabReplaceComma1.kt")
public void testTabReplaceComma1() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceComma1.kt");
@@ -311,6 +326,26 @@ public class SmartCompletionHandlerTestGenerated extends AbstractSmartCompletion
doTest("idea/testData/completion/handlers/smart/TabReplaceExpression3.kt");
}
@TestMetadata("TabReplaceExpression4.kt")
public void testTabReplaceExpression4() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceExpression4.kt");
}
@TestMetadata("TabReplaceFunctionName1.kt")
public void testTabReplaceFunctionName1() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceFunctionName1.kt");
}
@TestMetadata("TabReplaceFunctionName2.kt")
public void testTabReplaceFunctionName2() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceFunctionName2.kt");
}
@TestMetadata("TabReplaceFunctionName3.kt")
public void testTabReplaceFunctionName3() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceFunctionName3.kt");
}
@TestMetadata("TabReplaceIdentifier.kt")
public void testTabReplaceIdentifier() throws Exception {
doTest("idea/testData/completion/handlers/smart/TabReplaceIdentifier.kt");