diff --git a/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/SampleResolutionService.kt b/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/SampleResolutionService.kt new file mode 100644 index 00000000000..3a918910a1a --- /dev/null +++ b/idea/ide-common/src/org/jetbrains/kotlin/idea/kdoc/SampleResolutionService.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2017 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.openapi.components.ServiceManager +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.resolve.BindingContext + +interface SampleResolutionService { + fun resolveSample(context: BindingContext, fromDescriptor: DeclarationDescriptor, resolutionFacade: ResolutionFacade, qualifiedName: List): Collection + + companion object { + + /** + * It's internal implementation, please use [resolveKDocSampleLink], or [resolveKDocLink] + */ + internal fun resolveSample(context: BindingContext, fromDescriptor: DeclarationDescriptor, resolutionFacade: ResolutionFacade, qualifiedName: List): Collection { + val instance = ServiceManager.getService(resolutionFacade.project, SampleResolutionService::class.java) + return instance?.resolveSample(context, fromDescriptor, resolutionFacade, qualifiedName) ?: emptyList() + } + } +} 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 0c074020412..b4dfaca0ab4 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 @@ -35,17 +35,64 @@ import org.jetbrains.kotlin.resolve.scopes.* import org.jetbrains.kotlin.resolve.scopes.utils.collectDescriptorsFiltered import org.jetbrains.kotlin.resolve.scopes.utils.memberScopeAsImportingScope import org.jetbrains.kotlin.resolve.source.PsiSourceElement +import org.jetbrains.kotlin.utils.ifEmpty fun resolveKDocLink(context: BindingContext, resolutionFacade: ResolutionFacade, fromDescriptor: DeclarationDescriptor, fromSubjectOfTag: KDocTag?, - qualifiedName: List): Collection { + qualifiedName: List): Collection = + when (fromSubjectOfTag?.knownTag) { + KDocKnownTag.PARAM -> resolveParamLink(fromDescriptor, qualifiedName) + KDocKnownTag.SAMPLE -> resolveKDocSampleLink(context, resolutionFacade, fromDescriptor, qualifiedName) + else -> resolveDefaultKDocLink(context, resolutionFacade, fromDescriptor, qualifiedName) + } - if (fromSubjectOfTag?.knownTag == KDocKnownTag.PARAM) { - return resolveParamLink(fromDescriptor, qualifiedName) + +fun getParamDescriptors(fromDescriptor: DeclarationDescriptor): List { + // TODO resolve parameters of functions passed as parameters + when (fromDescriptor) { + is CallableDescriptor -> + return fromDescriptor.valueParameters + fromDescriptor.typeParameters + is ClassifierDescriptor -> { + val typeParams = fromDescriptor.typeConstructor.parameters + if (fromDescriptor is ClassDescriptor) { + val constructorDescriptor = fromDescriptor.unsubstitutedPrimaryConstructor + if (constructorDescriptor != null) { + return typeParams + constructorDescriptor.valueParameters + } + } + return typeParams + } } + return listOf() +} + +private fun resolveParamLink(fromDescriptor: DeclarationDescriptor, qualifiedName: List): List { + val name = qualifiedName.singleOrNull() ?: return listOf() + return getParamDescriptors(fromDescriptor).filter { it.name.asString() == name } +} + +fun resolveKDocSampleLink(context: BindingContext, + resolutionFacade: ResolutionFacade, + fromDescriptor: DeclarationDescriptor, + qualifiedName: List): Collection { + + val resolvedViaService = SampleResolutionService.resolveSample(context, fromDescriptor, resolutionFacade, qualifiedName) + if (resolvedViaService.isNotEmpty()) return resolvedViaService + + return resolveDefaultKDocLink(context, resolutionFacade, fromDescriptor, qualifiedName) +} + + + + +private fun resolveDefaultKDocLink(context: BindingContext, + resolutionFacade: ResolutionFacade, + fromDescriptor: DeclarationDescriptor, + qualifiedName: List): Collection { + val scope = getKDocLinkResolutionScope(resolutionFacade, fromDescriptor) if (qualifiedName.size == 1) { @@ -82,31 +129,6 @@ fun resolveKDocLink(context: BindingContext, return listOf(descriptor) } -fun getParamDescriptors(fromDescriptor: DeclarationDescriptor): List { - // TODO resolve parameters of functions passed as parameters - when (fromDescriptor) { - is CallableDescriptor -> - return fromDescriptor.valueParameters + fromDescriptor.typeParameters - is ClassifierDescriptor -> { - val typeParams = fromDescriptor.typeConstructor.parameters - if (fromDescriptor is ClassDescriptor) { - val constructorDescriptor = fromDescriptor.unsubstitutedPrimaryConstructor - if (constructorDescriptor != null) { - return typeParams + constructorDescriptor.valueParameters - } - } - return typeParams - } - } - - return listOf() -} - -private fun resolveParamLink(fromDescriptor: DeclarationDescriptor, qualifiedName: List): List { - val name = qualifiedName.singleOrNull() ?: return listOf() - return getParamDescriptors(fromDescriptor).filter { it.name.asString() == name } -} - private fun getPackageInnerScope(descriptor: PackageFragmentDescriptor): MemberScope { return descriptor.containingDeclaration.getPackage(descriptor.fqName).memberScope } diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 72efd37ee8b..357797340a6 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -314,6 +314,9 @@ + + diff --git a/idea/src/org/jetbrains/kotlin/idea/kdoc/IdeSampleResolutionService.kt b/idea/src/org/jetbrains/kotlin/idea/kdoc/IdeSampleResolutionService.kt new file mode 100644 index 00000000000..583340504a4 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/kdoc/IdeSampleResolutionService.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2017 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.openapi.project.Project +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.idea.stubindex.KotlinFunctionShortNameIndex +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +class IdeSampleResolutionService(val project: Project) : SampleResolutionService { + + override fun resolveSample(context: BindingContext, fromDescriptor: DeclarationDescriptor, resolutionFacade: ResolutionFacade, qualifiedName: List): Collection { + val allScope = GlobalSearchScope.projectScope(project) + val shortName = qualifiedName.lastOrNull() ?: return emptyList() + val functions = KotlinFunctionShortNameIndex.getInstance().get(shortName, project, allScope) + val targetFqName = FqName.fromSegments(qualifiedName) + + + val descriptors = functions.asSequence() + .filter { it.fqName == targetFqName } + .map { it.resolveToDescriptor(BodyResolveMode.PARTIAL) } // TODO Filter out not visible due dependencies config descriptors + .toList() + return descriptors + } +} \ No newline at end of file diff --git a/idea/testData/kdoc/multiModuleSamples/simple/code/usage.kt b/idea/testData/kdoc/multiModuleSamples/simple/code/usage.kt new file mode 100644 index 00000000000..9f1d27c0caf --- /dev/null +++ b/idea/testData/kdoc/multiModuleSamples/simple/code/usage.kt @@ -0,0 +1,11 @@ +/** + * @sample samples.sample + */ +fun some() { + +} + +//INFO:
public fun some(): Unit defined in root package

+//INFO:
Samples:
samples.sample

+//INFO: println("Hello, world")
+//INFO: 
diff --git a/idea/testData/kdoc/multiModuleSamples/simple/samplesTest/sample.kt b/idea/testData/kdoc/multiModuleSamples/simple/samplesTest/sample.kt new file mode 100644 index 00000000000..754222cc295 --- /dev/null +++ b/idea/testData/kdoc/multiModuleSamples/simple/samplesTest/sample.kt @@ -0,0 +1,5 @@ +package samples + +fun sample() { + println("Hello, world") +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/AbstractQuickDocProviderTest.java b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/AbstractQuickDocProviderTest.java index 280d1f8b038..68f09aefa32 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/AbstractQuickDocProviderTest.java +++ b/idea/tests/org/jetbrains/kotlin/idea/editor/quickDoc/AbstractQuickDocProviderTest.java @@ -81,7 +81,7 @@ public abstract class AbstractQuickDocProviderTest extends KotlinLightCodeInsigh } } - private static void wrapToFileComparisonFailure(String info, String filePath, String fileData) { + public static void wrapToFileComparisonFailure(String info, String filePath, String fileData) { List infoLines = StringUtil.split(info, "\n"); StringBuilder infoBuilder = new StringBuilder(); for (String line : infoLines) { diff --git a/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocSampleTest.kt b/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocSampleTest.kt new file mode 100644 index 00000000000..cf421bfec66 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/kdoc/KDocSampleTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2017 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.documentation.DocumentationManager +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.rt.execution.junit.FileComparisonFailure +import org.jetbrains.kotlin.idea.editor.quickDoc.AbstractQuickDocProviderTest.wrapToFileComparisonFailure +import org.jetbrains.kotlin.idea.stubs.AbstractMultiModuleTest +import org.jetbrains.kotlin.test.InTextDirectivesUtils +import java.io.File + +class KDocSampleTest : AbstractMultiModuleTest() { + + override val testPath: String = "${super.testPath}/kdoc/multiModuleSamples/" + override fun getTestDataPath() = testPath + + fun testSimple() { + + val code = module("code") + val samples = module("samples", hasTestRoot = true) + + samples.addDependency(code) + + doTest("simple/code/usage.kt") + } + + fun doTest(path: String) { + val testDataFile = File(testPath, path) + configureByFile(path) + val documentationManager = DocumentationManager.getInstance(myProject) + val targetElement = documentationManager.findTargetElement(myEditor, file) + val originalElement = DocumentationManager.getOriginalElement(targetElement) + + var info = DocumentationManager.getProviderFromElement(targetElement).generateDoc(targetElement, originalElement) + if (info != null) { + info = StringUtil.convertLineSeparators(info) + } + if (info != null && !info.endsWith("\n")) { + info += "\n" + } + + val textData = FileUtil.loadFile(testDataFile, true) + val directives = InTextDirectivesUtils.findLinesWithPrefixesRemoved(textData, false, "INFO:") + + if (directives.isEmpty()) { + throw FileComparisonFailure( + "'// INFO:' directive was expected", + textData, + textData + "\n\n//INFO: " + info, + testDataFile.absolutePath) + } + else { + val expectedInfo = directives.joinToString("\n", postfix = "\n") + + if (expectedInfo.endsWith("...\n")) { + if (!info!!.startsWith(expectedInfo.removeSuffix("...\n"))) { + wrapToFileComparisonFailure(info, testDataPath, textData) + } + } + else if (expectedInfo != info) { + wrapToFileComparisonFailure(info, path, textData) + } + } + } +} \ No newline at end of file