Skip to content

Commit 323e6f0

Browse files
committed
Added support for source JARs with Maven
1 parent 57157f0 commit 323e6f0

File tree

14 files changed

+200
-64
lines changed

14 files changed

+200
-64
lines changed

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.javacs.kt
22

3+
import org.javacs.kt.classpath.ClassPathEntry
34
import org.javacs.kt.classpath.defaultClassPathResolver
45
import org.javacs.kt.compiler.Compiler
6+
import org.javacs.kt.util.AsyncExecutor
57
import java.io.Closeable
6-
import java.nio.file.Files
78
import java.nio.file.FileSystems
89
import java.nio.file.Path
910

@@ -14,10 +15,11 @@ import java.nio.file.Path
1415
class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
1516
private val workspaceRoots = mutableSetOf<Path>()
1617
private val javaSourcePath = mutableSetOf<Path>()
17-
private val classPath = mutableSetOf<Path>()
18+
val classPath = mutableSetOf<ClassPathEntry>()
1819
private val buildScriptClassPath = mutableSetOf<Path>()
19-
var compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath)
20+
var compiler = Compiler(javaSourcePath, classPath.map { it.compiledJar }.toSet(), buildScriptClassPath)
2021
private set
22+
private val async = AsyncExecutor()
2123

2224
init {
2325
compiler.updateConfiguration(config)
@@ -36,9 +38,18 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
3638
if (updateClassPath) {
3739
val newClassPath = resolver.classpathOrEmpty
3840
if (newClassPath != classPath) {
39-
syncPaths(classPath, newClassPath, "class path")
41+
synchronized(classPath) {
42+
syncClassPathEntries(classPath, newClassPath, "class path")
43+
}
4044
refreshCompiler = true
4145
}
46+
47+
async.compute {
48+
val newClassPathWithSources = resolver.fetchClasspathWithSources()
49+
synchronized(classPath) {
50+
syncClassPathEntries(classPath, newClassPathWithSources, "class path")
51+
}
52+
}
4253
}
4354

4455
if (updateBuildScriptClassPath) {
@@ -53,13 +64,25 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
5364
if (refreshCompiler) {
5465
LOG.info("Reinstantiating compiler")
5566
compiler.close()
56-
compiler = Compiler(javaSourcePath, classPath, buildScriptClassPath)
67+
compiler = Compiler(javaSourcePath, classPath.map { it.compiledJar }.toSet(), buildScriptClassPath)
5768
updateCompilerConfiguration()
5869
}
5970

6071
return refreshCompiler
6172
}
6273

74+
/** Synchronizes the given two class path entry sets and logs the differences. */
75+
private fun syncClassPathEntries(dest: MutableSet<ClassPathEntry>, new: Set<ClassPathEntry>, name: String) {
76+
val added = new - dest
77+
val removed = dest - new
78+
79+
logAdded(added.map { it.compiledJar }, name)
80+
logRemoved(removed.map { it.compiledJar }, name)
81+
82+
dest.removeAll(removed)
83+
dest.addAll(added)
84+
}
85+
6386
/** Synchronizes the given two path sets and logs the differences. */
6487
private fun syncPaths(dest: MutableSet<Path>, new: Set<Path>, name: String) {
6588
val added = new - dest

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
2828
val sourcePath = SourcePath(classPath, uriContentProvider, config.indexing)
2929
val sourceFiles = SourceFiles(sourcePath, uriContentProvider)
3030

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

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class KotlinTextDocumentService(
3838
private val sp: SourcePath,
3939
private val config: Configuration,
4040
private val tempDirectory: TemporaryDirectory,
41-
private val uriContentProvider: URIContentProvider
41+
private val uriContentProvider: URIContentProvider,
42+
private val classPath: CompilerClassPath
4243
) : TextDocumentService, Closeable {
4344
private lateinit var client: LanguageClient
4445
private val async = AsyncExecutor()
@@ -124,7 +125,7 @@ class KotlinTextDocumentService(
124125
LOG.info("Go-to-definition at {}", describePosition(position))
125126

126127
val (file, cursor) = recover(position, Recompile.NEVER)
127-
goToDefinition(file, cursor, uriContentProvider.jarClassContentProvider, tempDirectory, config.externalSources)
128+
goToDefinition(file, cursor, uriContentProvider.jarClassContentProvider, tempDirectory, config.externalSources, classPath.classPath)
128129
?.let(::listOf)
129130
?.let { Either.forLeft<List<Location>, List<LocationLink>>(it) }
130131
?: noResult("Couldn't find definition at ${describePosition(position)}", Either.forLeft(emptyList()))

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import java.nio.file.Paths
77
import org.javacs.kt.externalsources.JarClassContentProvider
88
import org.javacs.kt.externalsources.toKlsURI
99
import org.javacs.kt.util.KotlinLSException
10+
import org.javacs.kt.util.partitionAroundLast
1011

1112
/**
1213
* Fetches the content of Kotlin files identified by a URI.
@@ -16,7 +17,7 @@ class URIContentProvider(
1617
) {
1718
fun contentOf(uri: URI): String = when (uri.scheme) {
1819
"file" -> Paths.get(uri).toFile().readText()
19-
"kls" -> uri.toKlsURI()?.let { jarClassContentProvider.contentOf(it).second }
20+
"kls" -> uri.toKlsURI()?.let { jarClassContentProvider.contentOf(it, it.source).second }
2021
?: throw KotlinLSException("Could not find ${uri}")
2122
else -> throw KotlinLSException("Unrecognized scheme ${uri.scheme}")
2223
}

server/src/main/kotlin/org/javacs/kt/definition/GoToDefinition.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import java.nio.file.Files
88
import org.javacs.kt.CompiledFile
99
import org.javacs.kt.LOG
1010
import org.javacs.kt.ExternalSourcesConfiguration
11+
import org.javacs.kt.classpath.ClassPathEntry
1112
import org.javacs.kt.externalsources.JarClassContentProvider
1213
import org.javacs.kt.externalsources.toKlsURI
1314
import org.javacs.kt.externalsources.KlsURI
@@ -20,6 +21,9 @@ import org.javacs.kt.util.parseURI
2021
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
2122
import org.jetbrains.kotlin.psi.KtNamedDeclaration
2223
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
24+
import org.jetbrains.kotlin.js.parser.parse
25+
import org.jetbrains.kotlin.resolve.descriptorUtil.module
26+
import java.nio.file.Paths
2327

2428
private val cachedTempFiles = mutableMapOf<KlsURI, Path>()
2529
private val definitionPattern = Regex("(?:class|interface|object|fun)\\s+(\\w+)")
@@ -29,7 +33,8 @@ fun goToDefinition(
2933
cursor: Int,
3034
jarClassContentProvider: JarClassContentProvider,
3135
tempDir: TemporaryDirectory,
32-
config: ExternalSourcesConfiguration
36+
config: ExternalSourcesConfiguration,
37+
compilerClassPath: Set<ClassPathEntry>
3338
): Location? {
3439
val (_, target) = file.referenceAtPoint(cursor) ?: return null
3540

@@ -45,8 +50,10 @@ fun goToDefinition(
4550
val rawClassURI = destination.uri
4651

4752
if (isInsideJar(rawClassURI)) {
48-
parseURI(rawClassURI).toKlsURI()?.let { klsURI ->
49-
val (klsSourceURI, content) = jarClassContentProvider.contentOf(klsURI)
53+
val sourceURI = getSourceURI(rawClassURI, compilerClassPath)
54+
val actualClassURI = sourceURI ?: rawClassURI
55+
parseURI(actualClassURI).toKlsURI()?.let { klsURI ->
56+
val (klsSourceURI, content) = jarClassContentProvider.contentOf(klsURI, sourceURI != null)
5057

5158
if (config.useKlsScheme) {
5259
// Defer decompilation until a jarClassContents request is sent
@@ -86,4 +93,13 @@ fun goToDefinition(
8693
return destination
8794
}
8895

96+
private fun getSourceURI(rawClassURI: String, compilerClassPath: Set<ClassPathEntry>): String? {
97+
val rawClassPath = parseURI(rawClassURI).path.toString()
98+
val jarUri = rawClassPath.substring(0, rawClassPath.indexOf("!"))
99+
val classPartUri = rawClassPath.substring(rawClassPath.indexOf("!"))
100+
val sourceJar = compilerClassPath.find { it.compiledJar.toUri().path == jarUri }?.sourceJar
101+
102+
return if (sourceJar != null) sourceJar.toUri().toString() + classPartUri else null
103+
}
104+
89105
private fun isInsideJar(uri: String) = uri.contains(".jar!")

server/src/main/kotlin/org/javacs/kt/externalsources/JarClassContentProvider.kt

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,30 @@ class JarClassContentProvider(
2222
private val tempDir: TemporaryDirectory,
2323
private val decompiler: Decompiler = FernflowerDecompiler()
2424
) {
25-
/** Maps recently used (source-)KLS-URIs to their source contents (e.g. decompiled code). */
26-
private val cachedContents = object : LinkedHashMap<String, String>() {
27-
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, String>) = size > 5
25+
/** Maps recently used (source-)KLS-URIs to their source contents (e.g. decompiled code) and the file extension. */
26+
private val cachedContents = object : LinkedHashMap<String, Pair<String, String>>() {
27+
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Pair<String, String>>) = size > 5
2828
}
2929

3030
/**
3131
* Fetches the contents of a compiled class/source file in a JAR
3232
* and another URI which can be used to refer to these extracted
3333
* contents.
34+
* If the file is inside a source JAR, the source code is returned as is.
3435
*/
35-
public fun contentOf(uri: KlsURI): Pair<KlsURI, String> {
36-
val sourceURI = uri.withFileExtension(if (config.autoConvertToKotlin) "kt" else "java")
37-
val key = sourceURI.toString()
38-
val contents: String = cachedContents[key] ?: run {
39-
LOG.info("Finding contents of {}", describeURI(uri.uri))
40-
tryReadContentOf(uri)
41-
?: tryReadContentOf(uri.withFileExtension("class"))
42-
?: tryReadContentOf(uri.withFileExtension("java"))
43-
?: tryReadContentOf(uri.withFileExtension("kt"))
44-
?: throw KotlinLSException("Could not find $uri")
45-
}.also { cachedContents[key] = it }
36+
public fun contentOf(uri: KlsURI, source: Boolean): Pair<KlsURI, String> {
37+
val key = uri.toString()
38+
val (contents, extension) = cachedContents[key] ?: run {
39+
LOG.info("Finding contents of {}", describeURI(uri.uri))
40+
tryReadContentOf(uri, source)
41+
?: tryReadContentOf(uri.withFileExtension("class"), source)
42+
?: tryReadContentOf(uri.withFileExtension("java"), source)
43+
?: tryReadContentOf(uri.withFileExtension("kt"), source)
44+
?: throw KotlinLSException("Could not find $uri")
45+
}.also {
46+
cachedContents[key] = Pair(it.first, it.second)
47+
}
48+
val sourceURI = uri.withFileExtension(extension).withSource(source)
4649
return Pair(sourceURI, contents)
4750
}
4851

@@ -52,16 +55,17 @@ class JarClassContentProvider(
5255
javaCode
5356
}
5457

55-
private fun tryReadContentOf(uri: KlsURI): String? = try {
56-
when (uri.fileExtension) {
57-
"class" -> uri.extractToTemporaryFile(tempDir)
58+
private fun tryReadContentOf(uri: KlsURI, source: Boolean): Pair<String, String>? = try {
59+
val actualUri = uri.withoutQuery()
60+
when (actualUri.fileExtension) {
61+
"class" -> Pair(actualUri.extractToTemporaryFile(tempDir)
5862
.let(decompiler::decompileClass)
5963
.let { Files.newInputStream(it) }
6064
.bufferedReader()
6165
.use(BufferedReader::readText)
62-
.let(this::convertToKotlinIfNeeded)
63-
"java" -> convertToKotlinIfNeeded(uri.readContents())
64-
else -> uri.readContents() // e.g. for Kotlin source files
66+
.let(this::convertToKotlinIfNeeded), if (config.autoConvertToKotlin) "kt" else "java")
67+
"java" -> if (source) Pair(actualUri.readContents(), "java") else Pair(convertToKotlinIfNeeded(actualUri.readContents()), "kt")
68+
else -> Pair(actualUri.readContents(), "kt") // e.g. for Kotlin source files
6569
}
6670
} catch (e: FileNotFoundException) { null }
6771
}

server/src/main/kotlin/org/javacs/kt/externalsources/KlsURI.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,28 @@ data class KlsURI(val uri: URI) {
3737
.split(".")
3838
.takeIf { it.size > 1 }
3939
?.lastOrNull()
40+
val source: Boolean get() = uri.schemeSpecificPart.split("?").getOrNull(1)?.split("&")?.find {
41+
it.matches("source=(true|false)".toRegex())
42+
}?.split("=")?.get(1)?.toBoolean() ?: false
4043
val isCompiled: Boolean
4144
get() = fileExtension == "class"
4245

4346
fun withFileExtension(newExtension: String): KlsURI {
44-
val (parentURI, fileName) = uri.toString().partitionAroundLast("/")
45-
val newURI = "$parentURI${fileName.split(".").first()}.$newExtension"
47+
val (parentURI, fileNamePlusQuery) = uri.toString().partitionAroundLast("/")
48+
val (fileName, query) = if (fileNamePlusQuery.contains("?")) fileNamePlusQuery.partitionAroundLast("?") else Pair(fileNamePlusQuery, "")
49+
val newURI = "$parentURI${fileName.split(".").first()}.$newExtension$query"
4650
return KlsURI(URI(newURI))
4751
}
4852

49-
private fun toFileURI(): URI = URI(uri.schemeSpecificPart)
53+
fun withSource(source: Boolean): KlsURI {
54+
return KlsURI(URI("${uri.toString()}?source=$source"))
55+
}
56+
57+
fun withoutQuery(): KlsURI {
58+
return KlsURI(URI(uri.toString().split("?")[0]))
59+
}
60+
61+
fun toFileURI(): URI = URI(uri.schemeSpecificPart)
5062

5163
private fun toJarURL(): URL = URL("jar:${uri.schemeSpecificPart}")
5264

shared/src/main/kotlin/org/javacs/kt/classpath/BackupClassPathResolver.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import java.nio.file.Paths
1212
/** Backup classpath that find Kotlin in the user's Maven/Gradle home or kotlinc's libraries folder. */
1313
object BackupClassPathResolver : ClassPathResolver {
1414
override val resolverType: String = "Backup"
15-
override val classpath: Set<Path> get() = findKotlinStdlib()?.let { setOf(it) }.orEmpty()
15+
override val classpath: Set<ClassPathEntry> get() = findKotlinStdlib()?.let { setOf(it) }.orEmpty().map { ClassPathEntry(it, null) }.toSet()
1616
}
1717

1818
fun findKotlinStdlib(): Path? =

shared/src/main/kotlin/org/javacs/kt/classpath/ClassPathResolver.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ package org.javacs.kt.classpath
33
import org.javacs.kt.LOG
44
import java.nio.file.Path
55

6+
data class ClassPathEntry(val compiledJar: Path, val sourceJar: Path?)
7+
68
/** A source for creating class paths */
79
interface ClassPathResolver {
810
val resolverType: String
911

10-
val classpath: Set<Path> // may throw exceptions
11-
val classpathOrEmpty: Set<Path> // does not throw exceptions
12+
val classpath: Set<ClassPathEntry> // may throw exceptions
13+
val classpathOrEmpty: Set<ClassPathEntry> // does not throw exceptions
1214
get() = try {
1315
classpath
1416
} catch (e: Exception) {
1517
LOG.warn("Could not resolve classpath using {}: {}", resolverType, e.message)
16-
emptySet<Path>()
18+
emptySet<ClassPathEntry>()
1719
}
1820

1921
val buildScriptClasspath: Set<Path>
@@ -26,11 +28,13 @@ interface ClassPathResolver {
2628
emptySet<Path>()
2729
}
2830

31+
fun fetchClasspathWithSources(): Set<ClassPathEntry> = classpath
32+
2933
companion object {
3034
/** A default empty classpath implementation */
3135
val empty = object : ClassPathResolver {
3236
override val resolverType = "[]"
33-
override val classpath = emptySet<Path>()
37+
override val classpath = emptySet<ClassPathEntry>()
3438
}
3539
}
3640
}
@@ -52,6 +56,7 @@ internal class UnionClassPathResolver(val lhs: ClassPathResolver, val rhs: Class
5256
override val classpathOrEmpty get() = lhs.classpathOrEmpty + rhs.classpathOrEmpty
5357
override val buildScriptClasspath get() = lhs.buildScriptClasspath + rhs.buildScriptClasspath
5458
override val buildScriptClasspathOrEmpty get() = lhs.buildScriptClasspathOrEmpty + rhs.buildScriptClasspathOrEmpty
59+
override fun fetchClasspathWithSources() = lhs.fetchClasspathWithSources() + rhs.fetchClasspathWithSources()
5560
}
5661

5762
internal class FirstNonEmptyClassPathResolver(val lhs: ClassPathResolver, val rhs: ClassPathResolver) : ClassPathResolver {
@@ -60,4 +65,5 @@ internal class FirstNonEmptyClassPathResolver(val lhs: ClassPathResolver, val rh
6065
override val classpathOrEmpty get() = lhs.classpathOrEmpty.takeIf { it.isNotEmpty() } ?: rhs.classpathOrEmpty
6166
override val buildScriptClasspath get() = lhs.buildScriptClasspath.takeIf { it.isNotEmpty() } ?: rhs.buildScriptClasspath
6267
override val buildScriptClasspathOrEmpty get() = lhs.buildScriptClasspathOrEmpty.takeIf { it.isNotEmpty() } ?: rhs.buildScriptClasspathOrEmpty
68+
override fun fetchClasspathWithSources() = lhs.fetchClasspathWithSources().takeIf { it.isNotEmpty() } ?: rhs.fetchClasspathWithSources()
6369
}

shared/src/main/kotlin/org/javacs/kt/classpath/GradleClassPathResolver.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.javacs.kt.classpath
22

33
import org.javacs.kt.LOG
4-
import org.javacs.kt.util.firstNonNull
5-
import org.javacs.kt.util.tryResolving
64
import org.javacs.kt.util.execAndReadStdoutAndStderr
75
import org.javacs.kt.util.KotlinLSException
86
import org.javacs.kt.util.isOSWindows
@@ -15,12 +13,13 @@ import java.nio.file.Paths
1513
internal class GradleClassPathResolver(private val path: Path, private val includeKotlinDSL: Boolean): ClassPathResolver {
1614
override val resolverType: String = "Gradle"
1715
private val projectDirectory: Path get() = path.getParent()
18-
override val classpath: Set<Path> get() {
16+
override val classpath: Set<ClassPathEntry> get() {
1917
val scripts = listOf("projectClassPathFinder.gradle")
2018
val tasks = listOf("kotlinLSPProjectDeps")
2119

2220
return readDependenciesViaGradleCLI(projectDirectory, scripts, tasks)
2321
.apply { if (isNotEmpty()) LOG.info("Successfully resolved dependencies for '${projectDirectory.fileName}' using Gradle") }
22+
.map { ClassPathEntry(it, null) }.toSet()
2423
}
2524
override val buildScriptClasspath: Set<Path> get() {
2625
return if (includeKotlinDSL) {

0 commit comments

Comments
 (0)