Completion for fqNames in KDoc links

Tests, also proved that KT-14432 are fixed
Added completion and tests for extension functions.
This commit is contained in:
Simon Ogorodnik
2016-11-10 17:52:42 +03:00
parent 62077dafc3
commit 1a211ee7b0
9 changed files with 196 additions and 15 deletions
@@ -53,11 +53,16 @@ fun resolveKDocLink(context: BindingContext,
val descriptorsByName = scope.collectDescriptorsFiltered(nameFilter = { it.asString() == shortName })
// Try to find a matching local descriptor (parameter or type parameter) first.
val localDescriptors = descriptorsByName.filter { it.containingDeclaration == fromDescriptor }
if (localDescriptors.isNotEmpty()) return localDescriptors
if (descriptorsByName.isNotEmpty()) return descriptorsByName
val moduleDescriptor = fromDescriptor.module
return moduleDescriptor.getSubPackagesOf(FqName.ROOT, { it.asString() == shortName }).map { moduleDescriptor.getPackage(it) }
val packagesByName = moduleDescriptor.getSubPackagesOf(FqName.ROOT, { true })
.filter { it.asString() == shortName }
.map { moduleDescriptor.getPackage(it) }
return descriptorsByName + packagesByName
}
val moduleDescriptor = fromDescriptor.module
@@ -25,9 +25,11 @@ import com.intellij.patterns.StandardPatterns
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.PackageViewDescriptor
import org.jetbrains.kotlin.idea.core.ExpectedInfo
import org.jetbrains.kotlin.idea.kdoc.getKDocLinkResolutionScope
import org.jetbrains.kotlin.idea.kdoc.getParamDescriptors
import org.jetbrains.kotlin.idea.kdoc.resolveKDocLink
import org.jetbrains.kotlin.idea.util.CallType
import org.jetbrains.kotlin.idea.util.substituteExtensionIfCallable
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
@@ -35,6 +37,8 @@ import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -43,18 +47,21 @@ import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension
import org.jetbrains.kotlin.resolve.descriptorUtil.isExtensionProperty
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
import org.jetbrains.kotlin.resolve.scopes.utils.collectDescriptorsFiltered
import org.jetbrains.kotlin.resolve.scopes.utils.getImplicitReceiversHierarchy
class KDocCompletionContributor(): CompletionContributor() {
class KDocCompletionContributor() : CompletionContributor() {
init {
extend(CompletionType.BASIC, psiElement().inside(KDocName::class.java),
KDocNameCompletionProvider)
extend(CompletionType.BASIC,
psiElement().afterLeaf(
StandardPatterns.or(psiElement(KDocTokens.LEADING_ASTERISK), psiElement(KDocTokens.START))),
StandardPatterns.or(psiElement(KDocTokens.LEADING_ASTERISK), psiElement(KDocTokens.START))),
KDocTagCompletionProvider)
extend(CompletionType.BASIC,
@@ -62,7 +69,7 @@ class KDocCompletionContributor(): CompletionContributor() {
}
}
object KDocNameCompletionProvider: CompletionProvider<CompletionParameters>() {
object KDocNameCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
KDocNameCompletionSession(parameters, ToFromOriginalFileMapper.create(parameters), result).complete()
}
@@ -72,7 +79,7 @@ class KDocNameCompletionSession(
parameters: CompletionParameters,
toFromOriginalFileMapper: ToFromOriginalFileMapper,
resultSet: CompletionResultSet
): CompletionSession(CompletionSessionConfiguration(parameters), parameters, toFromOriginalFileMapper, resultSet) {
) : CompletionSession(CompletionSessionConfiguration(parameters), parameters, toFromOriginalFileMapper, resultSet) {
override val descriptorKindFilter: DescriptorKindFilter? get() = null
override val expectedInfos: Collection<ExpectedInfo> get() = emptyList()
@@ -83,11 +90,13 @@ class KDocNameCompletionSession(
val declarationDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] ?: return
if (kdocLink.getTagIfSubject()?.knownTag == KDocKnownTag.PARAM) {
addParamCompletions(position, declarationDescriptor)
} else {
addLinkCompletions(declarationDescriptor)
}
else {
addLinkCompletions(declarationDescriptor, kdocLink)
}
}
private fun addParamCompletions(position: KDocName,
declarationDescriptor: DeclarationDescriptor) {
val section = position.getContainingSection()
@@ -100,8 +109,13 @@ class KDocNameCompletionSession(
}
}
private fun addLinkCompletions(declarationDescriptor: DeclarationDescriptor) {
val scope = getKDocLinkResolutionScope(resolutionFacade, declarationDescriptor)
fun collectPackageViewDescriptors(qualifiedLink: List<String>, nameFilter: (Name) -> Boolean): Sequence<PackageViewDescriptor> {
val fqName = if (qualifiedLink.isEmpty()) FqName.ROOT else FqName.fromSegments(qualifiedLink)
return moduleDescriptor.getSubPackagesOf(fqName, nameFilter).asSequence()
.map { moduleDescriptor.getPackage(it) }
}
fun collectDescriptorsFromScope(scope: LexicalScope, nameFilter: (Name) -> Boolean, collectFormParentScopes: Boolean): Sequence<DeclarationDescriptor> {
val implicitReceivers = scope.getImplicitReceiversHierarchy().map { it.value }
fun isApplicable(descriptor: DeclarationDescriptor): Boolean {
@@ -110,15 +124,49 @@ class KDocNameCompletionSession(
if (extensionReceiver != null) {
val substituted = descriptor.substituteExtensionIfCallable(implicitReceivers, bindingContext, DataFlowInfo.EMPTY,
CallType.DEFAULT, moduleDescriptor)
return !substituted.isEmpty()
return substituted.isNotEmpty()
}
}
return true
}
scope.collectDescriptorsFiltered(nameFilter = descriptorNameFilter.toNameFilter()).filter(::isApplicable).forEach {
@Suppress("IfThenToElvis")
return (
if (collectFormParentScopes)
scope.collectDescriptorsFiltered(nameFilter = nameFilter).asSequence()
else if (scope is LexicalScope.Empty)
scope.parent.getContributedDescriptors(nameFilter = nameFilter).asSequence()
else
(scope.getContributedDescriptors(nameFilter = nameFilter).asSequence()
+ scope.parent.collectDescriptorsFiltered(nameFilter = nameFilter).asSequence()
.filter { it.isExtension || it.isExtensionProperty })
).filter(::isApplicable)
}
fun collectDescriptorsForLinkCompletion(declarationDescriptor: DeclarationDescriptor, kDocLink: KDocLink): Sequence<DeclarationDescriptor> {
val qualifiedLink = kDocLink.getLinkText().split('.').dropLast(1)
val nameFilter = descriptorNameFilter.toNameFilter()
if (qualifiedLink.isNotEmpty()) {
val parentDescriptors = resolveKDocLink(bindingContext, resolutionFacade, declarationDescriptor, null, qualifiedLink)
val childDescriptorsOfPartialLink = parentDescriptors.asSequence().flatMap {
val scope = getKDocLinkResolutionScope(resolutionFacade, it)
collectDescriptorsFromScope(scope, nameFilter, false)
}
return (collectPackageViewDescriptors(qualifiedLink, nameFilter) + childDescriptorsOfPartialLink)
}
else {
val scope = getKDocLinkResolutionScope(resolutionFacade, declarationDescriptor)
return (collectDescriptorsFromScope(scope, nameFilter, true)
+ collectPackageViewDescriptors(qualifiedLink, nameFilter))
}
}
private fun addLinkCompletions(declarationDescriptor: DeclarationDescriptor, kDocLink: KDocLink) {
collectDescriptorsForLinkCompletion(declarationDescriptor, kDocLink).forEach {
val element = basicLookupElementFactory.createLookupElement(it, parametersAndTypeGrayed = true)
collector.addElement(object: LookupElementDecorator<LookupElement>(element) {
collector.addElement(object : LookupElementDecorator<LookupElement>(element) {
override fun handleInsert(context: InsertionContext?) {
// insert only plain name here, no qualifier/parentheses/etc.
}
@@ -127,7 +175,7 @@ class KDocNameCompletionSession(
}
}
object KDocTagCompletionProvider: CompletionProvider<CompletionParameters>() {
object KDocTagCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
// findIdentifierPrefix() requires identifier part characters to be a superset of identifier start characters
val prefix = CompletionUtil.findIdentifierPrefix(
@@ -148,7 +196,7 @@ object KDocTagCompletionProvider: CompletionProvider<CompletionParameters>() {
}
}
private fun KDocKnownTag.isApplicable(declaration: KtDeclaration) = when(this) {
private fun KDocKnownTag.isApplicable(declaration: KtDeclaration) = when (this) {
KDocKnownTag.CONSTRUCTOR, KDocKnownTag.PROPERTY -> declaration is KtClassOrObject
KDocKnownTag.RETURN -> declaration is KtNamedFunction
KDocKnownTag.RECEIVER -> declaration is KtNamedFunction && declaration.receiverTypeReference != null
+20
View File
@@ -0,0 +1,20 @@
package a
class B {
/**
* [a.B.<caret>]
*/
fun member() {
}
}
fun B.ext() {
}
val B.extVal: String
get() = ""
// EXIST: ext
// EXIST: extVal
@@ -0,0 +1,33 @@
package a
class C {
class B {
/**
* [a.C.B.<caret>]
*/
fun member() {
}
}
fun B.ext() {
}
val B.extVal: String
get() = ""
}
fun C.B.topExt() {
}
val C.B.topExtVal: String
get() = ""
// EXIST: ext
// EXIST: extVal
// EXIST: topExt
// EXIST: topExtVal
+19
View File
@@ -0,0 +1,19 @@
package z
/**
* [z.<caret>]
*/
fun f(xyzzy: String) {
}
fun bar() {
}
class Z
// EXIST: Z
// EXIST: f
// EXIST: bar
// ABSENT: z
+6
View File
@@ -1,3 +1,5 @@
package z
/**
* [<caret>]
*/
@@ -9,5 +11,9 @@ fun bar() {
}
class Z
// EXIST: z
// EXIST: Z
// EXIST: xyzzy
// EXIST: bar
@@ -0,0 +1,11 @@
package z
/**
* [z.X.Y.<caret>]
*/
class X {
enum class Y { First, Second }
}
// EXIST: First
// EXIST: Second
@@ -0,0 +1,9 @@
/**
* [Y.<caret>]
*/
class X {
enum class Y { First, Second }
}
// EXIST: First
// EXIST: Second
@@ -36,12 +36,42 @@ public class KDocCompletionTestGenerated extends AbstractJvmBasicCompletionTest
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/kdoc"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("ExtensionsFQLink.kt")
public void testExtensionsFQLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/ExtensionsFQLink.kt");
doTest(fileName);
}
@TestMetadata("ExtensionsForSubClassFQLink.kt")
public void testExtensionsForSubClassFQLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/ExtensionsForSubClassFQLink.kt");
doTest(fileName);
}
@TestMetadata("FQLink.kt")
public void testFQLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/FQLink.kt");
doTest(fileName);
}
@TestMetadata("Link.kt")
public void testLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/Link.kt");
doTest(fileName);
}
@TestMetadata("MemberEnumEntryFQLink.kt")
public void testMemberEnumEntryFQLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/MemberEnumEntryFQLink.kt");
doTest(fileName);
}
@TestMetadata("MemberEnumEntryLink.kt")
public void testMemberEnumEntryLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/MemberEnumEntryLink.kt");
doTest(fileName);
}
@TestMetadata("MemberLink.kt")
public void testMemberLink() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/kdoc/MemberLink.kt");