diff --git a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt index 23fe1356e..0f1659098 100644 --- a/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt +++ b/server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt @@ -15,7 +15,8 @@ import java.nio.file.Path * and the compiler. Note that Kotlin sources are stored in SourcePath. */ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable { - private val workspaceRoots = mutableSetOf() + val workspaceRoots = mutableSetOf() + private val javaSourcePath = mutableSetOf() private val buildScriptClassPath = mutableSetOf() val classPath = mutableSetOf() diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt index 7f2017359..b3a417bd7 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt @@ -30,7 +30,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable { private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config, tempDirectory, uriContentProvider, classPath) private val workspaces = KotlinWorkspaceService(sourceFiles, sourcePath, classPath, textDocuments, config) - private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath) + private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath, sourcePath) private lateinit var client: LanguageClient diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt index cba9d5d6d..4fa37acf7 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt @@ -3,11 +3,14 @@ package org.javacs.kt import org.eclipse.lsp4j.* import org.javacs.kt.util.AsyncExecutor import org.javacs.kt.util.parseURI +import org.javacs.kt.resolve.resolveMain import java.util.concurrent.CompletableFuture +import java.nio.file.Paths class KotlinProtocolExtensionService( private val uriContentProvider: URIContentProvider, - private val cp: CompilerClassPath + private val cp: CompilerClassPath, + private val sp: SourcePath ) : KotlinProtocolExtensions { private val async = AsyncExecutor() @@ -18,4 +21,22 @@ class KotlinProtocolExtensionService( override fun buildOutputLocation(): CompletableFuture = async.compute { cp.outputDirectory.absolutePath } + + override fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> = async.compute { + val fileUri = parseURI(textDocument.uri) + val filePath = Paths.get(fileUri) + + // we find the longest one in case both the root and submodule are included + val workspacePath = cp.workspaceRoots.filter { + filePath.startsWith(it) + }.map { + it.toString() + }.maxByOrNull(String::length) ?: "" + + val compiledFile = sp.currentVersion(fileUri) + + resolveMain(compiledFile) + mapOf( + "projectRoot" to workspacePath + ) + } } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt index b6338eaf5..808ba0dce 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt @@ -12,4 +12,7 @@ interface KotlinProtocolExtensions { @JsonRequest fun buildOutputLocation(): CompletableFuture + + @JsonRequest + fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture> } diff --git a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt index 85af3b1ad..470853cc4 100644 --- a/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt +++ b/server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt @@ -13,6 +13,7 @@ import org.javacs.kt.KotlinTextDocumentService import org.javacs.kt.position.extractRange import org.javacs.kt.util.filePath import org.javacs.kt.util.parseURI +import org.javacs.kt.resolve.resolveMain import java.net.URI import java.nio.file.Paths import java.util.concurrent.CompletableFuture @@ -29,7 +30,7 @@ class KotlinWorkspaceService( ) : WorkspaceService, LanguageClientAware { private val gson = Gson() private var languageClient: LanguageClient? = null - + override fun connect(client: LanguageClient): Unit { languageClient = client } diff --git a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt index 0b47ad7e5..6d7feb7e0 100644 --- a/server/src/main/kotlin/org/javacs/kt/command/Commands.kt +++ b/server/src/main/kotlin/org/javacs/kt/command/Commands.kt @@ -1,6 +1,7 @@ package org.javacs.kt.command const val JAVA_TO_KOTLIN_COMMAND = "convertJavaToKotlin" + val ALL_COMMANDS = listOf( - JAVA_TO_KOTLIN_COMMAND + JAVA_TO_KOTLIN_COMMAND, ) diff --git a/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt new file mode 100644 index 000000000..b6282eae2 --- /dev/null +++ b/server/src/main/kotlin/org/javacs/kt/resolve/ResolveMain.kt @@ -0,0 +1,62 @@ +package org.javacs.kt.resolve + +import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.javacs.kt.CompiledFile +import org.javacs.kt.position.range +import org.javacs.kt.util.partitionAroundLast +import com.intellij.openapi.util.TextRange + + +fun resolveMain(file: CompiledFile): Map { + val parsedFile = file.parse.copy() as KtFile + + val mainFunction = findTopLevelMainFunction(parsedFile) + if(null != mainFunction) { + // the KtFiles name is weird. Full path. This causes the class to have full path in name as well. Correcting to top level only + parsedFile.name = parsedFile.name.partitionAroundLast("/").second.substring(1) + + return mapOf("mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(), + "range" to range(file.content, mainFunction.second)) + } + + val companionMain = findCompanionObjectMain(parsedFile) + if(null != companionMain) { + return mapOf( + "mainClass" to (companionMain.first ?: ""), + "range" to range(file.content, companionMain.second) + ) + } + + return emptyMap() +} + +// only one main method allowed top level in a file (so invalid syntax files will not show any main methods) +private fun findTopLevelMainFunction(file: KtFile): Pair? = file.declarations.find { + it is KtNamedFunction && "main" == it.name +}?.let { + Pair(it.name, it.textRangeInParent) +} + +// finds a top level class that contains a companion object with a main function inside +private fun findCompanionObjectMain(file: KtFile): Pair? = file.declarations.flatMap { topLevelDeclaration -> + if(topLevelDeclaration is KtClass) { + topLevelDeclaration.companionObjects + } else { + emptyList() + } +}.flatMap { companionObject -> + companionObject.body?.children?.toList() ?: emptyList() +}.mapNotNull { companionObjectInternal -> + if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name && companionObjectInternal.text.startsWith("@JvmStatic")) { + companionObjectInternal + } else { + null + } +}.firstOrNull()?.let { + // a little ugly, but because of success of the above, we know that "it" has 4 layers of parent objects (child of companion object body, companion object body, companion object, outer class) + Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange) +} diff --git a/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt new file mode 100644 index 000000000..65ca7d5fc --- /dev/null +++ b/server/src/test/kotlin/org/javacs/kt/ResolveMainTest.kt @@ -0,0 +1,76 @@ +package org.javacs.kt + +import com.google.gson.Gson +import org.eclipse.lsp4j.ExecuteCommandParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.junit.Test +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull + +class NoMainResolve : SingleFileTestFixture("resolvemain", "NoMain.kt") { + @Test + fun `Should not find any main class info`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + val mainInfo = result as Map + assertNull(mainInfo["mainClass"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + + +class SimpleMainResolve : SingleFileTestFixture("resolvemain", "Simple.kt") { + @Test + fun `Should resolve correct main class of simple file`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + val mainInfo = result as Map + assertEquals("test.SimpleKt", mainInfo["mainClass"]) + assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + + +class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNameAnnotation.kt") { + @Test + fun `Should resolve correct main class of file annotated with JvmName`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + val mainInfo = result as Map + assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"]) + assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} + +class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "CompanionObject.kt") { + @Test + fun `Should resolve correct main class of main function inside companion object`() { + val root = testResourcesRoot().resolve(workspaceRoot) + val fileUri = root.resolve(file).toUri().toString() + + val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get() + + assertNotNull(result) + val mainInfo = result as Map + assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"]) + assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"]) + assertEquals(root.toString(), mainInfo["projectRoot"]) + } +} diff --git a/server/src/test/resources/resolvemain/CompanionObject.kt b/server/src/test/resources/resolvemain/CompanionObject.kt new file mode 100644 index 000000000..b9a95def1 --- /dev/null +++ b/server/src/test/resources/resolvemain/CompanionObject.kt @@ -0,0 +1,14 @@ +package test.my.companion + +val SOME_GLOBAL_CONSTANT = 42 + +fun multiplyByOne(num: Int) = num*1 + +class SweetPotato { + companion object { + @JvmStatic + fun main() { + println("42 multiplied by 1: ${multiplyByOne(42)}") + } + } +} diff --git a/server/src/test/resources/resolvemain/JvmNameAnnotation.kt b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt new file mode 100644 index 000000000..04c03c434 --- /dev/null +++ b/server/src/test/resources/resolvemain/JvmNameAnnotation.kt @@ -0,0 +1,8 @@ +@JvmName("Potato") +package com.mypackage.name + +val MY_CONSTANT = 1 + +fun main(args: Array) { + +} diff --git a/server/src/test/resources/resolvemain/NoMain.kt b/server/src/test/resources/resolvemain/NoMain.kt new file mode 100644 index 000000000..64eb222b1 --- /dev/null +++ b/server/src/test/resources/resolvemain/NoMain.kt @@ -0,0 +1,3 @@ +package no.main.found.hopefully + +fun multiplyByOne(num: Int) = num*1 diff --git a/server/src/test/resources/resolvemain/Simple.kt b/server/src/test/resources/resolvemain/Simple.kt new file mode 100644 index 000000000..de8be317a --- /dev/null +++ b/server/src/test/resources/resolvemain/Simple.kt @@ -0,0 +1,5 @@ +package test + +fun main() { + println("Hello!") +}