Skip to content

Commit 0781235

Browse files
committed
Cache dependencies
1 parent cfd1717 commit 0781235

File tree

10 files changed

+238
-28
lines changed

10 files changed

+238
-28
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.javacs.kt
33
import org.javacs.kt.classpath.ClassPathEntry
44
import org.javacs.kt.classpath.defaultClassPathResolver
55
import org.javacs.kt.compiler.Compiler
6+
import org.javacs.kt.storage.Storage
67
import org.javacs.kt.util.AsyncExecutor
78
import java.io.Closeable
89
import java.nio.file.FileSystems
@@ -17,6 +18,7 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
1718
private val javaSourcePath = mutableSetOf<Path>()
1819
private val buildScriptClassPath = mutableSetOf<Path>()
1920
val classPath = mutableSetOf<ClassPathEntry>()
21+
var storage: Storage? = null
2022

2123
var compiler = Compiler(javaSourcePath, classPath.map { it.compiledJar }.toSet(), buildScriptClassPath)
2224
private set
@@ -34,7 +36,7 @@ class CompilerClassPath(private val config: CompilerConfiguration) : Closeable {
3436
updateJavaSourcePath: Boolean = true
3537
): Boolean {
3638
// TODO: Fetch class path and build script class path concurrently (and asynchronously)
37-
val resolver = defaultClassPathResolver(workspaceRoots)
39+
val resolver = defaultClassPathResolver(workspaceRoots, storage)
3840
var refreshCompiler = updateJavaSourcePath
3941

4042
if (updateClassPath) {

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ import org.eclipse.lsp4j.services.LanguageClient
77
import org.eclipse.lsp4j.services.LanguageClientAware
88
import org.eclipse.lsp4j.services.LanguageServer
99
import org.javacs.kt.command.ALL_COMMANDS
10-
import org.javacs.kt.externalsources.JarClassContentProvider
10+
import org.javacs.kt.config.ServerConfiguration
11+
import org.javacs.kt.config.parseServerConfiguration
1112
import org.javacs.kt.externalsources.ClassPathSourceJarProvider
13+
import org.javacs.kt.externalsources.JarClassContentProvider
14+
import org.javacs.kt.progress.LanguageClientProgress
15+
import org.javacs.kt.progress.Progress
16+
import org.javacs.kt.semantictokens.semanticTokensLegend
1217
import org.javacs.kt.util.AsyncExecutor
1318
import org.javacs.kt.util.TemporaryDirectory
1419
import org.javacs.kt.util.parseURI
15-
import org.javacs.kt.progress.Progress
16-
import org.javacs.kt.progress.LanguageClientProgress
17-
import org.javacs.kt.semantictokens.semanticTokensLegend
18-
import java.net.URI
1920
import java.io.Closeable
2021
import java.nio.file.Paths
2122
import java.util.concurrent.CompletableFuture
2223
import java.util.concurrent.CompletableFuture.completedFuture
2324

25+
2426
class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
2527
val config = Configuration()
2628
val classPath = CompilerClassPath(config.compiler)
@@ -43,6 +45,8 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
4345
sourcePath.progressFactory = factory
4446
}
4547

48+
private var serverConfiguration: ServerConfiguration? = null
49+
4650
companion object {
4751
val VERSION: String? = System.getProperty("kotlinLanguageServer.version")
4852
}
@@ -89,6 +93,10 @@ class KotlinLanguageServer : LanguageServer, LanguageClientAware, Closeable {
8993
serverCapabilities.documentRangeFormattingProvider = Either.forLeft(true)
9094
serverCapabilities.executeCommandProvider = ExecuteCommandOptions(ALL_COMMANDS)
9195

96+
serverConfiguration = parseServerConfiguration(params)
97+
98+
classPath.storage = serverConfiguration?.storage?.getSlice("classpath")
99+
92100
val clientCapabilities = params.capabilities
93101
config.completion.snippets.enabled = clientCapabilities?.textDocument?.completion?.completionItem?.snippetSupport ?: false
94102

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.javacs.kt.config
2+
3+
import com.google.gson.*
4+
import org.eclipse.lsp4j.InitializeParams
5+
import org.javacs.kt.LOG
6+
import org.javacs.kt.storage.Storage
7+
import java.lang.reflect.Type
8+
import java.nio.file.Files
9+
import java.nio.file.Path
10+
import java.nio.file.Paths
11+
12+
fun parseServerConfiguration(params: InitializeParams): ServerConfiguration? {
13+
val gson = GsonBuilder().registerTypeHierarchyAdapter(Path::class.java, GsonPathConverter()).create()
14+
15+
var storage: Storage? = null
16+
17+
if (params.initializationOptions != null) {
18+
val options = gson.fromJson(params.initializationOptions as JsonElement, InitializationOptions::class.java)
19+
20+
if (options.storagePath != null) {
21+
if (Files.exists(options.storagePath) && Files.isDirectory(options.storagePath)) {
22+
storage = Storage(options.storagePath)
23+
}
24+
}
25+
}
26+
27+
return ServerConfiguration(storage)
28+
}
29+
30+
data class InitializationOptions(val storagePath: Path?)
31+
32+
data class ServerConfiguration(val storage: Storage?)
33+
34+
class GsonPathConverter : JsonDeserializer<Path?> {
35+
36+
@Throws(JsonParseException::class)
37+
override fun deserialize(json: JsonElement, type: Type?, context: JsonDeserializationContext?): Path? {
38+
return try {
39+
Paths.get(json.asString)
40+
} catch (ex: Exception) {
41+
LOG.printStackTrace(ex)
42+
null
43+
}
44+
}
45+
}

server/src/test/kotlin/org/javacs/kt/ClassPathTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ClassPathTest {
2121

2222
assertTrue(Files.exists(buildFile))
2323

24-
val resolvers = defaultClassPathResolver(listOf(workspaceRoot))
24+
val resolvers = defaultClassPathResolver(listOf(workspaceRoot), null)
2525
print(resolvers)
2626
val classPath = resolvers.classpathOrEmpty.map { it.toString() }
2727

shared/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id 'org.jetbrains.kotlin.jvm'
33
id 'maven-publish'
4+
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlinVersion"
45
}
56

67
version = projectVersion
@@ -15,4 +16,5 @@ dependencies {
1516
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
1617
testImplementation 'org.hamcrest:hamcrest-all:1.3'
1718
testImplementation 'junit:junit:4.11'
19+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
1820
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
package org.javacs.kt.classpath
22

3+
import kotlinx.serialization.Serializable
4+
import org.javacs.kt.storage.PathAsStringSerializer
35
import java.nio.file.Path
46

5-
data class ClassPathEntry(val compiledJar: Path, val sourceJar: Path?)
7+
@Serializable
8+
data class ClassPathEntry(
9+
@Serializable(with = PathAsStringSerializer::class)
10+
val compiledJar: Path,
11+
12+
@Serializable(with = PathAsStringSerializer::class)
13+
val sourceJar: Path?
14+
)

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

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

33
import org.javacs.kt.LOG
4+
import org.javacs.kt.storage.Storage
45
import java.nio.file.Path
56
import java.nio.file.PathMatcher
67
import java.nio.file.FileSystems
78

8-
fun defaultClassPathResolver(workspaceRoots: Collection<Path>): ClassPathResolver =
9+
fun defaultClassPathResolver(workspaceRoots: Collection<Path>, storage: Storage?): ClassPathResolver =
910
WithStdlibResolver(
1011
ShellClassPathResolver.global(workspaceRoots.firstOrNull())
11-
.or(workspaceRoots.asSequence().flatMap(::workspaceResolvers).joined)
12+
.or(workspaceRoots.asSequence().flatMap { workspaceResolvers(it, storage) }.joined)
1213
).or(BackupClassPathResolver)
1314

1415
/** Searches the workspace for all files that could provide classpath info. */
15-
private fun workspaceResolvers(workspaceRoot: Path): Sequence<ClassPathResolver> {
16+
private fun workspaceResolvers(workspaceRoot: Path, storage: Storage?): Sequence<ClassPathResolver> {
1617
val ignored: List<PathMatcher> = ignoredPathPatterns(workspaceRoot, workspaceRoot.resolve(".gitignore"))
17-
return folderResolvers(workspaceRoot, ignored).asSequence()
18+
return folderResolvers(workspaceRoot, ignored, storage).asSequence()
1819
}
1920

2021
/** Searches the folder for all build-files. */
21-
private fun folderResolvers(root: Path, ignored: List<PathMatcher>): Collection<ClassPathResolver> =
22+
private fun folderResolvers(root: Path, ignored: List<PathMatcher>, storage: Storage?): Collection<ClassPathResolver> =
2223
root.toFile()
2324
.walk()
2425
.onEnter { file -> ignored.none { it.matches(file.toPath()) } }
25-
.mapNotNull { asClassPathProvider(it.toPath()) }
26+
.mapNotNull { asClassPathProvider(it.toPath(), storage) }
2627
.toList()
2728

2829
/** Tries to read glob patterns from a gitignore. */
@@ -47,7 +48,7 @@ private fun ignoredPathPatterns(root: Path, gitignore: Path): List<PathMatcher>
4748
?: emptyList()
4849

4950
/** Tries to create a classpath resolver from a file using as many sources as possible */
50-
private fun asClassPathProvider(path: Path): ClassPathResolver? =
51-
MavenClassPathResolver.maybeCreate(path)
52-
?: GradleClassPathResolver.maybeCreate(path)
51+
private fun asClassPathProvider(path: Path, storage: Storage?): ClassPathResolver? =
52+
MavenClassPathResolver.maybeCreate(path, storage)
53+
?: GradleClassPathResolver.maybeCreate(path, storage)
5354
?: ShellClassPathResolver.maybeCreate(path)

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

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

33
import org.javacs.kt.LOG
4+
import org.javacs.kt.storage.SetOfPathsAsStringSerializer
5+
import org.javacs.kt.storage.Storage
46
import org.javacs.kt.util.execAndReadStdoutAndStderr
57
import org.javacs.kt.util.KotlinLSException
68
import org.javacs.kt.util.isOSWindows
@@ -10,34 +12,65 @@ import java.nio.file.Files
1012
import java.nio.file.Path
1113
import java.nio.file.Paths
1214

13-
internal class GradleClassPathResolver(private val path: Path, private val includeKotlinDSL: Boolean): ClassPathResolver {
15+
internal class GradleClassPathResolver(private val path: Path, private val includeKotlinDSL: Boolean, private val storage: Storage?): ClassPathResolver {
1416
override val resolverType: String = "Gradle"
15-
private val projectDirectory: Path get() = path.getParent()
17+
private val projectDirectory: Path get() = path.parent
18+
19+
private var cachedClassPath: Set<ClassPathEntry>? = storage?.getObject("gradleClassPath")
20+
private var cachedBuildScriptClassPath: Set<Path>? = storage?.getObject("gradleBuildScriptClassPath", SetOfPathsAsStringSerializer)
21+
1622
override val classpath: Set<ClassPathEntry> get() {
23+
cachedClassPath?.let { if (!dependenciesChanged()) return it }
24+
1725
val scripts = listOf("projectClassPathFinder.gradle")
1826
val tasks = listOf("kotlinLSPProjectDeps")
1927

20-
return readDependenciesViaGradleCLI(projectDirectory, scripts, tasks)
28+
val newClasspath = readDependenciesViaGradleCLI(projectDirectory, scripts, tasks)
2129
.apply { if (isNotEmpty()) LOG.info("Successfully resolved dependencies for '${projectDirectory.fileName}' using Gradle") }
2230
.map { ClassPathEntry(it, null) }.toSet()
31+
32+
updateClasspathCache(newClasspath)
33+
return newClasspath
2334
}
2435
override val buildScriptClasspath: Set<Path> get() {
2536
return if (includeKotlinDSL) {
37+
cachedBuildScriptClassPath?.let { if (!dependenciesChanged()) return it }
38+
2639
val scripts = listOf("kotlinDSLClassPathFinder.gradle")
2740
val tasks = listOf("kotlinLSPKotlinDSLDeps")
2841

29-
return readDependenciesViaGradleCLI(projectDirectory, scripts, tasks)
42+
val newBuildScriptClasspath = readDependenciesViaGradleCLI(projectDirectory, scripts, tasks)
3043
.apply { if (isNotEmpty()) LOG.info("Successfully resolved build script dependencies for '${projectDirectory.fileName}' using Gradle") }
44+
45+
updateBuildScriptClasspathCache(newBuildScriptClasspath)
46+
return newBuildScriptClasspath
3147
} else {
3248
emptySet()
3349
}
3450
}
3551

52+
private fun updateClasspathCache(newClasspath: Set<ClassPathEntry>) {
53+
storage?.setObject("gradleClasspath", newClasspath)
54+
storage?.setObject("buildScriptFileVersion", getCurrentBuildScriptFileVersion())
55+
cachedClassPath = newClasspath
56+
}
57+
58+
private fun updateBuildScriptClasspathCache(newClasspath: Set<Path>) {
59+
storage?.setObject("gradleBuildScriptClasspath", newClasspath, SetOfPathsAsStringSerializer)
60+
storage?.setObject("buildScriptFileVersion", getCurrentBuildScriptFileVersion())
61+
cachedBuildScriptClassPath = newClasspath
62+
}
63+
64+
private fun dependenciesChanged(): Boolean =
65+
storage?.getObject<Long>("buildScriptFileVersion") ?: 0 < getCurrentBuildScriptFileVersion()
66+
67+
private fun getCurrentBuildScriptFileVersion(): Long = path.toFile().lastModified()
68+
3669
companion object {
3770
/** Create a Gradle resolver if a file is a pom. */
38-
fun maybeCreate(file: Path): GradleClassPathResolver? =
71+
fun maybeCreate(file: Path, storage: Storage?): GradleClassPathResolver? =
3972
file.takeIf { file.endsWith("build.gradle") || file.endsWith("build.gradle.kts") }
40-
?.let { GradleClassPathResolver(it, includeKotlinDSL = file.toString().endsWith(".kts")) }
73+
?.let { GradleClassPathResolver(it, includeKotlinDSL = file.toString().endsWith(".kts"), storage) }
4174
}
4275
}
4376

@@ -84,6 +117,7 @@ private fun readDependenciesViaGradleCLI(projectDirectory: Path, gradleScripts:
84117
.toSet()
85118

86119
tmpScripts.forEach(Files::delete)
120+
87121
return dependencies
88122
}
89123

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package org.javacs.kt.classpath
22

3+
import kotlinx.serialization.Serializable
34
import org.javacs.kt.LOG
5+
import org.javacs.kt.storage.Storage
46
import org.javacs.kt.util.findCommandOnPath
57
import org.javacs.kt.util.execAndReadStdoutAndStderr
68
import java.nio.file.Path
79
import java.nio.file.Files
810
import java.io.File
911

1012
/** Resolver for reading maven dependencies */
11-
internal class MavenClassPathResolver private constructor(private val pom: Path) : ClassPathResolver {
13+
internal class MavenClassPathResolver private constructor(private val pom: Path, private val storage: Storage?) : ClassPathResolver {
1214
private var artifacts: Set<Artifact>? = null
1315

1416
override val resolverType: String = "Maven"
17+
18+
private var cachedClassPath: MavenClasspathCache? = storage?.getObject("mavenClasspath")
19+
1520
override val classpath: Set<ClassPathEntry> get() {
21+
cachedClassPath?.let { if (!dependenciesChanged()) return it.classpathEntries }
22+
1623
val dependenciesOutput = generateMavenDependencyList(pom)
1724
val artifacts = readMavenDependencyList(dependenciesOutput)
1825

@@ -25,10 +32,17 @@ internal class MavenClassPathResolver private constructor(private val pom: Path)
2532
Files.deleteIfExists(dependenciesOutput)
2633

2734
this.artifacts = artifacts
28-
return artifacts.mapNotNull { findMavenArtifact(it, false)?.let { it1 -> ClassPathEntry(it1, null) } }.toSet()
35+
36+
val newClasspath = artifacts.mapNotNull { findMavenArtifact(it, false)?.let { it1 -> ClassPathEntry(it1, null) } }.toSet()
37+
38+
updateClasspathCache(MavenClasspathCache(newClasspath, false))
39+
40+
return newClasspath
2941
}
3042

3143
override val classpathWithSources: Set<ClassPathEntry> get() {
44+
cachedClassPath?.let { if (!dependenciesChanged() && it.includesSources) return it.classpathEntries }
45+
3246
// Fetch artifacts if not yet present.
3347
var artifacts: Set<Artifact>
3448
if (this.artifacts != null) {
@@ -45,17 +59,33 @@ internal class MavenClassPathResolver private constructor(private val pom: Path)
4559
artifacts = readMavenDependencyListWithSources(artifacts, sourcesOutput)
4660

4761
Files.deleteIfExists(sourcesOutput)
48-
return artifacts.mapNotNull {
62+
val newClasspath = artifacts.mapNotNull {
4963
findMavenArtifact(it, false)?.let {
5064
it1 -> ClassPathEntry(it1, if (it.source) findMavenArtifact(it, it.source) else null)
5165
}
5266
}.toSet()
67+
68+
updateClasspathCache(MavenClasspathCache(newClasspath, true))
69+
70+
return newClasspath
71+
}
72+
73+
private fun updateClasspathCache(newClasspathCache: MavenClasspathCache) {
74+
storage?.setObject("mavenClasspath", newClasspathCache)
75+
storage?.setObject("mavenPomFileVersion", getCurrentPomFileVersion())
76+
cachedClassPath = newClasspathCache
5377
}
5478

79+
private fun dependenciesChanged(): Boolean {
80+
return storage?.getObject<Long>("mavenPomFileVersion") ?: 0 < getCurrentPomFileVersion()
81+
}
82+
83+
private fun getCurrentPomFileVersion(): Long = pom.toFile().lastModified()
84+
5585
companion object {
5686
/** Create a maven resolver if a file is a pom. */
57-
fun maybeCreate(file: Path): MavenClassPathResolver? =
58-
file.takeIf { it.endsWith("pom.xml") }?.let { MavenClassPathResolver(it) }
87+
fun maybeCreate(file: Path, storage: Storage?): MavenClassPathResolver? =
88+
file.takeIf { it.endsWith("pom.xml") }?.let { MavenClassPathResolver(it, storage) }
5989
}
6090
}
6191

@@ -201,3 +231,9 @@ data class Artifact(
201231
) {
202232
override fun toString() = "$group:$artifact:$version"
203233
}
234+
235+
@Serializable
236+
private data class MavenClasspathCache(
237+
val classpathEntries: Set<ClassPathEntry>,
238+
val includesSources: Boolean
239+
)

0 commit comments

Comments
 (0)