Code completion for string templates after simple name + dot

This commit is contained in:
Valentin Kipyatkov
2015-11-09 18:45:41 +03:00
parent 8389b8e8da
commit 8946e5f353
18 changed files with 248 additions and 62 deletions
@@ -54,6 +54,7 @@ import java.util.*
class BasicCompletionSession(
configuration: CompletionSessionConfiguration,
parameters: CompletionParameters,
toFromOriginalFileMapper: ToFromOriginalFileMapper,
resultSet: CompletionResultSet
) : CompletionSession(configuration, parameters, resultSet) {
@@ -21,6 +21,7 @@ import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionSorter
import com.intellij.codeInsight.completion.CompletionUtil
import com.intellij.codeInsight.completion.impl.CamelHumpMatcher
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.patterns.StandardPatterns
import com.intellij.psi.search.DelegatingGlobalSearchScope
@@ -161,9 +162,6 @@ abstract class CompletionSession(
return KotlinIndicesHelper(resolutionFacade, searchScope, filter, filterOutPrivate = !mayIncludeInaccessible)
}
protected val toFromOriginalFileMapper: ToFromOriginalFileMapper
= ToFromOriginalFileMapper(parameters.originalFile as KtFile, position.containingFile as KtFile, parameters.offset)
// excludes top-level extensions except for ones declared in the current file - those that are fetched from indices
protected val topLevelExtensionsExclude = object : DescriptorKindExclude() {
val extensionsFromThisFile = file.declarations
@@ -242,6 +240,10 @@ abstract class CompletionSession(
return !collector.isResultEmpty
}
public fun addLookupElementPostProcessor(processor: (LookupElement) -> LookupElement) {
collector.addLookupElementPostProcessor(processor)
}
protected abstract fun doComplete()
protected abstract val descriptorKindFilter: DescriptorKindFilter?
@@ -17,6 +17,8 @@
package org.jetbrains.kotlin.idea.completion
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.openapi.editor.Document
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.util.Key
@@ -51,6 +53,8 @@ public class KotlinCompletionContributor : CompletionContributor() {
companion object {
public val DEFAULT_DUMMY_IDENTIFIER: String = CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + "$" // add '$' to ignore context after the caret
private val STRING_TEMPLATE_AFTER_DOT_REAL_START_OFFSET = OffsetKey.create("STRING_TEMPLATE_AFTER_DOT_REAL_START_OFFSET")
}
init {
@@ -67,10 +71,27 @@ public class KotlinCompletionContributor : CompletionContributor() {
val psiFile = context.getFile()
if (psiFile !is KtFile) return
// this code will make replacement offset "modified" and prevents altering it by the code in CompletionProgressIndicator
context.replacementOffset = context.replacementOffset
val offset = context.getStartOffset()
val tokenBefore = psiFile.findElementAt(Math.max(0, offset - 1))
val dummyIdentifier = when {
if (offset > 0 && tokenBefore!!.node.elementType == KtTokens.REGULAR_STRING_PART && tokenBefore.text.startsWith(".")) {
val prev = tokenBefore.parent.prevSibling
if (prev != null && prev is KtSimpleNameStringTemplateEntry) {
val expression = prev.expression
if (expression != null) {
val prefix = tokenBefore.text.substring(0, offset - tokenBefore.startOffset)
context.dummyIdentifier = "{" + expression.text + prefix + CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED + "}"
context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, expression.startOffset)
context.offsetMap.addOffset(STRING_TEMPLATE_AFTER_DOT_REAL_START_OFFSET, offset + 1)
return
}
}
}
context.dummyIdentifier = when {
context.getCompletionType() == CompletionType.SMART -> DEFAULT_DUMMY_IDENTIFIER
PackageDirectiveCompletion.ACTIVATION_PATTERN.accepts(tokenBefore) -> PackageDirectiveCompletion.DUMMY_IDENTIFIER
@@ -87,10 +108,6 @@ public class KotlinCompletionContributor : CompletionContributor() {
?: specialInArgumentListDummyIdentifier(tokenBefore)
?: DEFAULT_DUMMY_IDENTIFIER
}
context.setDummyIdentifier(dummyIdentifier)
// this code will make replacement offset "modified" and prevents altering it by the code in CompletionProgressIndicator
context.setReplacementOffset(context.getReplacementOffset())
if (context.getCompletionType() == CompletionType.SMART && !isAtEndOfLine(offset, context.getEditor().getDocument()) /* do not use parent expression if we are at the end of line - it's probably parsed incorrectly */) {
val tokenAt = psiFile.findElementAt(Math.max(0, offset))
@@ -216,17 +233,38 @@ public class KotlinCompletionContributor : CompletionContributor() {
}
private fun performCompletion(parameters: CompletionParameters, result: CompletionResultSet) {
val position = parameters.getPosition()
val positionFile = position.containingFile
if (positionFile !is KtFile) return
if ((positionFile.originalFile as KtFile).doNotComplete ?: false) return
val position = parameters.position
val positionFile = position.containingFile as? KtFile ?: return
val originalFile = parameters.originalFile as KtFile
if (originalFile.doNotComplete ?: false) return
val toFromOriginalFileMapper = ToFromOriginalFileMapper(originalFile, positionFile, parameters.offset)
if (position.node.elementType == KtTokens.LONG_TEMPLATE_ENTRY_START) {
val expression = (position.parent as KtBlockStringTemplateEntry).expression as KtDotQualifiedExpression
val correctedPosition = (expression.selectorExpression as KtNameReferenceExpression).firstChild
val context = position.getUserData(CompletionContext.COMPLETION_CONTEXT_KEY)!!
val correctedOffset = context.offsetMap.getOffset(STRING_TEMPLATE_AFTER_DOT_REAL_START_OFFSET)
val correctedParameters = parameters.withPosition(correctedPosition, correctedOffset)
performCompletionWithOutOfBlockTracking(position) {
doComplete(correctedParameters, toFromOriginalFileMapper, result,
lookupElementPostProcessor = { wrapLookupElementForStringTemplateAfterDotCompletion(it) })
}
return
}
performCompletionWithOutOfBlockTracking(position) {
doComplete(parameters, position, result)
doComplete(parameters, toFromOriginalFileMapper, result)
}
}
private fun doComplete(parameters: CompletionParameters, position: PsiElement, result: CompletionResultSet) {
private fun doComplete(
parameters: CompletionParameters,
toFromOriginalFileMapper: ToFromOriginalFileMapper,
result: CompletionResultSet,
lookupElementPostProcessor: ((LookupElement) -> LookupElement)? = null
) {
val position = parameters.position
if (position.getNonStrictParentOfType<PsiComment>() != null) {
// don't stop here, allow other contributors to run
return
@@ -244,12 +282,20 @@ public class KotlinCompletionContributor : CompletionContributor() {
if (PropertyKeyCompletion.perform(parameters, result)) return
fun addPostProcessor(session: CompletionSession) {
if (lookupElementPostProcessor != null) {
session.addLookupElementPostProcessor(lookupElementPostProcessor)
}
}
try {
result.restartCompletionWhenNothingMatches()
val configuration = CompletionSessionConfiguration(parameters)
if (parameters.getCompletionType() == CompletionType.BASIC) {
val session = BasicCompletionSession(configuration, parameters, result)
val session = BasicCompletionSession(configuration, parameters, toFromOriginalFileMapper, result)
addPostProcessor(session)
if (parameters.isAutoPopup() && session.shouldDisableAutoPopup()) {
result.stopHere()
@@ -266,11 +312,16 @@ public class KotlinCompletionContributor : CompletionContributor() {
completeJavaClassesNotToBeUsed = false,
completeStaticMembers = parameters.invocationCount > 0
)
BasicCompletionSession(newConfiguration, parameters, result).complete()
val newSession = BasicCompletionSession(newConfiguration, parameters, toFromOriginalFileMapper, result)
addPostProcessor(newSession)
newSession.complete()
}
}
else {
SmartCompletionSession(configuration, parameters, result).complete()
val session = SmartCompletionSession(configuration, parameters, toFromOriginalFileMapper, result)
addPostProcessor(session)
session.complete()
}
}
catch (e: ProcessCanceledException) {
@@ -278,6 +329,32 @@ public class KotlinCompletionContributor : CompletionContributor() {
}
}
private fun wrapLookupElementForStringTemplateAfterDotCompletion(lookupElement: LookupElement): LookupElement {
return object : LookupElementDecorator<LookupElement>(lookupElement) {
override fun handleInsert(context: InsertionContext) {
val document = context.document
val startOffset = context.startOffset
val psiDocumentManager = PsiDocumentManager.getInstance(context.project)
psiDocumentManager.commitAllDocuments()
assert(startOffset > 1 && document.charsSequence[startOffset - 1] == '.')
val token = context.file.findElementAt(startOffset - 2)!!
assert(token.node.elementType == KtTokens.IDENTIFIER)
val nameRef = token.parent as KtNameReferenceExpression
assert(nameRef.parent is KtSimpleNameStringTemplateEntry)
document.insertString(nameRef.startOffset, "{")
val tailOffset = context.tailOffset
document.insertString(tailOffset, "}")
context.tailOffset = tailOffset
super.handleInsert(context)
}
}
}
private fun shouldSuppressCompletion(parameters: CompletionParameters, prefixMatcher: PrefixMatcher): Boolean {
val position = parameters.getPosition()
val invocationCount = parameters.getInvocationCount()
@@ -399,20 +476,6 @@ public class KotlinCompletionContributor : CompletionContributor() {
return null
}
private fun countParenthesisBalance(at: PsiElement, container: PsiElement): Int {
val stopAt = container.prevLeaf()
var current: PsiElement? = at
var balance = 0
while (current != stopAt) {
when (current!!.getNode().getElementType()) {
KtTokens.LPAR -> balance++
KtTokens.RPAR -> balance--
}
current = current.prevLeaf()
}
return balance
}
private fun isInUnclosedSuperQualifier(tokenBefore: PsiElement?): Boolean {
if (tokenBefore == null) return false
val tokensToSkip = TokenSet.orSet(TokenSet.create(KtTokens.IDENTIFIER, KtTokens.DOT ), KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET)
@@ -35,8 +35,12 @@ import org.jetbrains.kotlin.resolve.calls.callUtil.getCall
import org.jetbrains.kotlin.resolve.calls.util.DelegatingCall
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
class SmartCompletionSession(configuration: CompletionSessionConfiguration, parameters: CompletionParameters, resultSet: CompletionResultSet)
: CompletionSession(configuration, parameters, resultSet) {
class SmartCompletionSession(
configuration: CompletionSessionConfiguration,
parameters: CompletionParameters,
toFromOriginalFileMapper: ToFromOriginalFileMapper,
resultSet: CompletionResultSet
) : CompletionSession(configuration, parameters, resultSet) {
override val descriptorKindFilter: DescriptorKindFilter by lazy {
// we do not include SAM-constructors because they are handled separately and adding them requires iterating of java classes
@@ -1,5 +1,5 @@
fun foo(param: String) {
val s = "$<caret>"
val s = "$<caret>bla-bla-bla"
}
// EXIST: foo
@@ -0,0 +1,7 @@
fun foo(param: String) {
val s = "$param.<caret>bla-bla-bla"
}
// EXIST: { itemText: "length", attributes: "" }
// EXIST: { itemText: "hashCode", attributes: "" }
// EXIST: { itemText: "capitalize", attributes: "bold" }
@@ -0,0 +1,7 @@
fun foo(param: String) {
val s = "$param.l<caret>bla-bla-bla"
}
// EXIST: { itemText: "length", attributes: "" }
// ABSENT: hashCode
// EXIST: { itemText: "lastIndex", attributes: "" }
@@ -0,0 +1,6 @@
fun foo(param: String) {
val s = "$param.<caret>bla-bla-bla"
}
// ELEMENT: equals
// TAIL_TEXT: "(other: Any?)"
@@ -0,0 +1,6 @@
fun foo(param: String) {
val s = "${param.equals(<caret>)}bla-bla-bla"
}
// ELEMENT: equals
// TAIL_TEXT: "(other: Any?)"
@@ -0,0 +1,5 @@
fun foo(param: String) {
val s = "$param.<caret>bla-bla-bla"
}
// ELEMENT: hashCode
@@ -0,0 +1,5 @@
fun foo(param: String) {
val s = "${param.hashCode()<caret>}bla-bla-bla"
}
// ELEMENT: hashCode
@@ -0,0 +1,6 @@
fun foo(param: String) {
val s = "$param.eq<caret>bla-bla-bla"
}
// ELEMENT: equals
// TAIL_TEXT: "(other: Any?)"
@@ -0,0 +1,6 @@
fun foo(param: String) {
val s = "${param.equals(<caret>)}bla-bla-bla"
}
// ELEMENT: equals
// TAIL_TEXT: "(other: Any?)"
@@ -673,18 +673,6 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
doTest(fileName);
}
@TestMetadata("StringTemplate1.kt")
public void testStringTemplate1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/StringTemplate1.kt");
doTest(fileName);
}
@TestMetadata("StringTemplate2.kt")
public void testStringTemplate2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/StringTemplate2.kt");
doTest(fileName);
}
@TestMetadata("SubpackageInFun.kt")
public void testSubpackageInFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/SubpackageInFun.kt");
@@ -1468,6 +1456,39 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/inStringLiterals")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class InStringLiterals extends AbstractJSBasicCompletionTest {
public void testAllFilesPresentInInStringLiterals() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/inStringLiterals"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("StringTemplate1.kt")
public void testStringTemplate1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplate1.kt");
doTest(fileName);
}
@TestMetadata("StringTemplate2.kt")
public void testStringTemplate2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplate2.kt");
doTest(fileName);
}
@TestMetadata("StringTemplateDot.kt")
public void testStringTemplateDot() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplateDot.kt");
doTest(fileName);
}
@TestMetadata("StringTemplateDotSomething.kt")
public void testStringTemplateDotSomething() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplateDotSomething.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/lambdaSignature")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -673,18 +673,6 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
doTest(fileName);
}
@TestMetadata("StringTemplate1.kt")
public void testStringTemplate1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/StringTemplate1.kt");
doTest(fileName);
}
@TestMetadata("StringTemplate2.kt")
public void testStringTemplate2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/StringTemplate2.kt");
doTest(fileName);
}
@TestMetadata("SubpackageInFun.kt")
public void testSubpackageInFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/SubpackageInFun.kt");
@@ -1468,6 +1456,39 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/inStringLiterals")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class InStringLiterals extends AbstractJvmBasicCompletionTest {
public void testAllFilesPresentInInStringLiterals() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/basic/common/inStringLiterals"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("StringTemplate1.kt")
public void testStringTemplate1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplate1.kt");
doTest(fileName);
}
@TestMetadata("StringTemplate2.kt")
public void testStringTemplate2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplate2.kt");
doTest(fileName);
}
@TestMetadata("StringTemplateDot.kt")
public void testStringTemplateDot() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplateDot.kt");
doTest(fileName);
}
@TestMetadata("StringTemplateDotSomething.kt")
public void testStringTemplateDotSomething() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/basic/common/inStringLiterals/StringTemplateDotSomething.kt");
doTest(fileName);
}
}
@TestMetadata("idea/idea-completion/testData/basic/common/lambdaSignature")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -591,6 +591,24 @@ public class BasicCompletionHandlerTestGenerated extends AbstractBasicCompletion
doTest(fileName);
}
@TestMetadata("AfterDot1.kt")
public void testAfterDot1() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/basic/stringTemplate/AfterDot1.kt");
doTest(fileName);
}
@TestMetadata("AfterDot2.kt")
public void testAfterDot2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/basic/stringTemplate/AfterDot2.kt");
doTest(fileName);
}
@TestMetadata("AfterDot3.kt")
public void testAfterDot3() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/handlers/basic/stringTemplate/AfterDot3.kt");
doTest(fileName);
}
public void testAllFilesPresentInStringTemplate() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/handlers/basic/stringTemplate"), Pattern.compile("^(.+)\\.kt$"), true);
}
@@ -47,6 +47,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.KtNodeTypes;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry;
public class KotlinTypedHandler extends TypedHandlerDelegate {
private final static TokenSet CONTROL_FLOW_EXPRESSIONS = TokenSet.create(
@@ -153,11 +154,18 @@ public class KotlinTypedHandler extends TypedHandlerDelegate {
public boolean value(PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement lastElement = file.findElementAt(offset - 1);
if (lastElement == null) return false;
PsiElement lastToken = file.findElementAt(offset - 1);
if (lastToken == null) return false;
IElementType elementType = lastElement.getNode().getElementType();
return elementType == KtTokens.DOT || elementType == KtTokens.SAFE_ACCESS;
IElementType elementType = lastToken.getNode().getElementType();
if (elementType == KtTokens.DOT || elementType == KtTokens.SAFE_ACCESS) return true;
if (elementType == KtTokens.REGULAR_STRING_PART && lastToken.getTextRange().getStartOffset() == offset - 1) {
PsiElement prevSibling = lastToken.getParent().getPrevSibling();
return prevSibling != null && prevSibling instanceof KtSimpleNameStringTemplateEntry;
}
return false;
}
});
}