diff --git a/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/resolveKDocLink.kt b/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/resolveKDocLink.kt index 74ea890710a..0c074020412 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/resolveKDocLink.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/resolveKDocLink.kt @@ -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 diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KDocCompletionContributor.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KDocCompletionContributor.kt index 27eea420070..6057a3f80e7 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KDocCompletionContributor.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/KDocCompletionContributor.kt @@ -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() { +object KDocNameCompletionProvider : CompletionProvider() { 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 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, nameFilter: (Name) -> Boolean): Sequence { + 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 { 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 { + 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(element) { + collector.addElement(object : LookupElementDecorator(element) { override fun handleInsert(context: InsertionContext?) { // insert only plain name here, no qualifier/parentheses/etc. } @@ -127,7 +175,7 @@ class KDocNameCompletionSession( } } -object KDocTagCompletionProvider: CompletionProvider() { +object KDocTagCompletionProvider : CompletionProvider() { 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() { } } - 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 diff --git a/idea/idea-completion/testData/kdoc/ExtensionsFQLink.kt b/idea/idea-completion/testData/kdoc/ExtensionsFQLink.kt new file mode 100644 index 00000000000..dea43293ae8 --- /dev/null +++ b/idea/idea-completion/testData/kdoc/ExtensionsFQLink.kt @@ -0,0 +1,20 @@ +package a + +class B { + /** + * [a.B.] + */ + fun member() { + + } +} + +fun B.ext() { + +} + +val B.extVal: String + get() = "" + +// EXIST: ext +// EXIST: extVal \ No newline at end of file diff --git a/idea/idea-completion/testData/kdoc/ExtensionsForSubClassFQLink.kt b/idea/idea-completion/testData/kdoc/ExtensionsForSubClassFQLink.kt new file mode 100644 index 00000000000..2a751ec9475 --- /dev/null +++ b/idea/idea-completion/testData/kdoc/ExtensionsForSubClassFQLink.kt @@ -0,0 +1,33 @@ +package a + +class C { + + class B { + /** + * [a.C.B.] + */ + 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 \ No newline at end of file diff --git a/idea/idea-completion/testData/kdoc/FQLink.kt b/idea/idea-completion/testData/kdoc/FQLink.kt new file mode 100644 index 00000000000..531ef97dc64 --- /dev/null +++ b/idea/idea-completion/testData/kdoc/FQLink.kt @@ -0,0 +1,19 @@ +package z + +/** + * [z.] + */ +fun f(xyzzy: String) { + +} + +fun bar() { + +} + +class Z + +// EXIST: Z +// EXIST: f +// EXIST: bar +// ABSENT: z diff --git a/idea/idea-completion/testData/kdoc/Link.kt b/idea/idea-completion/testData/kdoc/Link.kt index 431a2ad7791..1bbb50aab98 100644 --- a/idea/idea-completion/testData/kdoc/Link.kt +++ b/idea/idea-completion/testData/kdoc/Link.kt @@ -1,3 +1,5 @@ +package z + /** * [] */ @@ -9,5 +11,9 @@ fun bar() { } +class Z + +// EXIST: z +// EXIST: Z // EXIST: xyzzy // EXIST: bar diff --git a/idea/idea-completion/testData/kdoc/MemberEnumEntryFQLink.kt b/idea/idea-completion/testData/kdoc/MemberEnumEntryFQLink.kt new file mode 100644 index 00000000000..45727031cd2 --- /dev/null +++ b/idea/idea-completion/testData/kdoc/MemberEnumEntryFQLink.kt @@ -0,0 +1,11 @@ +package z + +/** + * [z.X.Y.] + */ +class X { + enum class Y { First, Second } +} + +// EXIST: First +// EXIST: Second \ No newline at end of file diff --git a/idea/idea-completion/testData/kdoc/MemberEnumEntryLink.kt b/idea/idea-completion/testData/kdoc/MemberEnumEntryLink.kt new file mode 100644 index 00000000000..4290998e0da --- /dev/null +++ b/idea/idea-completion/testData/kdoc/MemberEnumEntryLink.kt @@ -0,0 +1,9 @@ +/** + * [Y.] + */ +class X { + enum class Y { First, Second } +} + +// EXIST: First +// EXIST: Second \ No newline at end of file diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KDocCompletionTestGenerated.java b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KDocCompletionTestGenerated.java index fcd58359c59..71d0f9dc297 100644 --- a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KDocCompletionTestGenerated.java +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KDocCompletionTestGenerated.java @@ -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");