Skip to content

Commit ad435bb

Browse files
committed
Merge pull request #345
Method for resolving main class - used for run/debug code lenses in editors.
2 parents 5dab995 + d9d7b6a commit ad435bb

File tree

12 files changed

+200
-5
lines changed

12 files changed

+200
-5
lines changed

server/src/main/kotlin/org/javacs/kt/CompilerClassPath.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import java.nio.file.Path
1515
* and the compiler. Note that Kotlin sources are stored in SourcePath.
1616
*/
1717
class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
18-
private val workspaceRoots = mutableSetOf<Path>()
18+
val workspaceRoots = mutableSetOf<Path>()
19+
1920
private val javaSourcePath = mutableSetOf<Path>()
2021
private val buildScriptClassPath = mutableSetOf<Path>()
2122
val classPath = mutableSetOf<ClassPathEntry>()

server/src/main/kotlin/org/javacs/kt/KotlinLanguageServer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
3030

3131
private val textDocuments = KotlinTextDocumentService(sourceFiles, sourcePath, config, tempDirectory, uriContentProvider, classPath)
3232
private val workspaces = KotlinWorkspaceService(sourceFiles, sourcePath, classPath, textDocuments, config)
33-
private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath)
33+
private val protocolExtensions = KotlinProtocolExtensionService(uriContentProvider, classPath, sourcePath)
3434

3535
private lateinit var client: LanguageClient
3636

server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensionService.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package org.javacs.kt
33
import org.eclipse.lsp4j.*
44
import org.javacs.kt.util.AsyncExecutor
55
import org.javacs.kt.util.parseURI
6+
import org.javacs.kt.resolve.resolveMain
67
import java.util.concurrent.CompletableFuture
8+
import java.nio.file.Paths
79

810
class KotlinProtocolExtensionService(
911
private val uriContentProvider: URIContentProvider,
10-
private val cp: CompilerClassPath
12+
private val cp: CompilerClassPath,
13+
private val sp: SourcePath
1114
) : KotlinProtocolExtensions {
1215
private val async = AsyncExecutor()
1316

@@ -18,4 +21,22 @@ class KotlinProtocolExtensionService(
1821
override fun buildOutputLocation(): CompletableFuture<String?> = async.compute {
1922
cp.outputDirectory.absolutePath
2023
}
24+
25+
override fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture<Map<String, Any?>> = async.compute {
26+
val fileUri = parseURI(textDocument.uri)
27+
val filePath = Paths.get(fileUri)
28+
29+
// we find the longest one in case both the root and submodule are included
30+
val workspacePath = cp.workspaceRoots.filter {
31+
filePath.startsWith(it)
32+
}.map {
33+
it.toString()
34+
}.maxByOrNull(String::length) ?: ""
35+
36+
val compiledFile = sp.currentVersion(fileUri)
37+
38+
resolveMain(compiledFile) + mapOf(
39+
"projectRoot" to workspacePath
40+
)
41+
}
2142
}

server/src/main/kotlin/org/javacs/kt/KotlinProtocolExtensions.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ interface KotlinProtocolExtensions {
1212

1313
@JsonRequest
1414
fun buildOutputLocation(): CompletableFuture<String?>
15+
16+
@JsonRequest
17+
fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture<Map<String, Any?>>
1518
}

server/src/main/kotlin/org/javacs/kt/KotlinWorkspaceService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.javacs.kt.KotlinTextDocumentService
1313
import org.javacs.kt.position.extractRange
1414
import org.javacs.kt.util.filePath
1515
import org.javacs.kt.util.parseURI
16+
import org.javacs.kt.resolve.resolveMain
1617
import java.net.URI
1718
import java.nio.file.Paths
1819
import java.util.concurrent.CompletableFuture
@@ -29,7 +30,7 @@ class KotlinWorkspaceService(
2930
) : WorkspaceService, LanguageClientAware {
3031
private val gson = Gson()
3132
private var languageClient: LanguageClient? = null
32-
33+
3334
override fun connect(client: LanguageClient): Unit {
3435
languageClient = client
3536
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.javacs.kt.command
22

33
const val JAVA_TO_KOTLIN_COMMAND = "convertJavaToKotlin"
4+
45
val ALL_COMMANDS = listOf(
5-
JAVA_TO_KOTLIN_COMMAND
6+
JAVA_TO_KOTLIN_COMMAND,
67
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.javacs.kt.resolve
2+
3+
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
4+
import org.jetbrains.kotlin.psi.KtFile
5+
import org.jetbrains.kotlin.psi.KtNamedFunction
6+
import org.jetbrains.kotlin.psi.KtClass
7+
import org.jetbrains.kotlin.psi.KtObjectDeclaration
8+
import org.javacs.kt.CompiledFile
9+
import org.javacs.kt.position.range
10+
import org.javacs.kt.util.partitionAroundLast
11+
import com.intellij.openapi.util.TextRange
12+
13+
14+
fun resolveMain(file: CompiledFile): Map<String,Any> {
15+
val parsedFile = file.parse.copy() as KtFile
16+
17+
val mainFunction = findTopLevelMainFunction(parsedFile)
18+
if(null != mainFunction) {
19+
// the KtFiles name is weird. Full path. This causes the class to have full path in name as well. Correcting to top level only
20+
parsedFile.name = parsedFile.name.partitionAroundLast("/").second.substring(1)
21+
22+
return mapOf("mainClass" to JvmFileClassUtil.getFileClassInfoNoResolve(parsedFile).facadeClassFqName.asString(),
23+
"range" to range(file.content, mainFunction.second))
24+
}
25+
26+
val companionMain = findCompanionObjectMain(parsedFile)
27+
if(null != companionMain) {
28+
return mapOf(
29+
"mainClass" to (companionMain.first ?: ""),
30+
"range" to range(file.content, companionMain.second)
31+
)
32+
}
33+
34+
return emptyMap()
35+
}
36+
37+
// only one main method allowed top level in a file (so invalid syntax files will not show any main methods)
38+
private fun findTopLevelMainFunction(file: KtFile): Pair<String?, TextRange>? = file.declarations.find {
39+
it is KtNamedFunction && "main" == it.name
40+
}?.let {
41+
Pair(it.name, it.textRangeInParent)
42+
}
43+
44+
// finds a top level class that contains a companion object with a main function inside
45+
private fun findCompanionObjectMain(file: KtFile): Pair<String?, TextRange>? = file.declarations.flatMap { topLevelDeclaration ->
46+
if(topLevelDeclaration is KtClass) {
47+
topLevelDeclaration.companionObjects
48+
} else {
49+
emptyList<KtObjectDeclaration>()
50+
}
51+
}.flatMap { companionObject ->
52+
companionObject.body?.children?.toList() ?: emptyList()
53+
}.mapNotNull { companionObjectInternal ->
54+
if(companionObjectInternal is KtNamedFunction && "main" == companionObjectInternal.name && companionObjectInternal.text.startsWith("@JvmStatic")) {
55+
companionObjectInternal
56+
} else {
57+
null
58+
}
59+
}.firstOrNull()?.let {
60+
// 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)
61+
Pair((it.parent.parent.parent.parent as KtClass).fqName?.toString(), it.textRange)
62+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.javacs.kt
2+
3+
import com.google.gson.Gson
4+
import org.eclipse.lsp4j.ExecuteCommandParams
5+
import org.eclipse.lsp4j.Position
6+
import org.eclipse.lsp4j.Range
7+
import org.eclipse.lsp4j.TextDocumentIdentifier
8+
import org.junit.Test
9+
import org.junit.Assert.assertEquals
10+
import org.junit.Assert.assertNotNull
11+
import org.junit.Assert.assertNull
12+
13+
class NoMainResolve : SingleFileTestFixture("resolvemain", "NoMain.kt") {
14+
@Test
15+
fun `Should not find any main class info`() {
16+
val root = testResourcesRoot().resolve(workspaceRoot)
17+
val fileUri = root.resolve(file).toUri().toString()
18+
19+
val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()
20+
21+
assertNotNull(result)
22+
val mainInfo = result as Map<String, String>
23+
assertNull(mainInfo["mainClass"])
24+
assertEquals(root.toString(), mainInfo["projectRoot"])
25+
}
26+
}
27+
28+
29+
class SimpleMainResolve : SingleFileTestFixture("resolvemain", "Simple.kt") {
30+
@Test
31+
fun `Should resolve correct main class of simple file`() {
32+
val root = testResourcesRoot().resolve(workspaceRoot)
33+
val fileUri = root.resolve(file).toUri().toString()
34+
35+
val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()
36+
37+
assertNotNull(result)
38+
val mainInfo = result as Map<String, Any>
39+
assertEquals("test.SimpleKt", mainInfo["mainClass"])
40+
assertEquals(Range(Position(2, 0), Position(4, 1)), mainInfo["range"])
41+
assertEquals(root.toString(), mainInfo["projectRoot"])
42+
}
43+
}
44+
45+
46+
class JvmNameAnnotationMainResolve : SingleFileTestFixture("resolvemain", "JvmNameAnnotation.kt") {
47+
@Test
48+
fun `Should resolve correct main class of file annotated with JvmName`() {
49+
val root = testResourcesRoot().resolve(workspaceRoot)
50+
val fileUri = root.resolve(file).toUri().toString()
51+
52+
val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()
53+
54+
assertNotNull(result)
55+
val mainInfo = result as Map<String, Any>
56+
assertEquals("com.mypackage.name.Potato", mainInfo["mainClass"])
57+
assertEquals(Range(Position(5, 0), Position(7, 1)), mainInfo["range"])
58+
assertEquals(root.toString(), mainInfo["projectRoot"])
59+
}
60+
}
61+
62+
class CompanionObjectMainResolve : SingleFileTestFixture("resolvemain", "CompanionObject.kt") {
63+
@Test
64+
fun `Should resolve correct main class of main function inside companion object`() {
65+
val root = testResourcesRoot().resolve(workspaceRoot)
66+
val fileUri = root.resolve(file).toUri().toString()
67+
68+
val result = languageServer.getProtocolExtensionService().mainClass(TextDocumentIdentifier(fileUri)).get()
69+
70+
assertNotNull(result)
71+
val mainInfo = result as Map<String, Any>
72+
assertEquals("test.my.companion.SweetPotato", mainInfo["mainClass"])
73+
assertEquals(Range(Position(8, 8), Position(11, 9)), mainInfo["range"])
74+
assertEquals(root.toString(), mainInfo["projectRoot"])
75+
}
76+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package test.my.companion
2+
3+
val SOME_GLOBAL_CONSTANT = 42
4+
5+
fun multiplyByOne(num: Int) = num*1
6+
7+
class SweetPotato {
8+
companion object {
9+
@JvmStatic
10+
fun main() {
11+
println("42 multiplied by 1: ${multiplyByOne(42)}")
12+
}
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@JvmName("Potato")
2+
package com.mypackage.name
3+
4+
val MY_CONSTANT = 1
5+
6+
fun main(args: Array<String>) {
7+
8+
}

0 commit comments

Comments
 (0)