diff --git a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocName.kt b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocName.kt index eeeac3d2219..7be79583574 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocName.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/kdoc/psi/impl/KDocName.kt @@ -30,10 +30,12 @@ import org.jetbrains.kotlin.psi.psiUtil.getChildOfType public class KDocName(node: ASTNode): JetElementImpl(node) { public fun getContainingDoc(): KDoc { val kdoc = getStrictParentOfType() - if (kdoc == null) { - throw IllegalStateException("KDocName must be inside a KDoc") - } - return kdoc + return kdoc ?: throw IllegalStateException("KDocName must be inside a KDoc") + } + + public fun getContainingSection(): KDocSection { + val kdoc = getStrictParentOfType() + return kdoc ?: throw IllegalStateException("KDocName must be inside a KDocSection") } public fun getQualifier(): KDocName? = getChildOfType() diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index f623777e652..8020bfd686a 100644 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -678,6 +678,10 @@ fun main(args: Array) { testClass(javaClass()) { model("kdoc/highlighting") } + + testClass("org.jetbrains.kotlin.idea.kdoc.KDocCompletionTestGenerated") { + model("kdoc/completion") + } } testGroup("idea/tests", "compiler/testData") { @@ -734,6 +738,13 @@ fun main(args: Array) { } private class TestGroup(val testsRoot: String, val testDataRoot: String) { + inline fun testClass( + suiteTestClass: String = getDefaultSuiteTestClass(javaClass()), + [noinline] init: TestClass.() -> Unit + ) { + testClass(javaClass(), suiteTestClass, init) + } + fun testClass( baseTestClass: Class, suiteTestClass: String = getDefaultSuiteTestClass(baseTestClass), diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/kdoc/KDocReference.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/kdoc/KDocReference.kt index b2256652453..2f923b921c6 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/kdoc/KDocReference.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/kdoc/KDocReference.kt @@ -46,6 +46,8 @@ import org.jetbrains.kotlin.resolve.source.PsiSourceElement import org.jetbrains.kotlin.psi.JetFile import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithSource import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import com.intellij.psi.PsiReference +import org.jetbrains.kotlin.idea.caches.resolve.analyze public class KDocReference(element: KDocName): JetMultiReference(element) { override fun getTargetDescriptors(context: BindingContext): Collection { @@ -92,27 +94,28 @@ public fun resolveKDocLink(session: KotlinCodeAnalyzer, return result } -private fun resolveParamLink(fromDescriptor: DeclarationDescriptor, qualifiedName: List): List { +public fun getParamDescriptors(fromDescriptor: DeclarationDescriptor): List { // TODO resolve parameters of functions passed as parameters - val name = qualifiedName.singleOrNull() ?: return listOf() when (fromDescriptor) { is CallableDescriptor -> - return fromDescriptor.getValueParameters().filter { it.getName().asString() == name } + return fromDescriptor.getValueParameters() is ClassifierDescriptor -> { - val typeParams = fromDescriptor.getTypeConstructor().getParameters().filter { it.getName().asString() == name } - if (typeParams.isNotEmpty()) { - return typeParams - } + val typeParams = fromDescriptor.getTypeConstructor().getParameters() if (fromDescriptor is ClassDescriptor) { - return resolveParamLink(fromDescriptor.getUnsubstitutedPrimaryConstructor(), qualifiedName) + return typeParams + fromDescriptor.getUnsubstitutedPrimaryConstructor().getValueParameters() } - return listOf() + return typeParams } } return listOf() } +private fun resolveParamLink(fromDescriptor: DeclarationDescriptor, qualifiedName: List): List { + val name = qualifiedName.singleOrNull() ?: return listOf() + return getParamDescriptors(fromDescriptor).filter { it.getName().asString() == name } +} + private fun getPackageInnerScope(descriptor: PackageFragmentDescriptor): JetScope { val module = descriptor.getContainingDeclaration() val packageView = module.getPackage(descriptor.fqName) @@ -136,7 +139,7 @@ private fun getClassInnerScope(outerScope: JetScope, descriptor: ClassDescriptor return classScope } -private fun getResolutionScope(session: KotlinCodeAnalyzer, descriptor: DeclarationDescriptor): JetScope { +public fun getResolutionScope(session: KotlinCodeAnalyzer, descriptor: DeclarationDescriptor): JetScope { return when (descriptor) { is PackageFragmentDescriptor -> getPackageInnerScope(descriptor) diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index e6040c9003f..31564220e78 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -276,6 +276,8 @@ id="JetCompletionContributor" order="first" implementationClass="org.jetbrains.kotlin.idea.completion.KotlinCompletionContributor"/> + () != null) { + // don't stop here, allow other contributors to run + return + } + if (shouldSuppressCompletion(parameters, result.getPrefixMatcher())) { result.stopHere() return @@ -235,10 +240,6 @@ public class KotlinCompletionContributor : CompletionContributor() { val position = parameters.getPosition() val invocationCount = parameters.getInvocationCount() - // no completion in comments - // TODO: this must be changed if we will have references in doc-comments - if (position.getNonStrictParentOfType() != null) return true - // no completion inside number literals if (AFTER_NUMBER_LITERAL.accepts(position)) return true diff --git a/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocCompletionContributor.kt b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocCompletionContributor.kt new file mode 100644 index 00000000000..93d14d27986 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/kdoc/KDocCompletionContributor.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.kdoc + +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.patterns.PlatformPatterns.psiElement +import org.jetbrains.kotlin.kdoc.psi.impl.KDocName +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.util.ProcessingContext +import com.intellij.codeInsight.completion.CompletionResultSet +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.KotlinCacheService +import org.jetbrains.kotlin.resolve.lazy.KotlinCodeAnalyzer +import org.jetbrains.kotlin.idea.completion.LookupElementFactory +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import com.intellij.codeInsight.lookup.LookupElementDecorator +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.completion.InsertionContext +import org.jetbrains.kotlin.kdoc.lexer.KDocTokens +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.patterns.StandardPatterns + +class KDocCompletionContributor(): CompletionContributor() { + { + extend(CompletionType.BASIC, psiElement().inside(javaClass()), + KDocNameCompletionProvider) + + extend(CompletionType.BASIC, + psiElement().afterLeaf( + StandardPatterns.or(psiElement(KDocTokens.LEADING_ASTERISK), psiElement(KDocTokens.START))), + KDocTagCompletionProvider) + } +} + +object KDocNameCompletionProvider: CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { + val position = parameters.getPosition().getParentOfType(false) ?: return + val declaration = position.getContainingDoc().getOwner() ?: return + val bindingContext = declaration.analyze() + val kdocLink = position.getStrictParentOfType()!! + val declarationDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] + if (kdocLink.getTagIfSubject()?.knownTag == KDocKnownTag.PARAM) { + addParamCompletions(position, declarationDescriptor, result) + } else { + val session = KotlinCacheService.getInstance(parameters.getPosition().getProject()).getLazyResolveSession(position) + addLinkCompletions(session, position, declarationDescriptor, result) + } + } + + private fun addParamCompletions(position: KDocName, + declarationDescriptor: DeclarationDescriptor, + result: CompletionResultSet) { + val section = position.getContainingSection() + val documentedParameters = section.findTagsByName("param").map { it.getSubjectName() }.toSet() + val descriptors = getParamDescriptors(declarationDescriptor) + .filter { it.getName().asString() !in documentedParameters } + + val resolutionFacade = position.getResolutionFacade() + val factory = LookupElementFactory(listOf()) + descriptors.forEach { + result.addElement(factory.createLookupElement(resolutionFacade, it, false)) + } + } + + private fun addLinkCompletions(session: KotlinCodeAnalyzer, + position: KDocName, + declarationDescriptor: DeclarationDescriptor, + result: CompletionResultSet) { + val scope = getResolutionScope(session, declarationDescriptor) + val resolutionFacade = position.getResolutionFacade() + val factory = LookupElementFactory(listOf()) + scope.getAllDescriptors().forEach { + val element = factory.createLookupElement(resolutionFacade, it, false) + result.addElement(object: LookupElementDecorator(element) { + override fun handleInsert(context: InsertionContext?) { + // don't insert any qualified name here + } + }) + } + } +} + +object KDocTagCompletionProvider: CompletionProvider() { + override fun addCompletions(parameters: CompletionParameters?, context: ProcessingContext, result: CompletionResultSet) { + KDocKnownTag.values().forEach { + result.addElement(LookupElementBuilder.create("@" + it.name().toLowerCase())) + } + } +} diff --git a/idea/testData/completion/basic/common/NoCompletionInDocComment.kt b/idea/testData/completion/basic/common/NoCompletionInDocComment.kt deleted file mode 100644 index e2f30d01a9e..00000000000 --- a/idea/testData/completion/basic/common/NoCompletionInDocComment.kt +++ /dev/null @@ -1,7 +0,0 @@ -/** - * - */ -class C - -// INVOCATION_COUNT: 1 -// NUMBER: 0 \ No newline at end of file diff --git a/idea/testData/kdoc/completion/Link.kt b/idea/testData/kdoc/completion/Link.kt new file mode 100644 index 00000000000..431a2ad7791 --- /dev/null +++ b/idea/testData/kdoc/completion/Link.kt @@ -0,0 +1,13 @@ +/** + * [] + */ +fun f(xyzzy: String) { + +} + +fun bar() { + +} + +// EXIST: xyzzy +// EXIST: bar diff --git a/idea/testData/kdoc/completion/ParamTag.kt b/idea/testData/kdoc/completion/ParamTag.kt new file mode 100644 index 00000000000..01e8d60ba9e --- /dev/null +++ b/idea/testData/kdoc/completion/ParamTag.kt @@ -0,0 +1,8 @@ +/** + * @param + */ +fun foo(xyzzy: Int, foobar: String) { +} + +// EXIST: xyzzy +// EXIST: foobar diff --git a/idea/testData/kdoc/completion/SkipExistingParamTag.kt b/idea/testData/kdoc/completion/SkipExistingParamTag.kt new file mode 100644 index 00000000000..afa6b0d2d04 --- /dev/null +++ b/idea/testData/kdoc/completion/SkipExistingParamTag.kt @@ -0,0 +1,9 @@ +/** + * @param xyzzy This is a xyzzy + * @param + */ +fun foo(xyzzy: Int, foobar: String) { +} + +// ABSENT: xyzzy +// EXIST: foobar diff --git a/idea/testData/kdoc/completion/TagName.kt b/idea/testData/kdoc/completion/TagName.kt new file mode 100644 index 00000000000..99b8d84cb24 --- /dev/null +++ b/idea/testData/kdoc/completion/TagName.kt @@ -0,0 +1,8 @@ +/** + * + */ +fun f(x: Int): Int { +} + +// EXIST: @param +// EXIST: @return diff --git a/idea/testData/kdoc/completion/TagNameStart.kt b/idea/testData/kdoc/completion/TagNameStart.kt new file mode 100644 index 00000000000..31ec329eb26 --- /dev/null +++ b/idea/testData/kdoc/completion/TagNameStart.kt @@ -0,0 +1,6 @@ +/** */ +fun f(x: Int): Int { +} + +// EXIST: @param +// EXIST: @return diff --git a/idea/tests/org/jetbrains/kotlin/completion/JSBasicCompletionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/completion/JSBasicCompletionTestGenerated.java index 9897177c5cc..8ea502d7631 100644 --- a/idea/tests/org/jetbrains/kotlin/completion/JSBasicCompletionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/completion/JSBasicCompletionTestGenerated.java @@ -694,12 +694,6 @@ public class JSBasicCompletionTestGenerated extends AbstractJSBasicCompletionTes doTest(fileName); } - @TestMetadata("NoCompletionInDocComment.kt") - public void testNoCompletionInDocComment() throws Exception { - String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/NoCompletionInDocComment.kt"); - doTest(fileName); - } - @TestMetadata("NoEmptyPackage.kt") public void testNoEmptyPackage() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/NoEmptyPackage.kt"); diff --git a/idea/tests/org/jetbrains/kotlin/completion/JvmBasicCompletionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/completion/JvmBasicCompletionTestGenerated.java index 9e729fda75d..248715fc214 100644 --- a/idea/tests/org/jetbrains/kotlin/completion/JvmBasicCompletionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/completion/JvmBasicCompletionTestGenerated.java @@ -694,12 +694,6 @@ public class JvmBasicCompletionTestGenerated extends AbstractJvmBasicCompletionT doTest(fileName); } - @TestMetadata("NoCompletionInDocComment.kt") - public void testNoCompletionInDocComment() throws Exception { - String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/NoCompletionInDocComment.kt"); - doTest(fileName); - } - @TestMetadata("NoEmptyPackage.kt") public void testNoEmptyPackage() throws Exception { String fileName = JetTestUtils.navigationMetadata("idea/testData/completion/basic/common/NoEmptyPackage.kt"); diff --git a/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocCompletionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocCompletionTestGenerated.java new file mode 100644 index 00000000000..d84edf11e54 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocCompletionTestGenerated.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.kdoc; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.InnerTestClasses; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.JetTestUtils; +import org.jetbrains.kotlin.completion.AbstractJvmBasicCompletionTest; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/testData/kdoc/completion") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class KDocCompletionTestGenerated extends AbstractJvmBasicCompletionTest { + public void testAllFilesPresentInCompletion() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/kdoc/completion"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("Link.kt") + public void testLink() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/kdoc/completion/Link.kt"); + doTest(fileName); + } + + @TestMetadata("ParamTag.kt") + public void testParamTag() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/kdoc/completion/ParamTag.kt"); + doTest(fileName); + } + + @TestMetadata("SkipExistingParamTag.kt") + public void testSkipExistingParamTag() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/kdoc/completion/SkipExistingParamTag.kt"); + doTest(fileName); + } + + @TestMetadata("TagName.kt") + public void testTagName() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/kdoc/completion/TagName.kt"); + doTest(fileName); + } + + @TestMetadata("TagNameStart.kt") + public void testTagNameStart() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/kdoc/completion/TagNameStart.kt"); + doTest(fileName); + } +}