PY-44026 Add completion for empty attribute prefix to PyUnresolvedModuleAttributeCompletionContributor

GitOrigin-RevId: 3354826398aa85adc54742155da90138cd9a2279
This commit is contained in:
Lada Gagina
2021-02-17 15:56:27 +00:00
committed by intellij-monorepo-bot
parent 8e05b0d7b6
commit 383dc15c13
3 changed files with 93 additions and 140 deletions
@@ -110,8 +110,6 @@
implementationClass="com.jetbrains.python.codeInsight.completion.PyModulePackageCompletionContributor"/>
<completion.contributor language="Python"
implementationClass="com.jetbrains.python.codeInsight.completion.PyUnresolvedModuleAttributeCompletionContributor"/>
<completion.contributor language="Python"
implementationClass="com.jetbrains.python.codeInsight.completion.PyUnresolvedModuleTransitiveAttributeCompletionContributor"/>
<completion.contributor language="Python"
implementationClass="com.jetbrains.python.codeInsight.completion.PyFStringLikeCompletionContributor"/>
<completion.contributor language="Python"
@@ -1,98 +1,116 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.codeInsight.completion
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Key
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.QualifiedName
import com.intellij.util.ProcessingContext
import com.intellij.util.Processor
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.codeInsight.imports.AddImportHelper
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.resolve.PyResolveUtil
import com.jetbrains.python.psi.search.PySearchUtilBase
import com.jetbrains.python.psi.stubs.PyModuleNameIndex
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher.QualifiedNameMatcher
/**
* Adds completion variants for unresolved module after dot.
*/
class PyUnresolvedModuleAttributeCompletionContributor : CompletionContributor() {
/**
* Checks whether completion should be performed for a given [parameters] and delegates actual work to [doFillCompletionVariants].
*/
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
if (!shouldDoCompletion(parameters)) return
doFillCompletionVariants(parameters, result)
}
fun doFillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
private companion object {
val QUALIFIED_REFERENCE_EXPRESSION = psiElement(PyTokenTypes.IDENTIFIER).withParent(
psiElement(PyReferenceExpression::class.java)
.andNot(psiElement().inside(PyImportStatementBase::class.java))
.with(object : PatternCondition<PyReferenceExpression>("plain qualified name") {
override fun accepts(expression: PyReferenceExpression, context: ProcessingContext): Boolean {
if (!expression.isQualified) return false
val qualifiedName = expression.asQualifiedName() ?: return false
context.put(REFERENCE_QUALIFIER, qualifiedName.removeLastComponent())
return true
}
})
)
val REFERENCE_QUALIFIER: Key<QualifiedName> = Key.create("QUALIFIER")
val element = parameters.position
val parent = element.parent
if (parent !is PyReferenceExpression || !parent.isQualified) {
return
private val importingInsertHandler: InsertHandler<LookupElement> = InsertHandler { context, item ->
addImportForLookupElement(context, item, context.tailOffset - 1)
}
val qualifier = parent.qualifier!!.name ?: return
val prefixMatcher = PlainPrefixMatcher(qualifier)
val targetFile = parameters.originalFile
val moduleKeys = PyModuleNameIndex.getAllKeys(targetFile.project)
val modulesFromIndex = moduleKeys.asSequence()
.filter { prefixMatcher.prefixMatches(it) }
.flatMap { PyModuleNameIndex.find(it, targetFile.project, true).asSequence() }
.toList()
val builders = modulesFromIndex.asSequence()
.filter { PyUtil.isImportable(targetFile, it) }
.flatMap { it.iterateNames().asSequence() }
.filter { it !is PsiFileSystemItem && it.name != null }
.mapNotNull {
val lookupString = if (it is PyQualifiedNameOwner && it.qualifiedName != null) it.qualifiedName!! else it.name!!
LookupElementBuilder.create(it, lookupString)
.withIcon(it.getIcon(0))
private fun addImportForLookupElement(context: InsertionContext, item: LookupElement, tailOffset: Int) {
val manager = PsiDocumentManager.getInstance(context.project)
val document = manager.getDocument(context.file)
if (document != null) {
manager.commitDocument(document)
}
.map { it.withInsertHandler(importingInsertHandler) }
val newResultSet = result.withPrefixMatcher(PlainPrefixMatcher("$qualifier."))
builders.forEach { newResultSet.addElement(it) }
}
private val importingInsertHandler: InsertHandler<LookupElement> = InsertHandler { context, item ->
addImportForLookupElement(context, item, context.tailOffset - 1)
}
private fun addImportForLookupElement(context: InsertionContext, item: LookupElement, tailOffset: Int) {
val manager = PsiDocumentManager.getInstance(context.project)
val document = manager.getDocument(context.file)
if (document != null) {
manager.commitDocument(document)
}
val ref = context.file.findReferenceAt(tailOffset)
if (ref == null || ref.resolve() === item.psiElement) {
// no import statement needed
return
}
WriteCommandAction.writeCommandAction(context.project, context.file).run<RuntimeException> {
val psiElement = item.psiElement
if (psiElement is PsiNamedElement && psiElement.containingFile != null) {
val fileName = psiElement.containingFile.name
val elementNameQualifier = if (psiElement is PyQualifiedNameOwner) psiElement.qualifiedName?.substringBeforeLast('.') else null
val nameToImport = elementNameQualifier ?: fileName.substringBefore(".py")
AddImportHelper.addImportStatement(context.file, nameToImport, null, null, ref.element as PyElement)
val ref = context.file.findReferenceAt(tailOffset)
if (ref == null || ref.resolve() === item.psiElement) {
// no import statement needed
return
}
WriteCommandAction.writeCommandAction(context.project, context.file).run<RuntimeException> {
val psiElement = item.psiElement
if (psiElement is PsiNamedElement && psiElement.containingFile != null) {
val fileName = psiElement.containingFile.name
val elementNameQualifier = if (psiElement is PyQualifiedNameOwner) psiElement.qualifiedName?.substringBeforeLast('.') else null
val nameToImport = elementNameQualifier ?: fileName.substringBefore(".py")
AddImportHelper.addImportStatement(context.file, nameToImport, null, null, ref.element as PyElement)
}
}
}
private fun filterAttributes(it: PyElement, qualifier: String): Boolean {
val qualifiedName = (it as? PyQualifiedNameOwner)?.qualifiedName ?: return false
return qualifiedName.startsWith(qualifier) && !qualifiedName.substringAfter(qualifier).startsWith("._")
}
}
private fun shouldDoCompletion(parameters: CompletionParameters): Boolean {
val element = parameters.position
val parent = element.parent
if (parent is PyReferenceExpression && parent.isQualified && PsiTreeUtil.getParentOfType(element,
PyImportStatementBase::class.java) == null) {
return PyResolveUtil.resolveLocally(parent).isEmpty()
}
return false
init {
extend(CompletionType.BASIC, QUALIFIED_REFERENCE_EXPRESSION, object : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val originalReferenceExpr = parameters.originalPosition?.parent as? PyReferenceExpression
// It cannot be checked in the pattern, because the default placeholder splits the reference, e.g. "foo.ba<caret>IntellijIdeaRulezzz z".
val isOtherReferenceQualifier = originalReferenceExpr?.parent is PyReferenceExpression
if (isOtherReferenceQualifier) return
val project = parameters.position.project
val attribute = result.prefixMatcher.prefix
val qualifier = context.get(REFERENCE_QUALIFIER).toString()
if (attribute.isEmpty()) {
val builders = PyModuleNameIndex.find(qualifier, project, true).asSequence()
.filter { PyUtil.isImportable(parameters.originalFile, it) }
.flatMap { it.iterateNames().asSequence() }
.filter { filterAttributes(it, qualifier) }
.mapNotNull {
val lookupString = if (it is PyQualifiedNameOwner && it.qualifiedName != null) it.qualifiedName!! else it.name!!
LookupElementBuilder.create(it, lookupString)
.withIcon(it.getIcon(0))
.withInsertHandler(importingInsertHandler)
}
val newResultSet = result.withPrefixMatcher(PlainPrefixMatcher("$qualifier."))
builders.forEach { newResultSet.addElement(it) }
result.restartCompletionOnAnyPrefixChange()
return
}
val scope = PySearchUtilBase.excludeSdkTestsScope(project)
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifier, attribute))
PyQualifiedNameCompletionMatcher.processMatchingExportedNames(qualifier, attribute, parameters.originalFile, scope, Processor {
ProgressManager.checkCanceled()
resultMatchingCompleteReference.addElement(LookupElementBuilder
.createWithSmartPointer(it.qualifiedName.toString(), it.element)
.withIcon(it.element.getIcon(0))
.withInsertHandler(importingInsertHandler))
return@Processor true
})
}
})
}
}
}
@@ -1,63 +0,0 @@
package com.jetbrains.python.codeInsight.completion
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Key
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.psi.util.QualifiedName
import com.intellij.util.ProcessingContext
import com.intellij.util.Processor
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.psi.PyImportStatementBase
import com.jetbrains.python.psi.PyReferenceExpression
import com.jetbrains.python.psi.search.PySearchUtilBase
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher
import com.jetbrains.python.psi.stubs.PyQualifiedNameCompletionMatcher.QualifiedNameMatcher
class PyUnresolvedModuleTransitiveAttributeCompletionContributor : CompletionContributor() {
private companion object {
val QUALIFIED_REFERENCE_EXPRESSION = psiElement(PyTokenTypes.IDENTIFIER).withParent(
psiElement(PyReferenceExpression::class.java)
.andNot(psiElement().inside(PyImportStatementBase::class.java))
.with(object : PatternCondition<PyReferenceExpression>("plain qualified name") {
override fun accepts(expression: PyReferenceExpression, context: ProcessingContext): Boolean {
if (!expression.isQualified) return false
val qualifiedName = expression.asQualifiedName() ?: return false
context.put(REFERENCE_QUALIFIER, qualifiedName.removeLastComponent())
return true
}
})
)
val REFERENCE_QUALIFIER: Key<QualifiedName> = Key.create("QUALIFIER")
}
init {
extend(CompletionType.BASIC, QUALIFIED_REFERENCE_EXPRESSION, object : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val originalReferenceExpr = parameters.originalPosition?.parent as? PyReferenceExpression
// It cannot be checked in the pattern, because the default placeholder splits the reference, e.g. "foo.ba<caret>IntellijIdeaRulezzz z".
val isOtherReferenceQualifier = originalReferenceExpr?.parent is PyReferenceExpression
if (isOtherReferenceQualifier) return
val attribute = result.prefixMatcher.prefix
val qualifier = context.get(REFERENCE_QUALIFIER).toString()
if (attribute.isEmpty()) {
result.restartCompletionOnAnyPrefixChange()
return
}
val scope = PySearchUtilBase.excludeSdkTestsScope(parameters.position.project)
val resultMatchingCompleteReference = result.withPrefixMatcher(QualifiedNameMatcher(qualifier, attribute))
PyQualifiedNameCompletionMatcher.processMatchingExportedNames(qualifier, attribute, parameters.originalFile, scope, Processor {
ProgressManager.checkCanceled()
resultMatchingCompleteReference.addElement(LookupElementBuilder
.createWithSmartPointer(it.qualifiedName.toString(), it.element)
.withIcon(it.element.getIcon(0)))
return@Processor true
})
}
})
}
}