Skip to content

Commit e2e3dc0

Browse files
committed
Setup incremental indexing on file changes
1 parent 8e52cc0 commit e2e3dc0

File tree

2 files changed

+194
-78
lines changed

2 files changed

+194
-78
lines changed

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

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,14 @@ import org.javacs.kt.util.filePath
77
import org.javacs.kt.util.describeURI
88
import org.javacs.kt.index.SymbolIndex
99
import org.javacs.kt.progress.Progress
10-
import org.javacs.kt.IndexingConfiguration
1110
import com.intellij.lang.Language
12-
import com.intellij.psi.PsiFile
13-
import com.intellij.openapi.fileTypes.FileType
14-
import com.intellij.openapi.fileTypes.LanguageFileType
1511
import org.jetbrains.kotlin.container.ComponentProvider
1612
import org.jetbrains.kotlin.container.getService
1713
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
1814
import org.jetbrains.kotlin.psi.KtFile
1915
import org.jetbrains.kotlin.resolve.BindingContext
2016
import org.jetbrains.kotlin.resolve.CompositeBindingContext
17+
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
2118
import kotlin.concurrent.withLock
2219
import java.nio.file.Path
2320
import java.nio.file.Paths
@@ -99,14 +96,16 @@ class SourcePath(
9996
private fun doCompile() {
10097
LOG.debug("Compiling {}", path?.fileName)
10198

99+
val oldFile = clone()
100+
102101
val (context, container) = cp.compiler.compileKtFile(parsed!!, allIncludingThis(), kind)
103102
parseDataWriteLock.withLock {
104103
compiledContext = context
105104
compiledContainer = container
106105
compiledFile = parsed
107106
}
108107

109-
initializeIndexAsyncIfNeeded(container)
108+
refreshIndexes(container, listOfNotNull(oldFile), listOfNotNull(this))
110109
}
111110

112111
private fun doCompileIfChanged() {
@@ -125,6 +124,9 @@ class SourcePath(
125124
if (isTemporary) (all().asSequence() + sequenceOf(parsed!!)).toList()
126125
else all()
127126
}
127+
128+
// Creates a shallow copy
129+
fun clone(): SourceFile = SourceFile(uri, content, path, parsed, compiledFile, compiledContext, compiledContainer, language, isTemporary)
128130
}
129131

130132
private fun sourceFile(uri: URI): SourceFile {
@@ -161,6 +163,8 @@ class SourcePath(
161163
}
162164

163165
fun delete(uri: URI) {
166+
files[uri]?.let { refreshIndexes(files[uri]?.compiledContainer!!, listOf(it), listOf()) }
167+
164168
files.remove(uri)
165169
}
166170

@@ -195,7 +199,20 @@ class SourcePath(
195199
// Compile changed files
196200
fun compileAndUpdate(changed: List<SourceFile>, kind: CompilationKind): BindingContext? {
197201
if (changed.isEmpty()) return null
202+
203+
// Get clones of the old files, so we can remove the old declarations from the index
204+
val oldFiles = changed.mapNotNull {
205+
if (it.compiledFile?.text != it.content || it.parsed?.text != it.content) {
206+
it.clone()
207+
} else {
208+
null
209+
}
210+
}
211+
212+
// Parse the files that have changed
198213
val parse = changed.associateWith { it.apply { parseIfChanged() }.parsed!! }
214+
215+
// Get all the files. This will parse them if they changed
199216
val allFiles = all()
200217
beforeCompileCallback.invoke()
201218
val (context, container) = cp.compiler.compileKtFiles(parse.values, allFiles, kind)
@@ -214,7 +231,7 @@ class SourcePath(
214231

215232
// Only index normal files, not build files
216233
if (kind == CompilationKind.DEFAULT) {
217-
initializeIndexAsyncIfNeeded(container)
234+
refreshIndexes(container, oldFiles, parse.keys.toList())
218235
}
219236

220237
return context
@@ -231,17 +248,41 @@ class SourcePath(
231248
}
232249

233250
/**
234-
* Initialized the symbol index asynchronously, if not
235-
* already done.
251+
* Refreshes the indexes. If already done, refreshes only the declarations in the files that were changed.
236252
*/
237-
private fun initializeIndexAsyncIfNeeded(container: ComponentProvider) = indexAsync.execute {
238-
if (indexEnabled && !indexInitialized) {
239-
indexInitialized = true
253+
private fun refreshIndexes(container: ComponentProvider, oldFiles: List<SourceFile>, newFiles: List<SourceFile>) = indexAsync.execute {
254+
if (indexEnabled) {
240255
val module = container.getService(ModuleDescriptor::class.java)
241-
index.refresh(module)
256+
val oldDeclarations = getDeclarationDescriptors(oldFiles)
257+
val newDeclarations = getDeclarationDescriptors(newFiles)
258+
259+
// Index all the declarations except any new declarations that were just compiled
260+
// TODO: Move this logic to a different place when re-indexing is triggered on configuration changes
261+
if (!indexInitialized) {
262+
indexInitialized = true
263+
index.refresh(module, newDeclarations)
264+
}
265+
266+
// Index the new declarations in the Kotlin source files that were just compiled, removing the old ones
267+
index.updateIndexes(oldDeclarations, newDeclarations)
242268
}
243269
}
244270

271+
// Gets all the declaration descriptors for the collection of files
272+
private fun getDeclarationDescriptors(files: Collection<SourceFile>) =
273+
files.flatMap { file ->
274+
val compiledFile = file.compiledFile ?: file.parsed
275+
val compiledContainer = file.compiledContainer
276+
if (compiledFile != null && compiledContainer != null) {
277+
val module = compiledContainer.getService(ModuleDescriptor::class.java)
278+
module.getPackage(compiledFile.packageFqName).memberScope.getContributedDescriptors(
279+
DescriptorKindFilter.ALL
280+
) { name -> compiledFile.declarations.map { it.name }.contains(name.toString()) }
281+
} else {
282+
listOf()
283+
}
284+
}.asSequence()
285+
245286
/**
246287
* Recompiles all source files that are initialized.
247288
*/

server/src/main/kotlin/org/javacs/kt/index/SymbolIndex.kt

Lines changed: 141 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,79 @@
11
package org.javacs.kt.index
22

3-
import org.jetbrains.exposed.sql.and
4-
import org.jetbrains.exposed.sql.count
5-
import org.jetbrains.exposed.sql.deleteAll
6-
import org.jetbrains.exposed.sql.innerJoin
7-
import org.jetbrains.exposed.sql.replace
8-
import org.jetbrains.exposed.sql.select
9-
import org.jetbrains.exposed.sql.selectAll
10-
import org.jetbrains.exposed.sql.Table
113
import org.jetbrains.exposed.sql.transactions.transaction
12-
import org.jetbrains.exposed.sql.SchemaUtils
134
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
145
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
156
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
16-
import org.jetbrains.kotlin.resolve.scopes.MemberScope
177
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
188
import org.jetbrains.kotlin.name.FqName
19-
import org.jetbrains.kotlin.psi2ir.intermediate.extensionReceiverType
209
import org.javacs.kt.LOG
2110
import org.javacs.kt.progress.Progress
22-
import org.jetbrains.exposed.sql.Database
23-
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
24-
import org.jetbrains.exposed.sql.insert
25-
26-
private val MAX_FQNAME_LENGTH = 255
27-
private val MAX_SHORT_NAME_LENGTH = 80
28-
29-
private object Symbols : Table() {
30-
val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH) references FqNames.fqName
11+
import org.jetbrains.exposed.dao.IntEntity
12+
import org.jetbrains.exposed.dao.IntEntityClass
13+
import org.jetbrains.exposed.dao.id.EntityID
14+
import org.jetbrains.exposed.dao.id.IntIdTable
15+
import org.jetbrains.exposed.sql.*
16+
import java.util.concurrent.CompletableFuture
17+
import kotlin.sequences.Sequence
18+
19+
private const val MAX_FQNAME_LENGTH = 255
20+
private const val MAX_SHORT_NAME_LENGTH = 80
21+
private const val MAX_URI_LENGTH = 511
22+
23+
private object Symbols : IntIdTable() {
24+
val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH).index()
25+
val shortName = varchar("shortname", length = MAX_SHORT_NAME_LENGTH)
3126
val kind = integer("kind")
3227
val visibility = integer("visibility")
3328
val extensionReceiverType = varchar("extensionreceivertype", length = MAX_FQNAME_LENGTH).nullable()
29+
val location = optReference("location", Locations)
30+
}
3431

35-
override val primaryKey = PrimaryKey(fqName)
32+
private object Locations : IntIdTable() {
33+
val uri = varchar("uri", length = MAX_URI_LENGTH)
34+
val range = reference("range", Ranges)
3635
}
3736

38-
private object FqNames : Table() {
39-
val fqName = varchar("fqname", length = MAX_FQNAME_LENGTH)
40-
val shortName = varchar("shortname", length = MAX_SHORT_NAME_LENGTH)
37+
private object Ranges : IntIdTable() {
38+
val start = reference("start", Positions)
39+
val end = reference("end", Positions)
40+
}
41+
42+
private object Positions : IntIdTable() {
43+
val line = integer("line")
44+
val character = integer("character")
45+
}
46+
47+
class SymbolEntity(id: EntityID<Int>) : IntEntity(id) {
48+
companion object : IntEntityClass<SymbolEntity>(Symbols)
49+
50+
var fqName by Symbols.fqName
51+
var shortName by Symbols.shortName
52+
var kind by Symbols.kind
53+
var visibility by Symbols.visibility
54+
var extensionReceiverType by Symbols.extensionReceiverType
55+
var location by LocationEntity optionalReferencedOn Symbols.location
56+
}
57+
58+
class LocationEntity(id: EntityID<Int>) : IntEntity(id) {
59+
companion object : IntEntityClass<LocationEntity>(Locations)
4160

42-
override val primaryKey = PrimaryKey(fqName)
61+
var uri by Locations.uri
62+
var range by RangeEntity referencedOn Locations.range
63+
}
64+
65+
class RangeEntity(id: EntityID<Int>) : IntEntity(id) {
66+
companion object : IntEntityClass<RangeEntity>(Ranges)
67+
68+
var start by PositionEntity referencedOn Ranges.start
69+
var end by PositionEntity referencedOn Ranges.end
70+
}
71+
72+
class PositionEntity(id: EntityID<Int>) : IntEntity(id) {
73+
companion object : IntEntityClass<PositionEntity>(Positions)
74+
75+
var line by Positions.line
76+
var character by Positions.character
4377
}
4478

4579
/**
@@ -48,47 +82,49 @@ private object FqNames : Table() {
4882
class SymbolIndex {
4983
private val db = Database.connect("jdbc:h2:mem:symbolindex;DB_CLOSE_DELAY=-1", "org.h2.Driver")
5084

51-
var progressFactory: Progress.Factory = Progress.Factory.None
85+
var progressFactory: Progress.Factory = object: Progress.Factory {
86+
override fun create(label: String): CompletableFuture<Progress> = CompletableFuture.supplyAsync { Progress.None }
87+
}
5288

5389
init {
5490
transaction(db) {
55-
SchemaUtils.create(Symbols, FqNames)
91+
SchemaUtils.create(Symbols, Locations, Ranges, Positions)
5692
}
5793
}
5894

5995
/** Rebuilds the entire index. May take a while. */
60-
fun refresh(module: ModuleDescriptor) {
96+
fun refresh(module: ModuleDescriptor, exclusions: Sequence<DeclarationDescriptor>) {
97+
val started = System.currentTimeMillis()
98+
LOG.info("Updating full symbol index...")
99+
100+
progressFactory.create("Indexing").thenApplyAsync { progress ->
101+
try {
102+
transaction(db) {
103+
addDeclarations(allDescriptors(module, exclusions))
104+
105+
val finished = System.currentTimeMillis()
106+
val count = Symbols.slice(Symbols.fqName.count()).selectAll().first()[Symbols.fqName.count()]
107+
LOG.info("Updated full symbol index in ${finished - started} ms! (${count} symbol(s))")
108+
}
109+
} catch (e: Exception) {
110+
LOG.error("Error while updating symbol index")
111+
LOG.printStackTrace(e)
112+
}
113+
114+
progress.close()
115+
}
116+
}
117+
118+
// Removes a list of indexes and adds another list. Everything is done in the same transaction.
119+
fun updateIndexes(remove: Sequence<DeclarationDescriptor>, add: Sequence<DeclarationDescriptor>) {
61120
val started = System.currentTimeMillis()
62121
LOG.info("Updating symbol index...")
63122

64-
progressFactory.create("Indexing").thenApply { progress ->
123+
progressFactory.create("Indexing").thenApplyAsync { progress ->
65124
try {
66-
// TODO: Incremental updates
67125
transaction(db) {
68-
Symbols.deleteAll()
69-
70-
for (descriptor in allDescriptors(module)) {
71-
val descriptorFqn = descriptor.fqNameSafe
72-
val extensionReceiverFqn = descriptor.accept(ExtractSymbolExtensionReceiverType, Unit)?.takeIf { !it.isRoot }
73-
74-
if (canStoreFqName(descriptorFqn) && (extensionReceiverFqn?.let { canStoreFqName(it) } ?: true)) {
75-
for (fqn in listOf(descriptorFqn, extensionReceiverFqn).filterNotNull()) {
76-
FqNames.replace {
77-
it[fqName] = fqn.toString()
78-
it[shortName] = fqn.shortName().toString()
79-
}
80-
}
81-
82-
Symbols.replace {
83-
it[fqName] = descriptorFqn.toString()
84-
it[kind] = descriptor.accept(ExtractSymbolKind, Unit).rawValue
85-
it[visibility] = descriptor.accept(ExtractSymbolVisibility, Unit).rawValue
86-
it[extensionReceiverType] = extensionReceiverFqn?.toString()
87-
}
88-
} else {
89-
LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString())
90-
}
91-
}
126+
removeDeclarations(remove)
127+
addDeclarations(add)
92128

93129
val finished = System.currentTimeMillis()
94130
val count = Symbols.slice(Symbols.fqName.count()).selectAll().first()[Symbols.fqName.count()]
@@ -103,29 +139,68 @@ class SymbolIndex {
103139
}
104140
}
105141

106-
private fun canStoreFqName(fqName: FqName) =
107-
fqName.toString().length <= MAX_FQNAME_LENGTH
108-
&& fqName.shortName().toString().length <= MAX_SHORT_NAME_LENGTH
142+
private fun Transaction.removeDeclarations(declarations: Sequence<DeclarationDescriptor>) =
143+
declarations.forEach { declaration ->
144+
val (descriptorFqn, extensionReceiverFqn) = getFqNames(declaration)
145+
146+
if (validFqName(descriptorFqn) && (extensionReceiverFqn?.let { validFqName(it) } != false)) {
147+
Symbols.deleteWhere {
148+
(Symbols.fqName eq descriptorFqn.toString()) and (Symbols.extensionReceiverType eq extensionReceiverFqn?.toString())
149+
}
150+
} else {
151+
LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString())
152+
}
153+
}
154+
155+
private fun Transaction.addDeclarations(declarations: Sequence<DeclarationDescriptor>) =
156+
declarations.forEach { declaration ->
157+
val (descriptorFqn, extensionReceiverFqn) = getFqNames(declaration)
158+
159+
if (validFqName(descriptorFqn) && (extensionReceiverFqn?.let { validFqName(it) } != false)) {
160+
SymbolEntity.new {
161+
fqName = descriptorFqn.toString()
162+
shortName = descriptorFqn.shortName().toString()
163+
kind = declaration.accept(ExtractSymbolKind, Unit).rawValue
164+
visibility = declaration.accept(ExtractSymbolVisibility, Unit).rawValue
165+
extensionReceiverType = extensionReceiverFqn?.toString()
166+
}
167+
} else {
168+
LOG.warn("Excluding symbol {} from index since its name is too long", descriptorFqn.toString())
169+
}
170+
}
171+
172+
private fun getFqNames(declaration: DeclarationDescriptor): Pair<FqName, FqName?> {
173+
val descriptorFqn = declaration.fqNameSafe
174+
val extensionReceiverFqn = declaration.accept(ExtractSymbolExtensionReceiverType, Unit)?.takeIf { !it.isRoot }
175+
176+
return Pair(descriptorFqn, extensionReceiverFqn)
177+
}
178+
179+
private fun validFqName(fqName: FqName) =
180+
fqName.toString().length <= MAX_FQNAME_LENGTH
181+
&& fqName.shortName().toString().length <= MAX_SHORT_NAME_LENGTH
109182

110183
fun query(prefix: String, receiverType: FqName? = null, limit: Int = 20): List<Symbol> = transaction(db) {
111184
// TODO: Extension completion currently only works if the receiver matches exactly,
112185
// ideally this should work with subtypes as well
113-
(Symbols innerJoin FqNames)
114-
.select { FqNames.shortName.like("$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString()) }
115-
.limit(limit)
186+
SymbolEntity.find {
187+
(Symbols.shortName like "$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString())
188+
}.limit(limit)
116189
.map { Symbol(
117-
fqName = FqName(it[Symbols.fqName]),
118-
kind = Symbol.Kind.fromRaw(it[Symbols.kind]),
119-
visibility = Symbol.Visibility.fromRaw(it[Symbols.visibility]),
120-
extensionReceiverType = it[Symbols.extensionReceiverType]?.let(::FqName)
190+
fqName = FqName(it.fqName),
191+
kind = Symbol.Kind.fromRaw(it.kind),
192+
visibility = Symbol.Visibility.fromRaw(it.visibility),
193+
extensionReceiverType = it.extensionReceiverType?.let(::FqName)
121194
) }
122195
}
123196

124-
private fun allDescriptors(module: ModuleDescriptor): Sequence<DeclarationDescriptor> = allPackages(module)
197+
private fun allDescriptors(module: ModuleDescriptor, exclusions: Sequence<DeclarationDescriptor>): Sequence<DeclarationDescriptor> = allPackages(module)
125198
.map(module::getPackage)
126199
.flatMap {
127200
try {
128-
it.memberScope.getContributedDescriptors(DescriptorKindFilter.ALL, MemberScope.ALL_NAME_FILTER)
201+
it.memberScope.getContributedDescriptors(
202+
DescriptorKindFilter.ALL
203+
) { name -> !exclusions.any { declaration -> declaration.name == name } }
129204
} catch (e: IllegalStateException) {
130205
LOG.warn("Could not query descriptors in package $it")
131206
emptyList()

0 commit comments

Comments
 (0)