diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index e8da0ff99..4c5c74ab4 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -84,6 +84,7 @@ class KotlinLanguageServer( serverCapabilities.completionProvider = CompletionOptions(false, listOf(".")) serverCapabilities.signatureHelpProvider = SignatureHelpOptions(listOf("(", ",")) serverCapabilities.definitionProvider = Either.forLeft(true) + serverCapabilities.implementationProvider = Either.forLeft(true) serverCapabilities.documentSymbolProvider = Either.forLeft(true) serverCapabilities.workspaceSymbolProvider = Either.forLeft(true) serverCapabilities.referencesProvider = Either.forLeft(true) diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt index 2ec1e5227..8e02dfb16 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt @@ -7,6 +7,7 @@ import org.eclipse.lsp4j.services.TextDocumentService import org.javacs.kt.codeaction.codeActions import org.javacs.kt.completion.completions import org.javacs.kt.definition.goToDefinition +import org.javacs.kt.implementation.findImplementations import org.javacs.kt.diagnostic.convertDiagnostic import org.javacs.kt.formatting.FormattingService import org.javacs.kt.hover.hoverAt @@ -136,6 +137,17 @@ class KotlinTextDocumentService( } } + override fun implementation(position: ImplementationParams): CompletableFuture, List>> = async.compute { + reportTime { + LOG.info("Go-to-implementation at {}", describePosition(position)) + + position.textDocument.filePath + ?.let { findImplementations(it, offset(sp.content(parseURI(position.textDocument.uri)), position.position.line, position.position.character), sp) } + ?.let { Either.forLeft, List>(it) } + ?: Either.forLeft(emptyList()) + } + } + override fun rangeFormatting(params: DocumentRangeFormattingParams): CompletableFuture> = async.compute { val code = extractRange(params.textDocument.content, params.range) listOf(TextEdit( diff --git a/server/src/main/kotlin/org/javacs/kt/implementation/FindImplementations.kt b/server/src/main/kotlin/org/javacs/kt/implementation/FindImplementations.kt new file mode 100644 index 000000000..10dd97be0 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/implementation/FindImplementations.kt @@ -0,0 +1,153 @@ +package org.javacs.kt.implementation + +import org.eclipse.lsp4j.Location +import org.javacs.kt.LOG +import org.javacs.kt.SourcePath +import org.javacs.kt.CompiledFile +import org.javacs.kt.position.location +import org.javacs.kt.util.findParent +import org.javacs.kt.util.toPath +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import java.nio.file.Path + +/** Finds implementations of interfaces, abstract classes, or their members. */ +fun findImplementations(file: Path, cursor: Int, sp: SourcePath): List { + val compiled = sp.currentVersion(file.toUri()) + val target = compiled.referenceExpressionAtPoint(cursor)?.second + ?: compiled.elementAtPoint(cursor) + ?.findParent() + ?.let { compiled.compile[BindingContext.DECLARATION_TO_DESCRIPTOR, it] } + ?: return emptyList() + + LOG.info("Finding implementations of {}", target) + + return when (target) { + is ClassDescriptor -> findClassImplementations(target, sp) + is FunctionDescriptor -> findFunctionImplementations(target, sp) + is PropertyDescriptor -> findPropertyImplementations(target, sp) + else -> emptyList() + } +} + +private fun findClassImplementations(target: ClassDescriptor, sp: SourcePath): List { + if (target.modality == Modality.FINAL) return emptyList() + + val targetFqName = target.fqNameSafe + val sourceFiles = sp.all().map { it.toPath() } + val context = sp.compileFiles(sourceFiles.map(Path::toUri)) + + return context.getSliceContents(BindingContext.CLASS) + .values + .filter { classDescriptor -> + classDescriptor != target + && classDescriptor.kind != ClassKind.INTERFACE + && classDescriptor.modality != Modality.ABSTRACT + && classDescriptor.getAllSuperClassifiers().any { it.fqNameSafe == targetFqName } + } + .mapNotNull { locationOfClassIdentifier(it) } + .sortedWith(compareBy({ it.uri }, { it.range.start.line })) +} + +private fun findFunctionImplementations(target: FunctionDescriptor, sp: SourcePath): List { + val containingClass = target.containingDeclaration as? ClassDescriptor ?: return emptyList() + if (containingClass.modality == Modality.FINAL) return emptyList() + + val targetFqName = containingClass.fqNameSafe + val targetName = target.name + val sourceFiles = sp.all().map { it.toPath() } + val context = sp.compileFiles(sourceFiles.map(Path::toUri)) + + return context.getSliceContents(BindingContext.CLASS) + .values + .filter { classDescriptor -> + classDescriptor != containingClass + && classDescriptor.getAllSuperClassifiers().any { it.fqNameSafe == targetFqName } + } + .flatMap { classDescriptor -> + classDescriptor.defaultType.memberScope.getContributedDescriptors() + .filterIsInstance() + .filter { fn -> + fn.name == targetName + && fn.overriddenDescriptors.any { overridden -> + overridden.fqNameSafe == target.fqNameSafe + || overridden.original.fqNameSafe == target.fqNameSafe + } + } + } + .mapNotNull { locationOfIdentifier(it) } + .sortedWith(compareBy({ it.uri }, { it.range.start.line })) +} + +private fun findPropertyImplementations(target: PropertyDescriptor, sp: SourcePath): List { + val containingClass = target.containingDeclaration as? ClassDescriptor ?: return emptyList() + if (containingClass.modality == Modality.FINAL) return emptyList() + + val targetFqName = containingClass.fqNameSafe + val targetName = target.name + val sourceFiles = sp.all().map { it.toPath() } + val context = sp.compileFiles(sourceFiles.map(Path::toUri)) + + return context.getSliceContents(BindingContext.CLASS) + .values + .filter { classDescriptor -> + classDescriptor != containingClass + && classDescriptor.getAllSuperClassifiers().any { it.fqNameSafe == targetFqName } + } + .flatMap { classDescriptor -> + classDescriptor.defaultType.memberScope.getContributedDescriptors() + .filterIsInstance() + .filter { prop -> + prop.name == targetName + && prop.overriddenDescriptors.any { overridden -> + overridden.fqNameSafe == target.fqNameSafe + || overridden.original.fqNameSafe == target.fqNameSafe + } + } + } + .mapNotNull { locationOfIdentifier(it) } + .sortedWith(compareBy({ it.uri }, { it.range.start.line })) +} + +private fun locationOfClassIdentifier(descriptor: ClassDescriptor): Location? { + val psi = descriptor.findPsi() + if (psi is KtNamedDeclaration) { + return psi.nameIdentifier?.let(::location) ?: location(psi) + } + return location(descriptor) +} + +private fun locationOfIdentifier(descriptor: DeclarationDescriptor): Location? { + val psi = descriptor.findPsi() + if (psi is KtNamedDeclaration) { + return psi.nameIdentifier?.let(::location) ?: location(psi) + } + return location(descriptor) +} + +private fun ClassDescriptor.getAllSuperClassifiers(): Sequence = sequence { + val visited = mutableSetOf() + val queue = ArrayDeque() + + for (supertype in this@getAllSuperClassifiers.typeConstructor.supertypes) { + (supertype.constructor.declarationDescriptor as? ClassDescriptor)?.let { queue.add(it) } + } + + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + if (!visited.add(current)) continue + yield(current) + for (supertype in current.typeConstructor.supertypes) { + (supertype.constructor.declarationDescriptor as? ClassDescriptor)?.let { queue.add(it) } + } + } +} diff --git a/server/src/test/kotlin/org/javacs/kt/ImplementationTest.kt b/server/src/test/kotlin/org/javacs/kt/ImplementationTest.kt new file mode 100644 index 000000000..fb19e4eb1 --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/ImplementationTest.kt @@ -0,0 +1,52 @@ +package org.javacs.kt + +import org.eclipse.lsp4j.ImplementationParams +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.hasItem +import org.hamcrest.Matchers.hasSize +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class ImplementationTest : SingleFileTestFixture("implementation", "Interface.kt") { + + private fun implementationParams(relativePath: String, line: Int, column: Int): ImplementationParams { + val file = workspaceRoot.resolve(relativePath) + val fileId = TextDocumentIdentifier(file.toUri().toString()) + return ImplementationParams(fileId, position(line, column)) + } + + @Test + fun `find implementations of interface`() { + // Cursor on "Animal" interface name (line 1, col 11) + val implementations = languageServer.textDocumentService.implementation(implementationParams(file, 1, 11)).get().left + val uris = implementations.map { it.uri } + + assertThat(implementations, hasSize(2)) + assertThat(uris, hasItem(containsString("Interface.kt"))) + } + + @Test + fun `find implementations of abstract class`() { + // Cursor on "Pet" abstract class name (line 6, col 16) + val implementations = languageServer.textDocumentService.implementation(implementationParams(file, 6, 16)).get().left + + assertThat(implementations, hasSize(1)) + } + + @Test + fun `find implementations of interface method`() { + // Cursor on "speak" method in Animal interface (line 2, col 9) + val implementations = languageServer.textDocumentService.implementation(implementationParams(file, 2, 9)).get().left + + assertThat(implementations, hasSize(2)) + } + + @Test + fun `find implementations of interface property`() { + // Cursor on "name" property in Animal interface (line 3, col 9) + val implementations = languageServer.textDocumentService.implementation(implementationParams(file, 3, 9)).get().left + + assertThat(implementations, hasSize(2)) + } +} diff --git a/server/src/test/resources/implementation/Interface.kt b/server/src/test/resources/implementation/Interface.kt new file mode 100644 index 000000000..6c19a4f01 --- /dev/null +++ b/server/src/test/resources/implementation/Interface.kt @@ -0,0 +1,24 @@ +interface Animal { + fun speak(): String + val name: String +} + +abstract class Pet : Animal { + abstract fun play(): String +} + +class Dog : Pet() { + override fun speak(): String = "Woof" + override val name: String = "Dog" + override fun play(): String = "Fetch" +} + +class Cat : Animal { + override fun speak(): String = "Meow" + override val name: String = "Cat" +} + +fun main() { + val animal: Animal = Dog() + animal.speak() +}