diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt index e22fe84a2b..1253cdf342 100755 --- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt +++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.plus import org.appdevforall.codeonthego.computervision.di.computerVisionModule +import org.jetbrains.kotlin.cli.jvm.compiler.setupIdeaStandaloneExecution import org.koin.android.ext.koin.androidContext import org.koin.core.context.GlobalContext import org.koin.core.context.startKoin @@ -103,6 +104,9 @@ class IDEApplication : private set init { + System.setProperty("java.awt.headless", "true") + setupIdeaStandaloneExecution() + @Suppress("Deprecation") Shell.setDefaultBuilder( Shell.Builder diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 0c61e21fd9..ffb8278904 100755 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -10,6 +10,8 @@ android { } dependencies { + compileOnly(libs.composite.javac) + api(platform(libs.sora.bom)) api(libs.common.editor) api(libs.common.lang3) diff --git a/common/src/main/java/com/itsaky/androidide/utils/Environment.java b/common/src/main/java/com/itsaky/androidide/utils/Environment.java index 3b4bd5fb46..5ef6788bbc 100755 --- a/common/src/main/java/com/itsaky/androidide/utils/Environment.java +++ b/common/src/main/java/com/itsaky/androidide/utils/Environment.java @@ -25,6 +25,8 @@ import com.blankj.utilcode.util.FileUtils; import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider; import com.itsaky.androidide.buildinfo.BuildInfo; +import com.itsaky.androidide.javac.config.JavacConfigProvider; + import java.io.File; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -200,6 +202,9 @@ public static void init(Context context) { TEMPLATES_DIR = mkdirIfNotExists(new File(ANDROIDIDE_HOME, "templates")); SNIPPETS_DIR = mkdirIfNotExists(new File(ANDROIDIDE_HOME, "snippets")); + // required by Java and Kotlin LSP + System.setProperty(JavacConfigProvider.PROP_ANDROIDIDE_JAVA_HOME, JAVA_HOME.getAbsolutePath()); + isInitialized.set(true); } diff --git a/composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/javac/config/JavacConfigProvider.java b/composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/javac/config/JavacConfigProvider.java index 9d8539df0a..173b570c05 100644 --- a/composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/javac/config/JavacConfigProvider.java +++ b/composite-builds/build-deps/java-compiler/src/main/java/com/itsaky/androidide/javac/config/JavacConfigProvider.java @@ -49,7 +49,11 @@ public class JavacConfigProvider { */ public static String getJavaHome() { String javaHome = System.getProperty(PROP_ANDROIDIDE_JAVA_HOME); - if (javaHome == null || javaHome.trim().length() == 0) { + if (javaHome == null || javaHome.trim().isEmpty()) { + System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!"); + System.err.println(PROP_ANDROIDIDE_JAVA_HOME + " is not set. Falling back to java.home!"); + System.err.println("!!!!!!!!!!!!!!!!!!!!!!!!"); + javaHome = System.getProperty("java.home"); } return javaHome; diff --git a/logger/src/main/java/com/itsaky/androidide/logging/IDELoggingConfigurator.kt b/logger/src/main/java/com/itsaky/androidide/logging/IDELoggingConfigurator.kt index d497677881..ddbcbc7290 100644 --- a/logger/src/main/java/com/itsaky/androidide/logging/IDELoggingConfigurator.kt +++ b/logger/src/main/java/com/itsaky/androidide/logging/IDELoggingConfigurator.kt @@ -23,7 +23,6 @@ import ch.qos.logback.classic.spi.Configurator import ch.qos.logback.classic.spi.ConfiguratorRank import ch.qos.logback.core.spi.ContextAwareBase import com.google.auto.service.AutoService -import com.itsaky.androidide.logging.encoder.IDELogFormatEncoder /** * Default IDE logging configurator. diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt index d5a8527ca3..a110f276dd 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/FilteredIndex.kt @@ -1,7 +1,5 @@ package org.appdevforall.codeonthego.indexing -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable import org.appdevforall.codeonthego.indexing.api.ReadableIndex @@ -77,13 +75,12 @@ open class FilteredIndex( suspend fun isCached(sourceId: String): Boolean = backing.containsSource(sourceId) - override fun query(query: IndexQuery): Flow { - // If the query already specifies a sourceId, check if it's active + override fun query(query: IndexQuery): Sequence { if (query.sourceId != null && query.sourceId !in activeSources) { - return kotlinx.coroutines.flow.emptyFlow() + return emptySequence() } - - return backing.query(query).filter { it.sourceId in activeSources } + val original = backing.query(query) + return original.filter { it.sourceId in activeSources } } override suspend fun get(key: String): T? { @@ -95,7 +92,7 @@ open class FilteredIndex( return sourceId in activeSources && backing.containsSource(sourceId) } - override fun distinctValues(fieldName: String): Flow { + override fun distinctValues(fieldName: String): Sequence { // This is imprecise — the backing index may return values // from inactive sources. For exact results, we'd need to // query all entries and filter. For package enumeration @@ -109,4 +106,4 @@ open class FilteredIndex( activeSources.clear() if (backing is Closeable) backing.close() } -} \ No newline at end of file +} diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt index 20067e998e..bccfb6f69d 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/InMemoryIndex.kt @@ -1,7 +1,5 @@ package org.appdevforall.codeonthego.indexing -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.IndexDescriptor import org.appdevforall.codeonthego.indexing.api.IndexQuery @@ -54,17 +52,12 @@ class InMemoryIndex( } } - override fun query(query: IndexQuery): Flow = flow { + override fun query(query: IndexQuery): Sequence { val keys = resolveMatchingKeys(query) - var emitted = 0 val limit = if (query.limit <= 0) Int.MAX_VALUE else query.limit - - for (key in keys) { - if (emitted >= limit) break - val entry = primaryMap[key] ?: continue - emit(entry) - emitted++ - } + return keys + .mapNotNull { primaryMap[it] } + .take(limit) } override suspend fun get(key: String): T? = primaryMap[key] @@ -72,17 +65,9 @@ class InMemoryIndex( override suspend fun containsSource(sourceId: String): Boolean = sourceMap.containsKey(sourceId) - override fun distinctValues(fieldName: String): Flow = flow { - val fieldMap = fieldMaps[fieldName] ?: return@flow - lock.read { - for (value in fieldMap.keys) { - emit(value) - } - } - } - - override suspend fun insert(entries: Flow) { - entries.collect { entry -> insertSingle(entry) } + override fun distinctValues(fieldName: String): Sequence { + val fieldMap = fieldMaps[fieldName] ?: return emptySequence() + return lock.read { fieldMap.keys.toList() }.asSequence() } override suspend fun insertAll(entries: Sequence) { @@ -93,7 +78,7 @@ class InMemoryIndex( } } - override suspend fun insert(entry: T) = insertSingle(entry) + override suspend fun insert(entry: T) = lock.write { insertSingleLocked(entry) } override suspend fun removeBySource(sourceId: String) = lock.write { val keys = sourceMap.remove(sourceId) ?: return@write @@ -176,10 +161,6 @@ class InMemoryIndex( return current.intersect(other) } - private fun insertSingle(entry: T) = lock.write { - insertSingleLocked(entry) - } - private fun insertSingleLocked(entry: T) { val existing = primaryMap[entry.key] if (existing != null) { diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt index af39930033..05a9bab1e7 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/MergedIndex.kt @@ -1,17 +1,17 @@ package org.appdevforall.codeonthego.indexing -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.launch import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable import org.appdevforall.codeonthego.indexing.api.ReadableIndex import java.io.Closeable -import java.util.concurrent.ConcurrentHashMap /** * Merges query results from multiple [ReadableIndex] instances. * + * Indexes are queried sequentially in the order they are provided. + * Duplicate keys (same entry present in more than one backing index) + * are deduplicated — the first occurrence wins. + * * @param T The indexed type. * @param indexes The indexes to merge, in priority order. */ @@ -21,28 +21,17 @@ class MergedIndex( constructor(vararg indexes: ReadableIndex) : this(indexes.toList()) - override fun query(query: IndexQuery): Flow = channelFlow { - val seen = ConcurrentHashMap.newKeySet() + override fun query(query: IndexQuery): Sequence = sequence { val limit = if (query.limit <= 0) Int.MAX_VALUE else query.limit - val emitted = java.util.concurrent.atomic.AtomicInteger(0) - - // Launch a producer coroutine per index. - // channelFlow provides structured concurrency: when the - // collector stops (limit reached), all producers are cancelled. + val seen = mutableSetOf() + var total = 0 for (index in indexes) { - launch { - index.query(query).collect { entry -> - if (emitted.get() >= limit) { - return@collect - } - if (seen.add(entry.key)) { - send(entry) - if (emitted.incrementAndGet() >= limit) { - // Close the channel - cancels other producers - channel.close() - return@collect - } - } + if (total >= limit) break + for (entry in index.query(query)) { + if (total >= limit) break + if (seen.add(entry.key)) { + yield(entry) + total++ } } } @@ -61,15 +50,11 @@ class MergedIndex( return indexes.any { it.containsSource(sourceId) } } - override fun distinctValues(fieldName: String): Flow = channelFlow { - val seen = ConcurrentHashMap.newKeySet() + override fun distinctValues(fieldName: String): Sequence = sequence { + val seen = mutableSetOf() for (index in indexes) { - launch { - index.distinctValues(fieldName).collect { value -> - if (seen.add(value)) { - send(value) - } - } + for (value in index.distinctValues(fieldName)) { + if (seen.add(value)) yield(value) } } } diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt similarity index 85% rename from lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt rename to lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index f3b0cf539b..786dca5c03 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/PersistentIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -7,9 +7,6 @@ import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.IndexDescriptor @@ -18,7 +15,7 @@ import org.appdevforall.codeonthego.indexing.api.Indexable import kotlin.collections.iterator /** - * A persistent [Index] backed by SQLite via AndroidX. + * An [Index] backed by SQLite via AndroidX. * * Creates a table dynamically based on the [IndexDescriptor]: * ``` @@ -41,18 +38,24 @@ import kotlin.collections.iterator * Uses WAL journal mode for concurrent read/write performance. * Inserts are batched inside transactions for throughput. * + * [query] and [distinctValues] eagerly collect results and return a + * [Sequence] backed by a list. The cursor is always closed before + * returning; callers are responsible for running on an appropriate + * thread (typically [Dispatchers.IO] via the suspend insert paths). + * * @param T The indexed entry type. * @param descriptor Defines fields and serialization. * @param context Android context (for database file location). - * @param dbName Database file name. Different index types can share + * @param dbName Database file name. Pass `null` to create an in-memory database + * that is discarded when closed. Different index types can share * a database (each gets its own table) or use separate files. * @param batchSize Number of rows per INSERT transaction. */ -class PersistentIndex( +class SQLiteIndex( override val descriptor: IndexDescriptor, context: Context, - dbName: String, - override val name: String = "persistent:${descriptor.name}", + dbName: String?, + override val name: String = "sqlite:${descriptor.name}", private val batchSize: Int = 500, ) : Index { @@ -99,18 +102,18 @@ class PersistentIndex( createTable(db) } - override fun query(query: IndexQuery): Flow = flow { + override fun query(query: IndexQuery): Sequence { val (sql, args) = buildSelectQuery(query) val cursor = db.query(sql, args.toTypedArray()) - - cursor.use { + return cursor.use { val payloadIdx = it.getColumnIndexOrThrow("_payload") - while (it.moveToNext()) { - val bytes = it.getBlob(payloadIdx) - emit(descriptor.deserialize(bytes)) + buildList { + while (it.moveToNext()) { + add(descriptor.deserialize(it.getBlob(payloadIdx))) + } } - } - }.flowOn(Dispatchers.IO) + }.asSequence() + } override suspend fun get(key: String): T? = withContext(Dispatchers.IO) { val cursor = db.query( @@ -133,41 +136,17 @@ class PersistentIndex( cursor.use { it.moveToFirst() } } - override fun distinctValues(fieldName: String): Flow = flow { + override fun distinctValues(fieldName: String): Sequence { val col = fieldColumns[fieldName] ?: throw IllegalArgumentException("Unknown field: $fieldName") - val cursor = db.query("SELECT DISTINCT $col FROM $tableName WHERE $col IS NOT NULL") - cursor.use { - val idx = 0 - while (it.moveToNext()) { - emit(it.getString(idx)) - } - } - }.flowOn(Dispatchers.IO) - - /** - * Streaming insert from a [Flow]. - * - * Collects entries from the flow and inserts them in batched - * transactions. Each batch is a single SQLite transaction - - * this is orders of magnitude faster than one transaction per row. - * - * The flow is collected on [Dispatchers.IO]. - */ - override suspend fun insert(entries: Flow) = withContext(Dispatchers.IO) { - val batch = mutableListOf() - entries.collect { entry -> - batch.add(entry) - if (batch.size >= batchSize) { - insertBatch(batch) - batch.clear() + return cursor.use { + buildList { + while (it.moveToNext()) { + add(it.getString(0)) + } } - } - // Flush remaining - if (batch.isNotEmpty()) { - insertBatch(batch) - } + }.asSequence() } override suspend fun insertAll(entries: Sequence) = withContext(Dispatchers.IO) { diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt index 39f9846494..222c74772e 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/api/Index.kt @@ -1,28 +1,26 @@ package org.appdevforall.codeonthego.indexing.api -import kotlinx.coroutines.flow.Flow import java.io.Closeable /** * Read-only view of an index. * - * All query methods return [Flow]s and results are produced lazily. - * The consumer decides how many to take, which dispatcher to - * collect on, and whether to buffer. + * All query methods return [Sequence]s and results are produced lazily. + * The consumer decides how many to take and which thread to run on. * * @param T The indexed type. */ interface ReadableIndex { /** - * Query the index. Returns a lazy [Flow] of matching entries. + * Query the index. Returns a lazy [Sequence] of matching entries. * * Results are not guaranteed to be in any particular order * unless the implementation specifies otherwise. * * If [IndexQuery.limit] is 0, all matches are emitted. */ - fun query(query: IndexQuery): Flow + fun query(query: IndexQuery): Sequence /** * Point lookup by key. Returns null if not found. @@ -43,32 +41,20 @@ interface ReadableIndex { * @param fieldName Must be one of the fields declared in the * [IndexDescriptor]. */ - fun distinctValues(fieldName: String): Flow + fun distinctValues(fieldName: String): Sequence } /** * Write interface for mutating an index. - * - * Accepts [Flow]s for streaming inserts so that the producer can - * yield entries one at a time without holding the entire set - * in memory. */ interface WritableIndex { /** - * Insert entries from a [Flow]. + * Insert entries from a [Sequence]. * - * Entries are consumed lazily from the flow and batched + * Entries are consumed lazily from the sequence and batched * internally for throughput. If an entry with the same key * already exists, it is replaced. - * - * The flow is collected on the caller's dispatcher; the - * implementation handles its own threading for storage I/O. - */ - suspend fun insert(entries: Flow) - - /** - * Convenience: insert a sequence (also lazy). */ suspend fun insertAll(entries: Sequence) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt index cbf074cce2..33f010fbd5 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingService.kt @@ -33,6 +33,14 @@ interface IndexingService : Closeable { */ suspend fun initialize(registry: IndexRegistry) + /** + * Called after a build completes. + * + * Implementations should re-index any build outputs that may have changed + * (e.g. generated JARs). The default is a no-op. + */ + suspend fun onBuildCompleted() {} + /** * Called when the project is closed or the IDE shuts down. * Release all resources. diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt index 6575c5df63..3a5b98de66 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/service/IndexingServiceManager.kt @@ -40,11 +40,6 @@ class IndexingServiceManager( * @throws IllegalStateException if called after initialization. */ fun register(service: IndexingService) { - check(!initialized) { - "Cannot register services after initialization. " + - "Register all services before the first onProjectSynced call." - } - if (services.putIfAbsent(service.id, service) != null) { log.warn("Attempt to re-register service with ID: {}", service.id) return @@ -74,12 +69,26 @@ class IndexingServiceManager( /** * Called after a build completes. + * + * Forwards the event to all registered services concurrently. + * Failures in one service don't affect others (SupervisorJob). */ fun onBuildCompleted() { if (!initialized) { log.warn("onBuildCompleted called before initialization, ignoring") return } + scope.launch { + services.values.forEach { service -> + launch { + try { + service.onBuildCompleted() + } catch (e: Exception) { + log.error("Service '{}' failed in onBuildCompleted", service.id, e) + } + } + } + } } /** diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt index 1ab75e4074..3d2e01a6ae 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -6,12 +6,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -52,12 +46,6 @@ class BackgroundIndexer( private val scope: CoroutineScope = CoroutineScope( SupervisorJob() + Dispatchers.Default ), - /** - * Buffer capacity between the producer flow and the index writer. - * Higher values use more memory but tolerate more producer/consumer - * speed mismatch. - */ - private val bufferCapacity: Int = 64, ) : Closeable { companion object { @@ -69,22 +57,22 @@ class BackgroundIndexer( private val activeJobs = ConcurrentHashMap() /** - * Index a single source. The [provider] returns a [Flow] that - * lazily produces entries so that it is NOT collected eagerly. + * Index a single source. The [provider] returns a [Sequence] that + * lazily produces entries — it is consumed on [Dispatchers.IO] by + * [Index.insertAll]. * * If [skipIfExists] is true and the source is already indexed, * this is a no-op. * * @param sourceId Identifies the source. * @param skipIfExists Skip if already indexed. - * @param provider Lambda returning a lazy [Flow] of entries. - * Runs on [Dispatchers.IO]. - * @return The launched job, or null if skipped. + * @param provider Lambda returning a [Sequence] of entries. + * @return The launched job. */ fun indexSource( sourceId: String, skipIfExists: Boolean = true, - provider: (sourceId: String) -> Flow, + provider: (sourceId: String) -> Sequence, ): Job { // Cancel any in-flight job for this source activeJobs[sourceId]?.cancel() @@ -104,61 +92,28 @@ class BackgroundIndexer( if (!isActive) return@launch - // Streaming pipeline: - // producer (IO) → buffer → consumer (index.insert) - // - // The producer emits entries lazily on Dispatchers.IO. - // The buffer decouples producer and consumer speeds. - // The index.insert collects from the buffered flow - // and batches into transactions internally. - var count = 0 - - val tracked = provider(sourceId) - .flowOn(Dispatchers.IO) - .buffer(bufferCapacity) - .onStart { - progressListener?.onProgress( - sourceId, IndexingEvent.Started - ) - } - .onCompletion { error -> - if (error == null) { - progressListener?.onProgress( - sourceId, IndexingEvent.Completed(count) - ) - log.info("Indexed {} entries from {}", count, sourceId) - } - } - .catch { error -> - log.error("Indexing failed for {}", sourceId, error) - progressListener?.onProgress( - sourceId, IndexingEvent.Failed(error) - ) - } + progressListener?.onProgress(sourceId, IndexingEvent.Started) - // Wrap in a counting flow that reports progress - val counted = kotlinx.coroutines.flow.flow { - tracked.collect { entry -> - emit(entry) - count++ - if (count % 1000 == 0) { - progressListener?.onProgress( - sourceId, IndexingEvent.Progress(count) - ) - } + var count = 0 + val tracked = provider(sourceId).map { entry -> + count++ + if (count % 1000 == 0) { + progressListener?.onProgress(sourceId, IndexingEvent.Progress(count)) } + entry } - index.insert(counted) + index.insertAll(tracked) + + progressListener?.onProgress(sourceId, IndexingEvent.Completed(count)) + log.info("Indexed {} entries from {}", count, sourceId) } catch (e: CancellationException) { log.debug("Indexing cancelled: {}", sourceId) throw e } catch (e: Exception) { log.error("Indexing failed: {}", sourceId, e) - progressListener?.onProgress( - sourceId, IndexingEvent.Failed(e) - ) + progressListener?.onProgress(sourceId, IndexingEvent.Failed(e)) } finally { activeJobs.remove(sourceId) } @@ -169,23 +124,23 @@ class BackgroundIndexer( } /** - * Index multiple sources in parallel. + * Index multiple sources sequentially in the background. * * Each source gets its own coroutine. The [SupervisorJob] ensures * that one failure doesn't cancel the others. * * @param sources The sources to index (e.g. a list of JAR paths). - * @param mapper Maps each source to a (sourceId, Flow) pair. + * @param mapper Maps each source to a (sourceId, Sequence) pair. */ fun indexSources( sources: Collection, skipIfExists: Boolean = true, - mapper: (S) -> Pair>, + mapper: (S) -> Pair>, ): List { return sources.map { source -> - val (sourceId, flow) = mapper(source) - indexSource(sourceId, skipIfExists) { flow } - }.filterNotNull() + val (sourceId, seq) = mapper(source) + indexSource(sourceId, skipIfExists) { seq } + } } /** diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt index a31a330d7e..fc94ebbc84 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt @@ -75,7 +75,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.appdevforall.codeonthego.indexing.jvm.JvmIndexingService +import org.appdevforall.codeonthego.indexing.jvm.JvmGeneratedIndexingService +import org.appdevforall.codeonthego.indexing.jvm.JvmLibraryIndexingService import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -122,7 +123,10 @@ class JavaLanguageServer : ILanguageServer { val projectManager = ProjectManagerImpl.getInstance() projectManager.indexingServiceManager.register( - service = JvmIndexingService(context = BaseApplication.baseInstance) + service = JvmLibraryIndexingService(context = BaseApplication.baseInstance) + ) + projectManager.indexingServiceManager.register( + service = JvmGeneratedIndexingService(context = BaseApplication.baseInstance) ) JavaSnippetRepository.init() @@ -161,7 +165,7 @@ class JavaLanguageServer : ILanguageServer { (ProjectManagerImpl.getInstance() .indexingServiceManager - .getService(JvmIndexingService.ID) as? JvmIndexingService?) + .getService(JvmLibraryIndexingService.ID) as? JvmLibraryIndexingService?) ?.refresh() // Once we have project initialized diff --git a/lsp/jvm-symbol-index/build.gradle.kts b/lsp/jvm-symbol-index/build.gradle.kts index 959f2264be..796860d0e3 100644 --- a/lsp/jvm-symbol-index/build.gradle.kts +++ b/lsp/jvm-symbol-index/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { api(projects.lsp.jvmSymbolModels) api(projects.subprojects.kotlinAnalysisApi) api(projects.subprojects.projects) + api(projects.lsp.kotlinCore) } diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt index f0bea84939..33256609df 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt @@ -1,9 +1,8 @@ package org.appdevforall.codeonthego.indexing.jvm -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile import org.jetbrains.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor @@ -22,12 +21,32 @@ object CombinedJarScanner { private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + @OptIn(KaImplementationDetail::class) + fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Sequence = sequence { + val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) + for (vf in allFiles) { + if (!vf.name.endsWith(".class")) continue + if (vf.name == "module-info.class" || vf.name == "package-info.class") continue + try { + val bytes = vf.contentsToByteArray() + val symbols = if (hasKotlinMetadata(bytes)) { + KotlinMetadataScanner.parseKotlinClass(bytes.inputStream(), sourceId) + } else { + JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) + } + symbols?.forEach { yield(it) } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", vf.path, e.message) + } + } + } + + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Sequence = sequence { val jar = try { JarFile(jarPath.toFile()) } catch (e: Exception) { log.warn("Failed to open JAR: {}", jarPath, e) - return@flow + return@sequence } jar.use { @@ -50,14 +69,13 @@ object CombinedJarScanner { JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) } - symbols?.forEach { emit(it) } + symbols?.forEach { yield(it) } } catch (e: Exception) { log.debug("Failed to parse {}: {}", entry.name, e.message) } } } } - .flowOn(Dispatchers.IO) private fun hasKotlinMetadata(classBytes: ByteArray): Boolean { var found = false diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt index 19a8422d69..2c30a93372 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt @@ -4,6 +4,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile import org.jetbrains.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor @@ -27,9 +30,28 @@ import kotlin.io.path.pathString */ object JarSymbolScanner { - private val log = LoggerFactory.getLogger(JarSymbolScanner::class.java) + private val log = LoggerFactory.getLogger(JarSymbolScanner::class.java) + + @OptIn(KaImplementationDetail::class) + fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Flow = flow { + val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) + for (vf in allFiles) { + if (!vf.name.endsWith(".class")) continue + if (vf.name == "module-info.class" || vf.name == "package-info.class") continue + try { + vf.contentsToByteArray().inputStream().use { input -> + for (symbol in parseClassFile(input, sourceId)) { + emit(symbol) + } + } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", vf.path, e.message) + } + } + } + .flowOn(Dispatchers.IO) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { val jar = try { JarFile(jarPath.toFile()) } catch (e: Exception) { @@ -59,244 +81,247 @@ object JarSymbolScanner { } .flowOn(Dispatchers.IO) - internal fun parseClassFile(input: InputStream, sourceId: String): List { - val reader = ClassReader(input) - val collector = SymbolCollector(sourceId) - reader.accept(collector, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) - return collector.symbols - } - - private class SymbolCollector( - private val sourceId: String, - ) : ClassVisitor(Opcodes.ASM9) { - - val symbols = mutableListOf() - - private var className = "" - private var classFqName = "" - private var packageName = "" - private var shortClassName = "" - private var classAccess = 0 - private var isKotlinClass = false - private var superName: String? = null - private var interfaces: Array? = null - private var isInnerClass = false - private var classDeprecated = false - - override fun visit( - version: Int, access: Int, name: String, - signature: String?, superName: String?, - interfaces: Array?, - ) { - className = name - classFqName = name.replace('/', '.').replace('$', '.') - classAccess = access - this.superName = superName - this.interfaces = interfaces - classDeprecated = false - - val lastSlash = name.lastIndexOf('/') - packageName = if (lastSlash >= 0) name.substring(0, lastSlash).replace('/', '.') else "" - - val afterPackage = if (lastSlash >= 0) name.substring(lastSlash + 1) else name - shortClassName = afterPackage.replace('$', '.') - - isInnerClass = name.contains('$') - } - - override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { - if (descriptor == "Ljava/lang/Deprecated;") classDeprecated = true - if (descriptor == "Lkotlin/Metadata;") isKotlinClass = true - return null - } - - override fun visitEnd() { - if (!isPublicOrProtected(classAccess)) return - - val isAnonymous = isInnerClass && - shortClassName.split('.').last().firstOrNull()?.isDigit() == true - if (isAnonymous) return - - val kind = classKindFromAccess(classAccess) - val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - - val supertypes = buildList { - superName?.let { - if (it != "java/lang/Object") add(it) - } - interfaces?.forEach { add(it) } - } - - val containingClass = if (isInnerClass) { + internal fun parseClassFile(input: InputStream, sourceId: String): List { + val reader = ClassReader(input) + val collector = SymbolCollector(sourceId) + reader.accept( + collector, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ) + return collector.symbols + } + + private class SymbolCollector( + private val sourceId: String, + ) : ClassVisitor(Opcodes.ASM9) { + + val symbols = mutableListOf() + + private var className = "" + private var classFqName = "" + private var packageName = "" + private var shortClassName = "" + private var classAccess = 0 + private var isKotlinClass = false + private var superName: String? = null + private var interfaces: Array? = null + private var isInnerClass = false + private var classDeprecated = false + + override fun visit( + version: Int, access: Int, name: String, + signature: String?, superName: String?, + interfaces: Array?, + ) { + className = name + classFqName = name.replace('/', '.').replace('$', '.') + classAccess = access + this.superName = superName + this.interfaces = interfaces + classDeprecated = false + + val lastSlash = name.lastIndexOf('/') + packageName = if (lastSlash >= 0) name.substring(0, lastSlash).replace('/', '.') else "" + + val afterPackage = if (lastSlash >= 0) name.substring(lastSlash + 1) else name + shortClassName = afterPackage.replace('$', '.') + + isInnerClass = name.contains('$') + } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor == "Ljava/lang/Deprecated;") classDeprecated = true + if (descriptor == "Lkotlin/Metadata;") isKotlinClass = true + return null + } + + override fun visitEnd() { + if (!isPublicOrProtected(classAccess)) return + + val isAnonymous = isInnerClass && + shortClassName.split('.').last().firstOrNull()?.isDigit() == true + if (isAnonymous) return + + val kind = classKindFromAccess(classAccess) + val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA + + val supertypes = buildList { + superName?.let { + if (it != "java/lang/Object") add(it) + } + interfaces?.forEach { add(it) } + } + + val containingClass = if (isInnerClass) { className.substringBeforeLast('$') - } else "" - - symbols.add( - JvmSymbol( - key = className, - sourceId = sourceId, - name = classFqName, - shortName = shortClassName.split('.').last(), - packageName = packageName, - kind = kind, - language = language, - visibility = visibilityFromAccess(classAccess), - isDeprecated = classDeprecated, - data = JvmClassInfo( + } else "" + + symbols.add( + JvmSymbol( + key = className, + sourceId = sourceId, + name = classFqName, + shortName = shortClassName.split('.').last(), + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(classAccess), + isDeprecated = classDeprecated, + data = JvmClassInfo( internalName = className, - containingClassName = containingClass, - supertypeNames = supertypes, - isAbstract = hasFlag(classAccess, Opcodes.ACC_ABSTRACT), - isFinal = hasFlag(classAccess, Opcodes.ACC_FINAL), - isInner = isInnerClass && !hasFlag(classAccess, Opcodes.ACC_STATIC), - isStatic = isInnerClass && hasFlag(classAccess, Opcodes.ACC_STATIC), - ), - ) - ) - } - - override fun visitMethod( - access: Int, name: String, descriptor: String, - signature: String?, exceptions: Array?, - ): MethodVisitor? { - if (!isPublicOrProtected(access)) return null - if (!isPublicOrProtected(classAccess)) return null - if (name.startsWith("access$")) return null - if (hasFlag(access, Opcodes.ACC_BRIDGE)) return null - if (hasFlag(access, Opcodes.ACC_SYNTHETIC)) return null - if (name == "") return null - - val methodType = Type.getMethodType(descriptor) - val paramTypes = methodType.argumentTypes - val returnType = methodType.returnType - - val isConstructor = name == "" - val methodName = if (isConstructor) shortClassName.split('.').last() else name - val kind = if (isConstructor) JvmSymbolKind.CONSTRUCTOR else JvmSymbolKind.FUNCTION - val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - - val parameters = paramTypes.map { type -> - JvmParameterInfo( - name = "", // not available without -parameters flag - typeName = typeToName(type), - typeDisplayName = typeToDisplayName(type), - ) - } - - val fqName = "$className#$methodName" - val key = "$fqName(${parameters.joinToString(",") { it.typeName }})" - - val signatureDisplay = buildString { - append("(") - append(parameters.joinToString(", ") { it.typeDisplayName }) - append(")") - if (!isConstructor) { - append(": ") - append(typeToDisplayName(returnType)) - } - } - - symbols.add( - JvmSymbol( - key = key, - sourceId = sourceId, - name = fqName, - shortName = methodName, - packageName = packageName, - kind = kind, - language = language, - visibility = visibilityFromAccess(access), - isDeprecated = classDeprecated, - data = JvmFunctionInfo( - containingClassName = className, - returnTypeName = typeToName(returnType), - returnTypeDisplayName = typeToDisplayName(returnType), - parameterCount = paramTypes.size, - parameters = parameters, - signatureDisplay = signatureDisplay, - isStatic = hasFlag(access, Opcodes.ACC_STATIC), - isAbstract = hasFlag(access, Opcodes.ACC_ABSTRACT), - isFinal = hasFlag(access, Opcodes.ACC_FINAL), - ), - ) - ) - - return null - } - - override fun visitField( - access: Int, name: String, descriptor: String, - signature: String?, value: Any?, - ): FieldVisitor? { - if (!isPublicOrProtected(access)) return null - if (!isPublicOrProtected(classAccess)) return null - if (hasFlag(access, Opcodes.ACC_SYNTHETIC)) return null - - val fieldType = Type.getType(descriptor) - val kind = if (isKotlinClass) JvmSymbolKind.PROPERTY else JvmSymbolKind.FIELD - val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - val iName = "$className#$name" - - symbols.add( - JvmSymbol( - key = iName, - sourceId = sourceId, - name = iName, - shortName = name, - packageName = packageName, - kind = kind, - language = language, - visibility = visibilityFromAccess(access), - isDeprecated = classDeprecated, - data = JvmFieldInfo( - containingClassName = className, - typeName = typeToName(fieldType), - typeDisplayName = typeToDisplayName(fieldType), - isStatic = hasFlag(access, Opcodes.ACC_STATIC), - isFinal = hasFlag(access, Opcodes.ACC_FINAL), - constantValue = value?.toString() ?: "", - ), - ) - ) - - return null - } - - private fun isPublicOrProtected(access: Int) = - hasFlag(access, Opcodes.ACC_PUBLIC) || hasFlag(access, Opcodes.ACC_PROTECTED) - - private fun hasFlag(access: Int, flag: Int) = (access and flag) != 0 - - private fun classKindFromAccess(access: Int) = when { - hasFlag(access, Opcodes.ACC_ANNOTATION) -> JvmSymbolKind.ANNOTATION_CLASS - hasFlag(access, Opcodes.ACC_ENUM) -> JvmSymbolKind.ENUM - hasFlag(access, Opcodes.ACC_INTERFACE) -> JvmSymbolKind.INTERFACE - else -> JvmSymbolKind.CLASS - } - - private fun visibilityFromAccess(access: Int) = when { - hasFlag(access, Opcodes.ACC_PUBLIC) -> JvmVisibility.PUBLIC - hasFlag(access, Opcodes.ACC_PROTECTED) -> JvmVisibility.PROTECTED - hasFlag(access, Opcodes.ACC_PRIVATE) -> JvmVisibility.PRIVATE - else -> JvmVisibility.PACKAGE_PRIVATE - } - - private fun typeToName(type: Type): String = when (type.sort) { - Type.VOID -> "V" - Type.BOOLEAN -> "Z" - Type.BYTE -> "B" - Type.CHAR -> "C" - Type.SHORT -> "S" - Type.INT -> "I" - Type.LONG -> "J" - Type.FLOAT -> "F" - Type.DOUBLE -> "D" - Type.ARRAY -> "[".repeat(type.dimensions) + typeToName(type.elementType) - Type.OBJECT -> type.internalName - else -> type.internalName - } - - private fun typeToDisplayName(type: Type): String = when (type.sort) { + containingClassName = containingClass, + supertypeNames = supertypes, + isAbstract = hasFlag(classAccess, Opcodes.ACC_ABSTRACT), + isFinal = hasFlag(classAccess, Opcodes.ACC_FINAL), + isInner = isInnerClass && !hasFlag(classAccess, Opcodes.ACC_STATIC), + isStatic = isInnerClass && hasFlag(classAccess, Opcodes.ACC_STATIC), + ), + ) + ) + } + + override fun visitMethod( + access: Int, name: String, descriptor: String, + signature: String?, exceptions: Array?, + ): MethodVisitor? { + if (!isPublicOrProtected(access)) return null + if (!isPublicOrProtected(classAccess)) return null + if (name.startsWith("access$")) return null + if (hasFlag(access, Opcodes.ACC_BRIDGE)) return null + if (hasFlag(access, Opcodes.ACC_SYNTHETIC)) return null + if (name == "") return null + + val methodType = Type.getMethodType(descriptor) + val paramTypes = methodType.argumentTypes + val returnType = methodType.returnType + + val isConstructor = name == "" + val methodName = if (isConstructor) shortClassName.split('.').last() else name + val kind = if (isConstructor) JvmSymbolKind.CONSTRUCTOR else JvmSymbolKind.FUNCTION + val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA + + val parameters = paramTypes.map { type -> + JvmParameterInfo( + name = "", // not available without -parameters flag + typeName = typeToName(type), + typeDisplayName = typeToDisplayName(type), + ) + } + + val fqName = "$className#$methodName" + val key = "$fqName(${parameters.joinToString(",") { it.typeName }})" + + val signatureDisplay = buildString { + append("(") + append(parameters.joinToString(", ") { it.typeDisplayName }) + append(")") + if (!isConstructor) { + append(": ") + append(typeToDisplayName(returnType)) + } + } + + symbols.add( + JvmSymbol( + key = key, + sourceId = sourceId, + name = fqName, + shortName = methodName, + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(access), + isDeprecated = classDeprecated, + data = JvmFunctionInfo( + containingClassName = className, + returnTypeName = typeToName(returnType), + returnTypeDisplayName = typeToDisplayName(returnType), + parameterCount = paramTypes.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + isStatic = hasFlag(access, Opcodes.ACC_STATIC), + isAbstract = hasFlag(access, Opcodes.ACC_ABSTRACT), + isFinal = hasFlag(access, Opcodes.ACC_FINAL), + ), + ) + ) + + return null + } + + override fun visitField( + access: Int, name: String, descriptor: String, + signature: String?, value: Any?, + ): FieldVisitor? { + if (!isPublicOrProtected(access)) return null + if (!isPublicOrProtected(classAccess)) return null + if (hasFlag(access, Opcodes.ACC_SYNTHETIC)) return null + + val fieldType = Type.getType(descriptor) + val kind = if (isKotlinClass) JvmSymbolKind.PROPERTY else JvmSymbolKind.FIELD + val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA + val iName = "$className#$name" + + symbols.add( + JvmSymbol( + key = iName, + sourceId = sourceId, + name = iName, + shortName = name, + packageName = packageName, + kind = kind, + language = language, + visibility = visibilityFromAccess(access), + isDeprecated = classDeprecated, + data = JvmFieldInfo( + containingClassName = className, + typeName = typeToName(fieldType), + typeDisplayName = typeToDisplayName(fieldType), + isStatic = hasFlag(access, Opcodes.ACC_STATIC), + isFinal = hasFlag(access, Opcodes.ACC_FINAL), + constantValue = value?.toString() ?: "", + ), + ) + ) + + return null + } + + private fun isPublicOrProtected(access: Int) = + hasFlag(access, Opcodes.ACC_PUBLIC) || hasFlag(access, Opcodes.ACC_PROTECTED) + + private fun hasFlag(access: Int, flag: Int) = (access and flag) != 0 + + private fun classKindFromAccess(access: Int) = when { + hasFlag(access, Opcodes.ACC_ANNOTATION) -> JvmSymbolKind.ANNOTATION_CLASS + hasFlag(access, Opcodes.ACC_ENUM) -> JvmSymbolKind.ENUM + hasFlag(access, Opcodes.ACC_INTERFACE) -> JvmSymbolKind.INTERFACE + else -> JvmSymbolKind.CLASS + } + + private fun visibilityFromAccess(access: Int) = when { + hasFlag(access, Opcodes.ACC_PUBLIC) -> JvmVisibility.PUBLIC + hasFlag(access, Opcodes.ACC_PROTECTED) -> JvmVisibility.PROTECTED + hasFlag(access, Opcodes.ACC_PRIVATE) -> JvmVisibility.PRIVATE + else -> JvmVisibility.PACKAGE_PRIVATE + } + + private fun typeToName(type: Type): String = when (type.sort) { + Type.VOID -> "V" + Type.BOOLEAN -> "Z" + Type.BYTE -> "B" + Type.CHAR -> "C" + Type.SHORT -> "S" + Type.INT -> "I" + Type.LONG -> "J" + Type.FLOAT -> "F" + Type.DOUBLE -> "D" + Type.ARRAY -> "[".repeat(type.dimensions) + typeToName(type.elementType) + Type.OBJECT -> type.internalName + else -> type.internalName + } + + private fun typeToDisplayName(type: Type): String = when (type.sort) { Type.BOOLEAN -> "boolean" Type.BYTE -> "byte" Type.CHAR -> "char" @@ -305,10 +330,10 @@ object JarSymbolScanner { Type.LONG -> "long" Type.FLOAT -> "float" Type.DOUBLE -> "double" - Type.VOID -> "void" - Type.ARRAY -> typeToDisplayName(type.elementType) + "[]".repeat(type.dimensions) - Type.OBJECT -> type.className.substringAfterLast('.') - else -> typeToName(type) - } - } + Type.VOID -> "void" + Type.ARRAY -> typeToDisplayName(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className.substringAfterLast('.') + else -> typeToName(type) + } + } } diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmGeneratedIndexingService.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmGeneratedIndexingService.kt new file mode 100644 index 0000000000..5a8c5ee4eb --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmGeneratedIndexingService.kt @@ -0,0 +1,132 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import android.content.Context +import com.itsaky.androidide.projects.ProjectManagerImpl +import com.itsaky.androidide.projects.api.ModuleProject +import com.itsaky.androidide.tasks.cancelIfActive +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.appdevforall.codeonthego.indexing.service.IndexKey +import org.appdevforall.codeonthego.indexing.service.IndexRegistry +import org.appdevforall.codeonthego.indexing.service.IndexingService +import org.slf4j.LoggerFactory +import java.nio.file.Paths +import kotlin.io.path.extension + +/** + * Well-known key for the JVM generated-symbol index. + * + * Covers build-time-generated JARs such as R.jar that are excluded + * from the main library index. Both the Kotlin and Java LSPs can + * retrieve this index from the [IndexRegistry]. + */ +val JVM_GENERATED_SYMBOL_INDEX = IndexKey("jvm-generated-symbols") + +/** + * [IndexingService] that scans build-generated JARs (R.jar, etc.) and + * maintains a dedicated [JvmSymbolIndex] for them. + * + * Generated JARs are re-indexed unconditionally on every build completion + * because their contents change (new R-field values, new resource IDs) even + * when the set of JARs doesn't change. + */ +class JvmGeneratedIndexingService( + private val context: Context, +) : IndexingService { + + companion object { + const val ID = "jvm-generated-indexing-service" + private const val DB_NAME = "jvm_generated_symbol_index.db" + private const val INDEX_NAME = "jvm-generated-cache" + private val log = LoggerFactory.getLogger(JvmGeneratedIndexingService::class.java) + } + + override val id = ID + + override val providedKeys = listOf(JVM_GENERATED_SYMBOL_INDEX) + + private var generatedIndex: JvmSymbolIndex? = null + private val indexingMutex = Mutex() + private val coroutineScope = CoroutineScope(Dispatchers.Default) + + override suspend fun initialize(registry: IndexRegistry) { + val index = JvmSymbolIndex.createSqliteIndex( + context = context, + dbName = DB_NAME, + indexName = INDEX_NAME, + ) + + this.generatedIndex = index + registry.register(JVM_GENERATED_SYMBOL_INDEX, index) + log.info("JVM generated symbol index initialized") + + // Kick off an initial index pass for any already-built JARs. + coroutineScope.launch { + indexingMutex.withLock { + reindexGeneratedJars(forceReindex = false) + } + } + } + + override suspend fun onBuildCompleted() { + // Generated JARs (especially R.jar) always change after a build — + // their field values are regenerated. Force a full re-index. + coroutineScope.launch { + indexingMutex.withLock { + reindexGeneratedJars(forceReindex = true) + } + } + } + + private suspend fun reindexGeneratedJars(forceReindex: Boolean) { + val index = this.generatedIndex ?: run { + log.warn("Not indexing generated JARs — index not initialized.") + return + } + + val workspace = ProjectManagerImpl.getInstance().workspace ?: run { + log.warn("Not indexing generated JARs — workspace model not available.") + return + } + + val generatedJars = + workspace.subProjects + .asSequence() + .filterIsInstance() + .filter { it.path != workspace.rootProject.path } + .flatMap { project -> project.getIntermediateClasspaths() } + .filter { jar -> jar.exists() && jar.toPath().extension.lowercase() == "jar" } + .map { jar -> jar.absolutePath } + .toSet() + + log.info("{} generated JARs found", generatedJars.size) + + // Make exactly these JARs visible; remove stale ones from scope. + index.setActiveSources(generatedJars) + + var submitted = 0 + for (jarPath in generatedJars) { + if (forceReindex || !index.isCached(jarPath)) { + submitted++ + index.indexSource(jarPath, skipIfExists = false) { sourceId -> + CombinedJarScanner.scan(Paths.get(jarPath), sourceId) + } + } + } + + if (submitted > 0) { + log.info("{} generated JARs submitted for background indexing (force={})", submitted, forceReindex) + } else { + log.info("All generated JARs already cached, nothing to index") + } + } + + override fun close() { + coroutineScope.cancelIfActive("generated indexing service closed") + generatedIndex?.close() + generatedIndex = null + } +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmIndexingService.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibraryIndexingService.kt similarity index 82% rename from lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmIndexingService.kt rename to lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibraryIndexingService.kt index a85c2c271e..8b53c393dd 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmIndexingService.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibraryIndexingService.kt @@ -27,36 +27,41 @@ import kotlin.io.path.extension * Both the Kotlin and Java LSPs use this key to retrieve the * shared index from the [IndexRegistry]. */ -val JVM_LIBRARY_SYMBOL_INDEX = IndexKey("jvm-library-symbols") +val JVM_LIBRARY_SYMBOL_INDEX = IndexKey("jvm-library-symbols") /** * [IndexingService] that scans classpath JARs/AARs and builds - * a [JvmLibrarySymbolIndex]. + * a [JvmSymbolIndex]. * * Thread safety: all methods are called from the * [IndexingServiceManager][org.appdevforall.codeonthego.indexing.service.IndexingServiceManager]'s - * coroutine scope. The [JvmLibrarySymbolIndex] handles its own internal thread safety. + * coroutine scope. The [JvmSymbolIndex] handles its own internal thread safety. */ -class JvmIndexingService( +class JvmLibraryIndexingService( private val context: Context, ) : IndexingService { companion object { const val ID = "jvm-indexing-service" - private val log = LoggerFactory.getLogger(JvmIndexingService::class.java) + private val log = LoggerFactory.getLogger(JvmLibraryIndexingService::class.java) } override val id = ID override val providedKeys = listOf(JVM_LIBRARY_SYMBOL_INDEX) - private var index: JvmLibrarySymbolIndex? = null + private var libraryIndex: JvmSymbolIndex? = null private var indexingMutex = Mutex() private val coroutineScope = CoroutineScope(Dispatchers.Default) override suspend fun initialize(registry: IndexRegistry) { - val jvmIndex = JvmLibrarySymbolIndex.create(context) - this.index = jvmIndex + val jvmIndex = JvmSymbolIndex.createSqliteIndex( + context = context, + dbName = JvmSymbolIndex.DB_NAME_DEFAULT, + indexName = JvmSymbolIndex.INDEX_NAME_LIBRARY + ) + + this.libraryIndex = jvmIndex registry.register(JVM_LIBRARY_SYMBOL_INDEX, jvmIndex) log.info("JVM symbol index initialized") } @@ -76,7 +81,7 @@ class JvmIndexingService( } private suspend fun reindexLibraries() { - val index = this.index ?: run { + val index = this.libraryIndex ?: run { log.warn("Not indexing libraries. Index not initialized.") return } @@ -110,7 +115,7 @@ class JvmIndexingService( // JARs not in the set become invisible to queries. // JARs in the set that are already cached become // visible immediately. - index.setActiveLibraries(currentJars) + index.setActiveSources(currentJars) // Step 2: Index any JARs not yet in the cache. // Already-cached JARs are skipped (cheap existence check). @@ -118,9 +123,9 @@ class JvmIndexingService( // they're already in the active set. var newCount = 0 for (jarPath in currentJars) { - if (!index.isLibraryCached(jarPath)) { + if (!index.isCached(jarPath)) { newCount++ - index.indexLibrary(jarPath) { sourceId -> + index.indexSource(jarPath, skipIfExists = true) { sourceId -> CombinedJarScanner.scan(Paths.get(jarPath), sourceId) } } @@ -135,8 +140,8 @@ class JvmIndexingService( override fun close() { coroutineScope.cancelIfActive("indexing service closed") - index?.close() - index = null + libraryIndex?.close() + libraryIndex = null } private fun isIndexableJar(path: Path): Boolean { diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt deleted file mode 100644 index ec52e5d633..0000000000 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmLibrarySymbolIndex.kt +++ /dev/null @@ -1,166 +0,0 @@ -package org.appdevforall.codeonthego.indexing.jvm - -import android.content.Context -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.take -import org.appdevforall.codeonthego.indexing.FilteredIndex -import org.appdevforall.codeonthego.indexing.PersistentIndex -import org.appdevforall.codeonthego.indexing.api.indexQuery -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_CONTAINING_CLASS -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_NAME -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_PACKAGE -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_RECEIVER_TYPE -import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer -import java.io.Closeable - -/** - * An index of symbols from external Java libraries (JARs). - */ -class JvmLibrarySymbolIndex private constructor( - /** Persistent cache — stores every JAR ever indexed. */ - val libraryCache: PersistentIndex, - - /** Filtered view — only shows JARs on the current classpath. */ - val libraryView: FilteredIndex, - - /** Background indexer writing to the cache. */ - val libraryIndexer: BackgroundIndexer, -) : Closeable { - - companion object { - - const val DB_NAME_DEFAULT = "jvm_symbol_index.db" - const val INDEX_NAME_LIBRARY = "jvm-library-cache" - - fun create( - context: Context, - dbName: String = DB_NAME_DEFAULT, - ): JvmLibrarySymbolIndex { - val cache = PersistentIndex( - descriptor = JvmSymbolDescriptor, - context = context, - dbName = dbName, - name = INDEX_NAME_LIBRARY, - ) - - val view = FilteredIndex(cache) - - val indexer = BackgroundIndexer(cache) - return JvmLibrarySymbolIndex( - libraryCache = cache, - libraryView = view, - libraryIndexer = indexer - ) - } - } - - /** - * Make a library visible in query results. - * - * If the library is already cached (indexed previously), - * this is instant. If not, call [indexLibrary] first. - */ - fun activateLibrary(sourceId: String) { - libraryView.activateSource(sourceId) - } - - /** - * Hide a library from query results. - * The cached index data is retained for future reuse. - */ - fun deactivateLibrary(sourceId: String) { - libraryView.deactivateSource(sourceId) - } - - /** - * Replace the entire active library set. - * - * Typical call after project sync: pass all current classpath - * JAR paths. Libraries not in the set become invisible. - * Libraries in the set that are already cached become - * instantly visible. - */ - fun setActiveLibraries(sourceIds: Set) { - libraryView.setActiveSources(sourceIds) - } - - /** - * Check if a library is already cached (regardless of whether - * it's currently active). - */ - suspend fun isLibraryCached(sourceId: String): Boolean = - libraryView.isCached(sourceId) - - /** - * Index a library JAR/AAR into the persistent cache. - * - * This does NOT make the library visible in queries — - * call [activateLibrary] after indexing completes. - * - * Skips if already cached. Call [reindexLibrary] to force. - */ - fun indexLibrary( - sourceId: String, - provider: (sourceId: String) -> Flow, - ) = libraryIndexer.indexSource(sourceId, skipIfExists = true, provider) - - fun reindexLibrary( - sourceId: String, - provider: (sourceId: String) -> Flow, - ) = libraryIndexer.indexSource(sourceId, skipIfExists = false, provider) - - fun findByPrefix(prefix: String, limit: Int = 200): Flow = - libraryView.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = limit }) - - fun findByPrefix( - prefix: String, kinds: Set, limit: Int = 200, - ): Flow = - libraryView.query(indexQuery { prefix(KEY_NAME, prefix); this.limit = 0 }) - .filter { it.kind in kinds } - .take(limit) - - fun findExtensionsFor( - receiverTypeFqName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = libraryView.query(indexQuery { - eq(KEY_RECEIVER_TYPE, receiverTypeFqName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = limit - }) - - fun findTopLevelCallablesInPackage( - packageName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = libraryView.query(indexQuery { - eq(KEY_PACKAGE, packageName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = 0 - }).filter { it.kind.isCallable && it.isTopLevel }.take(limit) - - fun findClassifiersInPackage( - packageName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = libraryView.query(indexQuery { - eq(KEY_PACKAGE, packageName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = 0 - }).filter { it.kind.isClassifier }.take(limit) - - fun findMembersOf( - classFqName: String, namePrefix: String = "", limit: Int = 200, - ): Flow = libraryView.query(indexQuery { - eq(KEY_CONTAINING_CLASS, classFqName) - if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) - this.limit = limit - }) - - suspend fun findByFqName(fqName: String): JvmSymbol? = libraryView.get(fqName) - - fun allPackages(): Flow = libraryView.distinctValues(KEY_PACKAGE) - - suspend fun awaitLibraryIndexing() = libraryIndexer.awaitAll() - - override fun close() { - libraryCache.close() - libraryIndexer.close() - libraryView.close() - } -} \ No newline at end of file diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt index 81e74380d3..4e6cb46bac 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbol.kt @@ -68,6 +68,9 @@ data class JvmSymbol( val data: JvmSymbolInfo, ) : Indexable { + val fqName: String + get() = name.toFqName() + val isTopLevel: Boolean get() = data.containingClassName.isEmpty() diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt new file mode 100644 index 0000000000..2b3c044e9c --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt @@ -0,0 +1,143 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import org.appdevforall.codeonthego.indexing.FilteredIndex +import org.appdevforall.codeonthego.indexing.SQLiteIndex +import org.appdevforall.codeonthego.indexing.api.Index +import org.appdevforall.codeonthego.indexing.api.WritableIndex +import org.appdevforall.codeonthego.indexing.api.indexQuery +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_CONTAINING_CLASS +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_NAME +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_PACKAGE +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolDescriptor.KEY_RECEIVER_TYPE +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex.Companion.DB_NAME_DEFAULT +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex.Companion.INDEX_NAME_LIBRARY +import org.appdevforall.codeonthego.indexing.util.BackgroundIndexer +import java.io.Closeable + +/** + * An index of symbols from JVM source and binary files. + */ +class JvmSymbolIndex( + private val backing: Index, + private val indexer: BackgroundIndexer, +) : FilteredIndex(backing), WritableIndex by backing, Closeable { + + companion object { + + const val DB_NAME_DEFAULT = "jvm_symbol_index.db" + const val INDEX_NAME_LIBRARY = "jvm-library-cache" + + /** + * Create (or get) a JVM symbol index backed by SQLite. + * + * @param context The context to use for accessing the SQLite database. + * @param dbName The name of the database. Defaults to [DB_NAME_DEFAULT]. + * @param indexName The name of the index. Defaults to [INDEX_NAME_LIBRARY]. + */ + fun createSqliteIndex( + context: Context, + dbName: String, + indexName: String, + ): JvmSymbolIndex { + val cache = SQLiteIndex( + descriptor = JvmSymbolDescriptor, + context = context, + dbName = dbName, + name = indexName, + ) + + val indexer = BackgroundIndexer(cache) + return JvmSymbolIndex(cache, indexer) + } + } + + /** + * Index a single source. The [provider] returns a [Sequence] that + * lazily produces entries — it is consumed on [Dispatchers.IO] by + * [Index.insertAll]. + * + * If [skipIfExists] is true and the source is already indexed, + * this is a no-op. + * + * @param sourceId Identifies the source. + * @param skipIfExists Skip if already indexed. + * @param provider Lambda returning a [Sequence] of entries. + * @return The launched job. + */ + fun indexSource( + sourceId: String, + skipIfExists: Boolean = true, + provider: (sourceId: String) -> Sequence, + ): Job = indexer.indexSource(sourceId, skipIfExists, provider) + + /** + * Find symbols matching the given prefix. + * + * @param prefix The prefix to search for. + * @param limit The result limit. + * @see query + */ + fun findByPrefix(prefix: String, limit: Int = 200): Sequence = + query(indexQuery { prefix(KEY_NAME, prefix); this.limit = limit }) + + /** + * Find symbols having the given [receiver type][receiverTypeFqName]. + */ + fun findExtensionsFor( + receiverTypeFqName: String, + namePrefix: String = "", + limit: Int = 200, + ): Sequence = query(indexQuery { + eq(KEY_RECEIVER_TYPE, receiverTypeFqName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = limit + }) + + fun findTopLevelCallablesInPackage( + packageName: String, + namePrefix: String = "", + limit: Int = 200, + ): Sequence = query(indexQuery { + eq(KEY_PACKAGE, packageName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = 0 + }).filter { it.kind.isCallable && it.isTopLevel }.take(limit) + + fun findClassifiersInPackage( + packageName: String, + namePrefix: String = "", + limit: Int = 200, + ): Sequence = query(indexQuery { + eq(KEY_PACKAGE, packageName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = 0 + }).filter { it.kind.isClassifier }.take(limit) + + fun findMembersOf( + classFqName: String, + namePrefix: String = "", + limit: Int = 200, + ): Sequence = query(indexQuery { + eq(KEY_CONTAINING_CLASS, classFqName) + if (namePrefix.isNotEmpty()) prefix(KEY_NAME, namePrefix) + this.limit = limit + }) + + suspend fun findByKey(key: String): JvmSymbol? = get(key) + + fun allPackages(): Sequence = distinctValues(KEY_PACKAGE) + + suspend fun awaitIndexing() = indexer.awaitAll() + + override fun close() { + super.close() + if (backing is AutoCloseable) { + backing.close() + } + + indexer.close() + } +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt index 59dc810c8a..ab6d9c7f46 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt @@ -4,6 +4,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile import org.jetbrains.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.ClassReader import org.jetbrains.org.objectweb.asm.ClassVisitor @@ -50,6 +53,23 @@ object KotlinMetadataScanner { private val log = LoggerFactory.getLogger(KotlinMetadataScanner::class.java) + @OptIn(KaImplementationDetail::class) + fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Flow = flow { + val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) + for (vf in allFiles) { + if (!vf.name.endsWith(".class")) continue + if (vf.name == "module-info.class") continue + try { + vf.contentsToByteArray().inputStream().use { input -> + parseKotlinClass(input, sourceId)?.forEach { emit(it) } + } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", vf.path, e.message) + } + } + } + .flowOn(Dispatchers.IO) + fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { val jar = try { JarFile(jarPath.toFile()) @@ -109,7 +129,8 @@ object KotlinMetadataScanner { } private fun extractFromClass( - klass: KmClass, sourceId: String, + klass: KmClass, + sourceId: String, ): List { val symbols = mutableListOf() val className = klass.name @@ -345,7 +366,8 @@ object KotlinMetadataScanner { } private fun kmTypeToDisplayName(type: KmType): String { - val base = kmTypeToDisplayName(type).substringAfterLast('.') + val base = kmTypeToName(type).substringAfterLast('/') + .substringAfterLast('$') val args = type.arguments.mapNotNull { it.type?.let { t -> kmTypeToDisplayName(t) } } return buildString { append(base) @@ -392,6 +414,7 @@ object KotlinMetadataScanner { metadataVersion = value.copyOf() } } + "k" -> metadataKind = value as? Int "xi" -> extraInt = value as? Int "xs" -> extraString = value as? String diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadata.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadata.kt new file mode 100644 index 0000000000..754fbddcd3 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadata.kt @@ -0,0 +1,47 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import org.appdevforall.codeonthego.indexing.api.Indexable +import java.time.Instant + +/** + * Metadata for a single Kotlin source file. + * + * Stored in [KtFileMetadataIndex], one entry per `.kt` file discovered + * in the project source roots. The entry is keyed by [filePath] so that + * subsequent updates for the same file replace the previous record. + * + * @param filePath Absolute path to the `.kt` file. Acts as [key] and [sourceId]. + * @param packageFqName Fully-qualified package name declared in the file + * (empty string for the root / default package). + * @param lastModified Wall-clock time the file was last written to disk. + * @param modificationStamp Monotonically increasing stamp from the VFS or + * filesystem; used to detect stale cache entries + * without comparing file content. + * @param isIndexed Whether [symbolKeys] has been populated for this file. + * Files are inserted with `isIndexed = false` as a placeholder + * when first discovered; the indexer flips this to `true` + * after scanning and writing all symbols. + * @param symbolKeys The [Indexable.key] values of every [JvmSymbol] + * declared in this file that was written to the symbol + * index. Empty until [isIndexed] becomes `true`. + */ +data class KtFileMetadata( + val filePath: String, + val packageFqName: String, + val lastModified: Instant, + val modificationStamp: Long, + val isIndexed: Boolean = false, + val symbolKeys: List = emptyList(), +) : Indexable { + + companion object { + fun shouldBeSkipped(existing: KtFileMetadata? = null, new: KtFileMetadata): Boolean { + return existing != null && !existing.lastModified.isBefore(new.lastModified) && + existing.modificationStamp >= new.modificationStamp && + (new.modificationStamp != 0L || existing.modificationStamp == 0L) + } + } + + override val key: String get() = filePath + override val sourceId: String get() = filePath +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataDescriptor.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataDescriptor.kt new file mode 100644 index 0000000000..0453003f2b --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataDescriptor.kt @@ -0,0 +1,64 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import org.appdevforall.codeonthego.indexing.api.IndexDescriptor +import org.appdevforall.codeonthego.indexing.api.IndexField +import org.appdevforall.codeonthego.indexing.jvm.proto.JvmSymbolProtos +import java.time.Instant + +/** + * [IndexDescriptor] for [KtFileMetadata]. + * + * Queryable fields: + * - `package` : exact match, for package → file path lookups and package + * existence checks used by the Kotlin LSP declaration/package + * providers. + * - `isIndexed` : exact match ("true"/"false"), to enumerate files that still + * need their declaration keys populated. + * + * Non-queryable data (`lastModified`, `modificationStamp`, `declarationKeys`) + * is stored opaquely in the protobuf payload blob. + * + * Serialization uses the `KtFileData` message from `jvm_symbol.proto`. + * `lastModified` is stored as epoch-milliseconds; `modificationStamp` is stored + * as-is (a raw long). + */ +object KtFileMetadataDescriptor : IndexDescriptor { + + const val KEY_PACKAGE = "package" + const val KEY_IS_INDEXED = "isIndexed" + + override val name: String = "kt_file_metadata" + + override val fields: List = listOf( + IndexField(name = KEY_PACKAGE), + IndexField(name = KEY_IS_INDEXED), + ) + + override fun fieldValues(entry: KtFileMetadata): Map = mapOf( + KEY_PACKAGE to entry.packageFqName, + KEY_IS_INDEXED to entry.isIndexed.toString(), + ) + + override fun serialize(entry: KtFileMetadata): ByteArray = + JvmSymbolProtos.KtFileData.newBuilder() + .setPath(entry.filePath) + .setPackageFqName(entry.packageFqName) + .setLastModified(entry.lastModified.toEpochMilli()) + .setModificationStamp(entry.modificationStamp) + .setIndexed(entry.isIndexed) + .addAllSymbolKeys(entry.symbolKeys) + .build() + .toByteArray() + + override fun deserialize(bytes: ByteArray): KtFileMetadata { + val proto = JvmSymbolProtos.KtFileData.parseFrom(bytes) + return KtFileMetadata( + filePath = proto.path, + packageFqName = proto.packageFqName, + lastModified = Instant.ofEpochMilli(proto.lastModified), + modificationStamp = proto.modificationStamp, + isIndexed = proto.indexed, + symbolKeys = proto.symbolKeysList.toList(), + ) + } +} diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataIndex.kt new file mode 100644 index 0000000000..ea39b293d9 --- /dev/null +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataIndex.kt @@ -0,0 +1,141 @@ +package org.appdevforall.codeonthego.indexing.jvm + +import android.content.Context +import org.appdevforall.codeonthego.indexing.SQLiteIndex +import org.appdevforall.codeonthego.indexing.api.indexQuery +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataDescriptor.KEY_IS_INDEXED +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataDescriptor.KEY_PACKAGE +import java.io.Closeable + +/** + * An index of [KtFileMetadata] entries, one per Kotlin source file. + */ +class KtFileMetadataIndex private constructor( + private val backing: SQLiteIndex, +) : Closeable { + + companion object { + + /** + * Creates a [KtFileMetadataIndex] backed by an in-memory SQLite database. + * + * The [context] is required by the AndroidX SQLite helpers even for in-memory + * databases; it is not used for any file I/O. + */ + fun create( + context: Context, + dbName: String? = null + ): KtFileMetadataIndex = + KtFileMetadataIndex( + SQLiteIndex( + descriptor = KtFileMetadataDescriptor, + context = context, + dbName = null, + name = "kt-file-metadata", + ) + ) + } + + /** + * Insert or replace the metadata record for a single file. + * + * Because [KtFileMetadata.key] == [KtFileMetadata.filePath], the + * underlying `CONFLICT_REPLACE` strategy ensures this is a true upsert. + */ + suspend fun upsert(metadata: KtFileMetadata) = backing.insert(metadata) + + /** + * Remove the metadata record for [filePath]. + * + * No-op if the file is not in the index. + */ + suspend fun remove(filePath: String) = backing.removeBySource(filePath) + + /** + * Return the [KtFileMetadata] for [filePath], or `null` if the file is + * not present in the index. + */ + suspend fun get(filePath: String): KtFileMetadata? = backing.get(filePath) + + /** + * Return `true` if [filePath] has a record in the index. + */ + suspend fun contains(filePath: String): Boolean = backing.containsSource(filePath) + + /** + * Returns a [Sequence] of files whose declared package exactly matches + * [packageFqName]. + */ + fun getFilesForPackage(packageFqName: String): Sequence = + backing.query( + indexQuery { + eq(KEY_PACKAGE, packageFqName) + limit = 0 + } + ) + + /** + * Returns a [Sequence] of absolute file paths whose declared package exactly + * matches [packageFqName]. + */ + fun getFilePathsForPackage(packageFqName: String): Sequence = + getFilesForPackage(packageFqName).map { it.filePath } + + /** + * Returns `true` if at least one file with package [packageFqName] is + * present in the index. + * + * Pass an empty string for the root (default) package. + */ + fun packageExists(packageFqName: String): Boolean = + backing.query(indexQuery { + eq(KEY_PACKAGE, packageFqName) + limit = 1 + }).firstOrNull() != null + + /** + * Returns the simple names of the direct child packages of [packageFqName]. + * + * For example, if the index contains `com.example.foo`, `com.example.bar`, + * and `com.example.foo.sub`, then `getSubpackageNames("com.example")` returns + * `{"foo", "bar"}`. Pass an empty string to enumerate top-level packages. + * + * Implemented by scanning all distinct package names and extracting the + * first component after [packageFqName]. This is fast for typical Android + * projects (dozens of packages) and avoids a secondary SQL schema. + */ + fun getSubpackageNames(packageFqName: String): Set { + val prefix = if (packageFqName.isEmpty()) "" else "$packageFqName." + val result = mutableSetOf() + for (pkg in backing.distinctValues(KEY_PACKAGE)) { + if (pkg == packageFqName) continue + if (prefix.isNotEmpty() && !pkg.startsWith(prefix)) continue + val remainder = if (prefix.isEmpty()) pkg else pkg.removePrefix(prefix) + val firstComponent = remainder.substringBefore('.') + if (firstComponent.isNotEmpty()) result.add(firstComponent) + } + return result + } + + /** + * Returns a [Sequence] of all distinct package names present in the index. + * + * Useful for building a complete package tree or bulk validity checks. + */ + fun allPackages(): Sequence = backing.distinctValues(KEY_PACKAGE) + + /** + * Returns a [Sequence] of file paths that have been discovered but whose + * symbols have not yet been extracted ([KtFileMetadata.isIndexed] is `false`). + */ + fun getUnindexedFiles(): Sequence = + backing.query(indexQuery { + eq(KEY_IS_INDEXED, false.toString()) + limit = 0 + }).map { it.filePath } + + /** Remove all records from the index. */ + suspend fun clear() = backing.clear() + + override fun close() = backing.close() +} diff --git a/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto b/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto index 0c92e0ab8e..7f7e5517e6 100644 --- a/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto +++ b/lsp/jvm-symbol-models/src/main/proto/jvm_symbol.proto @@ -208,3 +208,12 @@ enum JvmVisibility { VISIBILITY_PRIVATE = 4; VISIBILITY_PACKAGE_PRIVATE = 5; } + +message KtFileData { + string path = 1; + string packageFqName = 2; + int64 lastModified = 3; + int64 modificationStamp = 4; + bool indexed = 5; + repeated string symbolKeys = 6; +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt index c7e9223978..46d1062f19 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt @@ -22,13 +22,14 @@ import com.itsaky.androidide.app.configuration.IJdkDistributionProvider import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent -import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.IServerSettings import com.itsaky.androidide.lsp.kotlin.compiler.Compiler import com.itsaky.androidide.lsp.kotlin.compiler.KotlinProjectModel +import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_META_INDEX_KEY import com.itsaky.androidide.lsp.kotlin.completion.complete import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor import com.itsaky.androidide.lsp.models.CompletionParams @@ -56,7 +57,9 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.appdevforall.codeonthego.indexing.jvm.JvmIndexingService +import org.appdevforall.codeonthego.indexing.jvm.JvmLibraryIndexingService +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -91,7 +94,6 @@ class KotlinLanguageServer : ILanguageServer { get() = _settings ?: KotlinServerSettings.getInstance().also { _settings = it } companion object { - private val ANALYZE_DEBOUNCE_DELAY = 400.milliseconds const val SERVER_ID = "ide.lsp.kotlin" @@ -124,19 +126,36 @@ class KotlinLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) + val context = BaseApplication.baseInstance val indexingServiceManager = ProjectManagerImpl.getInstance() .indexingServiceManager - val jvmIndexingService = - indexingServiceManager.getService(JvmIndexingService.ID) as? JvmIndexingService? - jvmIndexingService?.refresh() + val indexingRegistry = indexingServiceManager.registry + indexingRegistry.register( + key = KT_SOURCE_FILE_INDEX_KEY, + index = JvmSymbolIndex.createSqliteIndex( + context = context, + dbName = KT_SOURCE_FILE_INDEX_KEY.name, + indexName = KT_SOURCE_FILE_INDEX_KEY.name, + ) + ) + + indexingRegistry.register( + key = KT_SOURCE_FILE_META_INDEX_KEY, + index = KtFileMetadataIndex.create( + context = context, + dbName = KT_SOURCE_FILE_META_INDEX_KEY.name + ) + ) + + val jvmLibraryIndexingService = + indexingServiceManager.getService(JvmLibraryIndexingService.ID) as? JvmLibraryIndexingService? + + jvmLibraryIndexingService?.refresh() val jdkHome = Environment.JAVA_HOME.toPath() val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE - val intellijPluginRoot = Paths.get( - BaseApplication - .baseInstance.applicationInfo.sourceDir - ) + val intellijPluginRoot = Paths.get(context.applicationInfo.sourceDir) val jvmTarget = JvmTarget.fromString(IJdkDistributionProvider.DEFAULT_JAVA_VERSION) ?: JvmTarget.JVM_21 @@ -151,6 +170,7 @@ class KotlinLanguageServer : ILanguageServer { this.projectModel = model val compiler = Compiler( + workspace = workspace, projectModel = model, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, @@ -248,8 +268,7 @@ class KotlinLanguageServer : ILanguageServer { } compiler?.compilationEnvironmentFor(event.openedFile)?.apply { - val content = FileManager.getDocumentContents(event.openedFile) - fileManager.onFileOpened(event.openedFile, content) + onFileOpen(event.openedFile) } selectedFile = event.openedFile @@ -284,8 +303,7 @@ class KotlinLanguageServer : ILanguageServer { } compiler?.compilationEnvironmentFor(event.changedFile)?.apply { - val content = FileManager.getDocumentContents(event.changedFile) - fileManager.onFileContentChanged(event.changedFile, content) + onFileContentChanged(event.changedFile) } debouncingAnalyze() @@ -299,8 +317,7 @@ class KotlinLanguageServer : ILanguageServer { } compiler?.compilationEnvironmentFor(event.closedFile)?.apply { - fileManager.onFileClosed(event.closedFile) - fileManager.clearAnalyzeTimestampOf(event.closedFile) + onFileClosed(event.closedFile) } if (FileManager.getActiveDocumentCount() == 0) { @@ -309,18 +326,6 @@ class KotlinLanguageServer : ILanguageServer { } } - @Subscribe(threadMode = ThreadMode.ASYNC) - @Suppress("unused") - fun onDocumentSaved(event: DocumentSaveEvent) { - if (!DocumentUtils.isKotlinFile(event.savedFile)) { - return - } - - compiler?.compilationEnvironmentFor(event.savedFile)?.apply { - fileManager.onFileSaved(event.savedFile) - } - } - @Subscribe(threadMode = ThreadMode.ASYNC) @Suppress("unused") fun onDocumentSelected(event: DocumentSelectedEvent) { diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt deleted file mode 100644 index c711534897..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KtFileManager.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin - -import com.itsaky.androidide.projects.FileManager -import org.jetbrains.kotlin.analysis.api.KaSession -import org.jetbrains.kotlin.analysis.api.analyze -import org.jetbrains.kotlin.analysis.api.analyzeCopy -import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode -import org.jetbrains.kotlin.com.intellij.openapi.editor.Document -import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager -import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager -import org.jetbrains.kotlin.com.intellij.psi.PsiManager -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtPsiFactory -import org.slf4j.LoggerFactory -import java.nio.file.Path -import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.name -import kotlin.io.path.pathString -import kotlin.time.Clock -import kotlin.time.Instant - -/** - * Manages [KtFile] instances for all open files. - */ -class KtFileManager( - private val psiFactory: KtPsiFactory, - private val psiManager: PsiManager, - private val psiDocumentManager: PsiDocumentManager, -) : FileEventConsumer, AutoCloseable { - - companion object { - private val logger = LoggerFactory.getLogger(KtFileManager::class.java) - } - - private val entries = ConcurrentHashMap() - - @ConsistentCopyVisibility - data class ManagedFile @Deprecated("Use ManagedFile.create instead") internal constructor( - val file: Path, - val diskKtFile: KtFile, - @Volatile var inMemoryKtFile: KtFile, - val document: Document, - @Volatile var lastModified: Instant, - @Volatile var isDirty: Boolean, - @Volatile var analyzeTimestamp: Instant, - ) { - - /** - * Analyze this [ManagedFile] contents. - * - * @param action The analysis action. - */ - fun analyze(action: KaSession.(file: KtFile) -> R): R { - if (diskKtFile === inMemoryKtFile) { - return analyze(useSiteElement = inMemoryKtFile) { action(inMemoryKtFile) } - } - - return analyzeCopy( - useSiteElement = inMemoryKtFile, - resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF - ) { - action(inMemoryKtFile) - } - } - - fun createInMemoryFileWithContent(psiFactory: KtPsiFactory, content: String): KtFile { - val inMemoryFile = psiFactory.createFile(file.name, content) - inMemoryFile.originalFile = diskKtFile - return inMemoryFile - } - - companion object { - @Suppress("DEPRECATION") - fun create( - file: Path, - ktFile: KtFile, - document: Document, - inMemoryKtFile: KtFile = ktFile, - lastModified: Instant = Clock.System.now(), - isDirty: Boolean = false, - analyzeTimestamp: Instant = Instant.DISTANT_PAST, - ) = - ManagedFile( - file = file, - diskKtFile = ktFile, - inMemoryKtFile = inMemoryKtFile, - document = document, - lastModified = lastModified, - isDirty = isDirty, - analyzeTimestamp = analyzeTimestamp, - ) - } - } - - override fun onFileOpened(path: Path, content: String) { - logger.debug("onFileOpened: {}", path) - - entries[path]?.let { existing -> - logger.info("File is already opened, updating content") - updateDocumentContent(existing, content) - return - } - - val ktFile = resolveKtFile(path) - - if (ktFile == null) { - logger.warn("Cannot resolve KtFile for: {}", path) - return - } - - val document = getOrCreateDocument(ktFile) - if (document == null) { - logger.warn("Cannot obtain Document for: {}", path) - return - } - - logger.info("Creating managed file entry") - val entry = ManagedFile.create( - file = path, - ktFile = ktFile, - document = document, - ) - - entries[path] = entry - - updateDocumentContent(entry, content) - logger.debug("File opened and managed: {}", path) - } - - override fun onFileContentChanged(path: Path, content: String) { - logger.debug("onFileContentChanged: {}", path) - val entry = entries[path] ?: run { - logger.debug("Content changed for unmanaged file: {}. Ignoring.", path) - return - } - - updateDocumentContent(entry, content) - } - - override fun onFileSaved(path: Path) { - val entry = entries[path] ?: return - entry.isDirty = false - - logger.debug("File saved: {}", path) - } - - override fun onFileClosed(path: Path) { - entries.remove(path) ?: return - logger.debug("File closed: {}", path) - } - - fun getOpenFile(path: Path): ManagedFile? { - val managed = entries[path] - if (managed != null) { - return managed - } - - val activeDocument = FileManager.getActiveDocument(path) - if (activeDocument != null) { - // document is active, but we were not notified - // open it now - onFileOpened(path, activeDocument.content) - return entries[path] - } - - return null - } - - fun allOpenFiles(): Collection = - entries.values.toList() - - fun clearAnalyzeTimestampOf(file: Path) { - val managed = getOpenFile(file) ?: return - managed.analyzeTimestamp = Instant.DISTANT_PAST - } - - private fun resolveKtFile(path: Path): KtFile? { - val vfs = VirtualFileManager.getInstance() - .getFileSystem(StandardFileSystems.FILE_PROTOCOL) - - val virtualFile = vfs.refreshAndFindFileByPath(path.pathString) - ?: return null - - val psiFile = psiManager.findFile(virtualFile) - - return psiFile as? KtFile - } - - private fun getOrCreateDocument(ktFile: KtFile): Document? { - return psiDocumentManager.getDocument(ktFile) - } - - private fun updateDocumentContent(entry: ManagedFile, content: String) { - logger.info("Updating doc content for {}", entry.file) - - val normalized = content.replace("\r", "") - if (entry.inMemoryKtFile.text == normalized) return - - entry.inMemoryKtFile = entry.createInMemoryFileWithContent(psiFactory, content) - entry.lastModified = Clock.System.now() - entry.isDirty = true - } - - override fun close() { - entries.clear() - } -} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt index 714179a548..3c59cdf099 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt @@ -1,31 +1,74 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.KtFileManager +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence +import com.itsaky.androidide.lsp.kotlin.compiler.modules.backingFilePath +import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule +import com.itsaky.androidide.lsp.kotlin.compiler.registrar.LspServiceRegistrar +import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAccessibilityChecker +import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAnnotationsProvider +import com.itsaky.androidide.lsp.kotlin.compiler.services.KtLspService +import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider +import com.itsaky.androidide.lsp.kotlin.compiler.services.WriteAccessGuard +import com.itsaky.androidide.lsp.kotlin.compiler.services.latestLanguageVersionSettings import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker -import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex -import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import com.itsaky.androidide.projects.FileManager +import com.itsaky.androidide.projects.api.Workspace +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.jetbrains.kotlin.K1Deprecation import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory -import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModificationTrackerFactory +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDirectInheritorsProvider +import org.jetbrains.kotlin.analysis.api.platform.java.KotlinJavaModuleAccessibilityChecker +import org.jetbrains.kotlin.analysis.api.platform.java.KotlinJavaModuleAnnotationsProvider +import org.jetbrains.kotlin.analysis.api.platform.modification.KaElementModificationType +import org.jetbrains.kotlin.analysis.api.platform.modification.KaSourceModificationService +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackagePartProviderFactory import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderFactory -import org.jetbrains.kotlin.analysis.api.standalone.StandaloneAnalysisAPISession -import org.jetbrains.kotlin.analysis.api.standalone.base.declarations.KotlinStandaloneAnnotationsResolverFactory -import org.jetbrains.kotlin.analysis.api.standalone.base.declarations.KotlinStandaloneDeclarationProviderFactory -import org.jetbrains.kotlin.analysis.api.standalone.base.modification.KotlinStandaloneModificationTrackerFactory -import org.jetbrains.kotlin.analysis.api.standalone.base.packages.KotlinStandalonePackageProviderFactory -import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinModuleDependentsProvider +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.ApplicationServiceRegistration +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.StandaloneProjectFactory +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectExtensionPoints +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectModelServices +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.registerProjectServices import org.jetbrains.kotlin.cli.common.intellijPluginRoot import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.CliMetadataFinderFactory +import org.jetbrains.kotlin.cli.jvm.compiler.CliVirtualFileFinderFactory +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCliJavaFileManagerImpl +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreApplicationEnvironmentMode +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreProjectEnvironment +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesDynamicCompoundIndex +import org.jetbrains.kotlin.cli.jvm.index.JvmDependenciesIndexImpl +import org.jetbrains.kotlin.cli.jvm.index.SingleJavaFileRootsIndex +import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleFinder +import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleResolver +import org.jetbrains.kotlin.cli.jvm.modules.JavaModuleGraph import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.core.CorePackageIndex +import org.jetbrains.kotlin.com.intellij.ide.highlighter.JavaFileType +import org.jetbrains.kotlin.com.intellij.mock.MockApplication import org.jetbrains.kotlin.com.intellij.mock.MockProject -import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager +import org.jetbrains.kotlin.com.intellij.openapi.command.CommandProcessor +import org.jetbrains.kotlin.com.intellij.openapi.editor.impl.DocumentWriteAccessGuard +import org.jetbrains.kotlin.com.intellij.openapi.roots.PackageIndex import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer -import org.jetbrains.kotlin.com.intellij.openapi.util.SimpleModificationTracker -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.psi.ClassTypePointerFactory import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager import org.jetbrains.kotlin.com.intellij.psi.PsiManager +import org.jetbrains.kotlin.com.intellij.psi.impl.file.impl.JavaFileManager +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.PsiClassReferenceTypePointerFactory +import org.jetbrains.kotlin.com.intellij.psi.search.ProjectScope import org.jetbrains.kotlin.config.ApiVersion import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.LanguageFeature @@ -37,11 +80,15 @@ import org.jetbrains.kotlin.config.languageVersionSettings import org.jetbrains.kotlin.config.messageCollector import org.jetbrains.kotlin.config.moduleName import org.jetbrains.kotlin.config.useFir +import org.jetbrains.kotlin.load.kotlin.MetadataFinderFactory +import org.jetbrains.kotlin.load.kotlin.VirtualFileFinderFactory import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtPsiFactory import org.slf4j.LoggerFactory import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.isDirectory import kotlin.io.path.pathString /** @@ -52,8 +99,11 @@ import kotlin.io.path.pathString * @param jdkHome Path to the JDK installation directory. * @param jdkRelease The JDK release version at [jdkHome]. */ +@Suppress("UnstableApiUsage") +@OptIn(K1Deprecation::class) internal class CompilationEnvironment( - val project: KotlinProjectModel, + workspace: Workspace, + val ktProject: KotlinProjectModel, val intellijPluginRoot: Path, val jdkHome: Path, val jdkRelease: Int, @@ -62,38 +112,65 @@ internal class CompilationEnvironment( ) : KotlinProjectModel.ProjectModelListener, AutoCloseable { private var disposable = Disposer.newDisposable() - var session: StandaloneAnalysisAPISession - private set + val projectEnv: KotlinCoreProjectEnvironment - var parser: KtPsiFactory - private set + val applicationEnv: KotlinCoreApplicationEnvironment + get() = projectEnv.environment as KotlinCoreApplicationEnvironment - var fileManager: KtFileManager - private set + val application: MockApplication + get() = applicationEnv.application + + val project: MockProject + get() = projectEnv.project + + val parser: KtPsiFactory + val commandProcessor: CommandProcessor + val modules: List val psiManager: PsiManager - get() = PsiManager.getInstance(session.project) + get() = PsiManager.getInstance(project) val psiDocumentManager: PsiDocumentManager - get() = PsiDocumentManager.getInstance(session.project) + get() = PsiDocumentManager.getInstance(project) - val modificationTrackerFactory: KotlinModificationTrackerFactory - get() = session.project.getService(KotlinModificationTrackerFactory::class.java) + val libraryIndex: JvmSymbolIndex? + get() = ktProject.libraryIndex - val coreApplicationEnvironment: CoreApplicationEnvironment - get() = session.coreApplicationEnvironment + val requireLibraryIndex: JvmSymbolIndex + get() = checkNotNull(libraryIndex) - val symbolVisibilityChecker: SymbolVisibilityChecker? - get() = project.symbolVisibilityChecker + val sourceIndex: JvmSymbolIndex? + get() = ktProject.sourceIndex - val requireSymbolVisibilityChecker: SymbolVisibilityChecker - get() = checkNotNull(symbolVisibilityChecker) + val requireSourceIndex: JvmSymbolIndex + get() = checkNotNull(sourceIndex) - val libraryIndex: JvmLibrarySymbolIndex? - get() = project.libraryIndex + val fileIndex: KtFileMetadataIndex? + get() = ktProject.fileIndex - val requireLibraryIndex: JvmLibrarySymbolIndex - get() = checkNotNull(libraryIndex) + val requireFileIndex: KtFileMetadataIndex + get() = checkNotNull(fileIndex) + + val generatedIndex: JvmSymbolIndex? + get() = ktProject.generatedIndex + + val symbolVisibilityChecker: SymbolVisibilityChecker by lazy { + val provider = + project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider + SymbolVisibilityChecker(provider) + } + + val ktSymbolIndex by lazy { + KtSymbolIndex( + project = project, + modules = modules, + fileIndex = requireFileIndex, + sourceIndex = requireSourceIndex, + libraryIndex = requireLibraryIndex, + ) + } + + private val serviceRegistrars = listOf(LspServiceRegistrar) private val envMessageCollector = object : MessageCollector { override fun clear() { @@ -118,39 +195,156 @@ internal class CompilationEnvironment( } init { - session = buildSession() - parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) - fileManager = KtFileManager(parser, psiManager, psiDocumentManager) - project.addListener(this) - } + projectEnv = StandaloneProjectFactory + .createProjectEnvironment( + projectDisposable = disposable, + applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.Production, + compilerConfiguration = createCompilerConfiguration(), + ) - private fun buildSession(): StandaloneAnalysisAPISession { - val configuration = createCompilerConfiguration() + project.registerRWLock() - val session = buildStandaloneAnalysisAPISession( - projectDisposable = disposable, - unitTestMode = false, - compilerConfiguration = configuration, + ApplicationServiceRegistration.registerWithCustomRegistration( + application, + serviceRegistrars, ) { - buildKtModuleProvider { - this@CompilationEnvironment.project.configureModules(this) + registerApplicationServices(application, data = Unit) + } + + KotlinCoreEnvironment.registerProjectExtensionPoints(project.extensionArea) + + CoreApplicationEnvironment.registerExtensionPoint( + application.extensionArea, + ClassTypePointerFactory.EP_NAME, + ClassTypePointerFactory::class.java, + ) + + application.extensionArea.getExtensionPoint(ClassTypePointerFactory.EP_NAME) + .registerExtension(PsiClassReferenceTypePointerFactory(), application) + + CoreApplicationEnvironment.registerExtensionPoint( + application.extensionArea, + DocumentWriteAccessGuard.EP_NAME, + WriteAccessGuard::class.java, + ) + + serviceRegistrars.registerProjectExtensionPoints(project, data = Unit) + serviceRegistrars.registerProjectServices(project, data = Unit) + serviceRegistrars.registerProjectModelServices(project, disposable, data = Unit) + + modules = workspace.collectKtModules(project, applicationEnv) + + val librariesScope = ProjectScope.getLibrariesScope(project) + val libraryRoots = modules + .asFlatSequence() + .filterNot { it.isSourceModule } + .flatMap { libMod -> + libMod.computeFiles(extended = false) + .map { file -> JavaRoot(file, JavaRoot.RootType.BINARY) } } + .toList() + + val javaFileManager = + project.getService(JavaFileManager::class.java) as KotlinCliJavaFileManagerImpl + val javaModuleFinder = + CliJavaModuleFinder(jdkHome.toFile(), null, javaFileManager, project, jdkRelease) + val javaModuleGraph = JavaModuleGraph(javaModuleFinder) + val delegateJavaModuleResolver = + CliJavaModuleResolver(javaModuleGraph, emptyList(), emptyList(), project) + + val corePackageIndex = project.getService(PackageIndex::class.java) as CorePackageIndex + val packagePartProvider = JvmPackagePartProvider( + latestLanguageVersionSettings, + librariesScope + ).apply { + addRoots(libraryRoots, MessageCollector.NONE) } - return session - } + val (javaRoots, singleJavaFileRoots) = modules + .asFlatSequence() + .filter { it.isSourceModule } + .flatMap { it.contentRoots } + .mapNotNull { VirtualFileManager.getInstance().findFileByNioPath(it) } + .partition { it.isDirectory || it.extension != JavaFileType.DEFAULT_EXTENSION } + + val rootsIndex = + JvmDependenciesDynamicCompoundIndex(shouldOnlyFindFirstClass = true).apply { + addIndex( + JvmDependenciesIndexImpl( + libraryRoots + javaRoots.map { JavaRoot(it, JavaRoot.RootType.SOURCE) }, + shouldOnlyFindFirstClass = true + ) + ) - private fun rebuildSession() { - logger.info("Rebuilding analysis session") + indexedRoots.forEach { javaRoot -> + if (javaRoot.file.isDirectory) { + if (javaRoot.type == JavaRoot.RootType.SOURCE) { + javaFileManager.addToClasspath(javaRoot.file) + corePackageIndex.addToClasspath(javaRoot.file) + } else { + projectEnv.addSourcesToClasspath(javaRoot.file) + } + } + } + } - disposable.dispose() - disposable = Disposer.newDisposable() + javaFileManager.initialize( + index = rootsIndex, + packagePartProviders = listOf(packagePartProvider), + singleJavaFileRootsIndex = SingleJavaFileRootsIndex(singleJavaFileRoots.map { + JavaRoot( + it, + JavaRoot.RootType.SOURCE + ) + }), + usePsiClassFilesReading = true, + perfManager = null, + ) - session = buildSession() - parser = KtPsiFactory(session.project, eventSystemEnabled = enableParserEventSystem) + val fileFinderFactory = CliVirtualFileFinderFactory(rootsIndex, false, perfManager = null) - logger.info("Analysis session rebuilt") + with(project) { + registerService( + KotlinJavaModuleAccessibilityChecker::class.java, + JavaModuleAccessibilityChecker(delegateJavaModuleResolver) + ) + registerService( + KotlinJavaModuleAnnotationsProvider::class.java, + JavaModuleAnnotationsProvider(delegateJavaModuleResolver), + ) + registerService(VirtualFileFinderFactory::class.java, fileFinderFactory) + registerService( + MetadataFinderFactory::class.java, + CliMetadataFinderFactory(fileFinderFactory) + ) + } + + // Setup platform services + val lspServices = listOf( + KotlinModuleDependentsProvider::class.java, + KotlinProjectStructureProvider::class.java, + KotlinPackageProviderFactory::class.java, + KotlinDeclarationProviderFactory::class.java, + KotlinPackagePartProviderFactory::class.java, + KotlinAnnotationsResolverFactory::class.java, + KotlinDirectInheritorsProvider::class.java, + ) + + for (lspService in lspServices) { + (project.getService(lspService) as KtLspService).setupWith( + project = project, + index = ktSymbolIndex, + modules = modules, + libraryRoots = libraryRoots + ) + } + + commandProcessor = application.getService(CommandProcessor::class.java) + parser = KtPsiFactory(project, eventSystemEnabled = enableParserEventSystem) + + // Sync the index in the background + ktSymbolIndex.syncIndexInBackground() } private fun createCompilerConfiguration(): CompilerConfiguration { @@ -172,81 +366,42 @@ internal class CompilationEnvironment( } } - private fun refreshSourceFiles() { - logger.info("Refreshing source files") - - val project = session.project - val sourceKtFiles = collectSourceKtFiles() - - ApplicationManager.getApplication().runWriteAction { - (project as MockProject).apply { - registerService( - KotlinAnnotationsResolverFactory::class.java, - KotlinStandaloneAnnotationsResolverFactory(this, sourceKtFiles) - ) - - val decProviderFactory = KotlinStandaloneDeclarationProviderFactory( - this, - session.coreApplicationEnvironment, - sourceKtFiles - ) - registerService( - KotlinDeclarationProviderFactory::class.java, - decProviderFactory - ) + fun onFileOpen(path: Path) { + val ktFile = loadKtFile(path) ?: return + ktSymbolIndex.openKtFile(path, ktFile) + } - registerService( - KotlinPackageProviderFactory::class.java, - KotlinStandalonePackageProviderFactory( - project, - sourceKtFiles + decProviderFactory.getAdditionalCreatedKtFiles() - ) - ) - } + fun onFileClosed(path: Path) { + ktSymbolIndex.closeKtFile(path) + (project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider) + .unregisterInMemoryFile(path.pathString) + } - val modificationTrackerFactory = - project.getService(KotlinModificationTrackerFactory::class.java) as? KotlinStandaloneModificationTrackerFactory? - val sourceModificationTracker = - modificationTrackerFactory?.createProjectWideSourceModificationTracker() as? SimpleModificationTracker? - sourceModificationTracker?.incModificationCount() - } + fun onFileContentChanged(path: Path) { + val newContent = FileManager.getDocumentContents(path) + val newKtFile = project.read { parser.createFile(path.pathString, newContent) } + newKtFile.backingFilePath = path - logger.info("Refreshed: {} source KtFiles", sourceKtFiles.size) - } + // Tell ProjectStructureProvider which module owns this LightVirtualFile. + val provider = + project.getService(KotlinProjectStructureProvider::class.java) as ProjectStructureProvider + provider.registerInMemoryFile(path.pathString, newKtFile.virtualFile) - @OptIn(KaExperimentalApi::class) - private fun collectSourceKtFiles(): List = buildList { - session.modulesWithFiles.keys.forEach { module -> - module.psiRoots.forEach { psiRoot -> - val rootFile = psiRoot.virtualFile ?: return@forEach - rootFile.refresh(false, false) - collectKtFilesRecursively(rootFile, this) - } + ktSymbolIndex.openKtFile(path, newKtFile) + project.write { + KaSourceModificationService.getInstance(project) + .handleElementModification(newKtFile, KaElementModificationType.Unknown) } } - private fun collectKtFilesRecursively( - dir: VirtualFile, - files: MutableList - ) { - dir.children.orEmpty().forEach { child -> - if (child.isDirectory) { - collectKtFilesRecursively(child, files) - return@forEach - } - - if (child.extension == "kt" || child.extension == "kts") { - val psiFile = psiManager.findFile(child) - if (psiFile is KtFile) { - files.add(psiFile) - } - } - } + private fun loadKtFile(path: Path): KtFile? { + val virtualFile = + project.read { VirtualFileManager.getInstance().findFileByNioPath(path) } ?: return null + return project.read { psiManager.findFile(virtualFile) as? KtFile } } override fun close() { - fileManager.close() - project.removeListener(this) + ktProject.removeListener(this) disposable.dispose() } @@ -254,9 +409,5 @@ internal class CompilationEnvironment( model: KotlinProjectModel, changeKind: KotlinProjectModel.ChangeKind ) { - when (changeKind) { - KotlinProjectModel.ChangeKind.STRUCTURE -> rebuildSession() - KotlinProjectModel.ChangeKind.SOURCES -> refreshSourceFiles() - } } } \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt index 9d501b0d65..977e1e61aa 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt @@ -1,5 +1,6 @@ package com.itsaky.androidide.lsp.kotlin.compiler +import com.itsaky.androidide.projects.api.Workspace import com.itsaky.androidide.utils.DocumentUtils import org.jetbrains.kotlin.com.intellij.lang.Language import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems @@ -17,6 +18,7 @@ import java.nio.file.Paths import kotlin.io.path.pathString internal class Compiler( + workspace: Workspace, projectModel: KotlinProjectModel, intellijPluginRoot: Path, jdkHome: Path, @@ -35,7 +37,8 @@ internal class Compiler( init { defaultCompilationEnv = CompilationEnvironment( - project = projectModel, + workspace = workspace, + ktProject = projectModel, intellijPluginRoot = intellijPluginRoot, jdkHome = jdkHome, jdkRelease = jdkRelease, @@ -66,7 +69,7 @@ internal class Compiler( } fun psiFileFactoryFor(compilationKind: CompilationKind): PsiFileFactory = - PsiFileFactory.getInstance(compilationEnvironmentFor(compilationKind).session.project) + PsiFileFactory.getInstance(compilationEnvironmentFor(compilationKind).project) fun createPsiFileFor( content: String, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt index 12e11306d4..f109360c3e 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt @@ -1,23 +1,16 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker +import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_META_INDEX_KEY import com.itsaky.androidide.projects.ProjectManagerImpl -import com.itsaky.androidide.projects.api.AndroidModule -import com.itsaky.androidide.projects.api.ModuleProject import com.itsaky.androidide.projects.api.Workspace -import com.itsaky.androidide.projects.models.bootClassPaths +import org.appdevforall.codeonthego.indexing.jvm.JVM_GENERATED_SYMBOL_INDEX import org.appdevforall.codeonthego.indexing.jvm.JVM_LIBRARY_SYMBOL_INDEX -import org.appdevforall.codeonthego.indexing.jvm.JvmLibrarySymbolIndex -import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule -import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule -import org.jetbrains.kotlin.analysis.project.structure.builder.KtModuleProviderBuilder -import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule -import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex import org.jetbrains.kotlin.platform.TargetPlatform import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.slf4j.LoggerFactory -import java.nio.file.Path -import kotlin.io.path.nameWithoutExtension /** * Holds the project structure derived from a [Workspace]. @@ -36,23 +29,36 @@ internal class KotlinProjectModel { private var workspace: Workspace? = null private var platform: TargetPlatform = JvmPlatforms.defaultJvmPlatform - private var _moduleResolver: ModuleResolver? = null - private var _symbolVisibilityChecker: SymbolVisibilityChecker? = null private val listeners = mutableListOf() - val moduleResolver: ModuleResolver? - get() = _moduleResolver - - val symbolVisibilityChecker: SymbolVisibilityChecker? - get() = _symbolVisibilityChecker - - val libraryIndex: JvmLibrarySymbolIndex? + val libraryIndex: JvmSymbolIndex? get() = ProjectManagerImpl.getInstance() .indexingServiceManager .registry .get(JVM_LIBRARY_SYMBOL_INDEX) + val sourceIndex: JvmSymbolIndex? + get() = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(KT_SOURCE_FILE_INDEX_KEY) + + val fileIndex: KtFileMetadataIndex? + get() = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(KT_SOURCE_FILE_META_INDEX_KEY) + + val generatedIndex: JvmSymbolIndex? + get() = ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(JVM_GENERATED_SYMBOL_INDEX) + /** * The kind of change that occurred. */ @@ -98,97 +104,6 @@ internal class KotlinProjectModel { notifyListeners(ChangeKind.SOURCES) } - /** - * Configures a [KtModuleProviderBuilder] with the current project structure. - * - * Called by [CompilationEnvironment] during session creation or rebuild. - * This is where the module/dependency graph is constructed — the same logic - * currently in [KotlinLanguageServer.recreateSession], but centralized here. - */ - fun configureModules(builder: KtModuleProviderBuilder) { - val workspace = this.workspace - ?: throw IllegalStateException("Project model not initialized") - - builder.apply { - this.platform = this@KotlinProjectModel.platform - - val moduleProjects = workspace.subProjects - .asSequence() - .filterIsInstance() - .filter { it.path != workspace.rootProject.path } - - val jarToModMap = mutableMapOf() - - fun addLibrary(path: Path): KaLibraryModule { - val module = addModule(buildKtLibraryModule { - this.platform = this@KotlinProjectModel.platform - this.libraryName = path.nameWithoutExtension - addBinaryRoot(path) - }) - - jarToModMap[path] = module - return module - } - - val bootClassPaths = moduleProjects - .filterIsInstance() - .flatMap { project -> - project.bootClassPaths - .asSequence() - .filter { it.exists() } - .map { it.toPath() } - .map(::addLibrary) - } - - val libraryDependencies = moduleProjects - .flatMap { it.getCompileClasspaths() } - .filter { it.exists() } - .map { it.toPath() } - .associateWith(::addLibrary) - - val subprojectsAsModules = mutableMapOf() - - fun getOrCreateModule(project: ModuleProject): KaSourceModule { - subprojectsAsModules[project]?.let { return it } - - val sourceRoots = project.getSourceDirectories().map { it.toPath() } - val module = buildKtSourceModule { - this.platform = this@KotlinProjectModel.platform - this.moduleName = project.name - addSourceRoots(sourceRoots) - - bootClassPaths.forEach { addRegularDependency(it) } - - project.getCompileClasspaths(excludeSourceGeneratedClassPath = true) - .forEach { classpath -> - val libDep = libraryDependencies[classpath.toPath()] - if (libDep == null) { - logger.error( - "Skipping non-existent classpath classpath: {}", - classpath - ) - return@forEach - } - addRegularDependency(libDep) - } - - project.getCompileModuleProjects().forEach { dep -> - addRegularDependency(getOrCreateModule(dep)) - } - } - - subprojectsAsModules[project] = module - return module - } - - moduleProjects.forEach { addModule(getOrCreateModule(it)) } - - val moduleResolver = ModuleResolver(jarMap = jarToModMap) - _moduleResolver = moduleResolver - _symbolVisibilityChecker = SymbolVisibilityChecker(moduleResolver) - } - } - private fun notifyListeners(changeKind: ChangeKind) { logger.info("Notifying project listeners for change: {}", changeKind) listeners.forEach { it.onProjectModelChanged(this, changeKind) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt deleted file mode 100644 index 704d02978a..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ModuleResolver.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler - -import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule -import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule -import org.slf4j.LoggerFactory -import java.nio.file.Path -import java.nio.file.Paths - -internal class ModuleResolver( - private val jarMap: Map, -) { - companion object { - private val logger = LoggerFactory.getLogger(ModuleResolver::class.java) - } - - /** - * Find the module that declares the given source ID (JAR, source file, etc.) - */ - fun findDeclaringModule(sourceId: String): KaModule? { - val path = Paths.get(sourceId) - jarMap[path]?.let { return it } - - return null - } -} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt new file mode 100644 index 0000000000..bd9c000672 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt @@ -0,0 +1,24 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.util.Key +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +private val key = Key.create("org.adfa.cotg.rwlock") +private val lock = ReentrantReadWriteLock() + +fun Project.registerRWLock() { + putUserData(key, lock) +} + +fun Project.read(fn: () -> T): T { + val lock = getUserData(key)!! + return lock.read(fn) +} + +fun Project.write(fn: () -> T): T { + val lock = getUserData(key)!! + return lock.write(fn) +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt new file mode 100644 index 0000000000..7d07076842 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt @@ -0,0 +1,93 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtLibraryModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtSourceModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtLibraryModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtSourceModule +import com.itsaky.androidide.projects.api.AndroidModule +import com.itsaky.androidide.projects.api.ModuleProject +import com.itsaky.androidide.projects.api.Workspace +import com.itsaky.androidide.projects.models.bootClassPaths +import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.slf4j.LoggerFactory +import java.nio.file.Path +import kotlin.io.path.pathString + +private val logger = LoggerFactory.getLogger("WorkspaceExts") + +internal fun Workspace.collectKtModules( + project: Project, + appEnv: CoreApplicationEnvironment +): List = buildList { + fun addModule(module: KtModule) = add(module) + + val moduleProjects = subProjects + .asSequence() + .filterIsInstance() + .filter { it.path != rootProject.path } + + val jarToModMap = mutableMapOf() + + fun addLibrary(path: Path): KtLibraryModule { + val module = buildKtLibraryModule(project, appEnv) { + id = path.pathString + addContentRoot(path) + } + jarToModMap[path] = module + return module + } + + val bootClassPaths = moduleProjects + .filterIsInstance() + .flatMap { project -> + project.bootClassPaths + .asSequence() + .filter { it.exists() } + .map { it.toPath() } + .map(::addLibrary) + } + + val libraryDependencies = moduleProjects + .flatMap { it.getCompileClasspaths() } + .filter { it.exists() } + .map { it.toPath() } + .associateWith(::addLibrary) + + val subprojectsAsModules = mutableMapOf() + val sourceRootToModuleMap = mutableMapOf() + + fun getOrCreateModule(moduleProject: ModuleProject): KtSourceModule { + subprojectsAsModules[moduleProject]?.let { return it } + + val module = buildKtSourceModule(project) { + this.module = moduleProject + + bootClassPaths.forEach { addDependency(it) } + + moduleProject.getCompileClasspaths(excludeSourceGeneratedClassPath = true) + .forEach { classpath -> + val libDep = libraryDependencies[classpath.toPath()] + if (libDep == null) { + logger.error( + "Skipping non-existent classpath classpath: {}", + classpath + ) + return@forEach + } + addDependency(libDep) + } + + moduleProject.getCompileModuleProjects().forEach { dep -> + addDependency(getOrCreateModule(dep)) + } + } + + subprojectsAsModules[moduleProject] = module + module.contentRoots.forEach { root -> sourceRootToModuleMap[root] = module } + return module + } + + moduleProjects.forEach { addModule(getOrCreateModule(it)) } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt new file mode 100644 index 0000000000..1d890426c1 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt @@ -0,0 +1,13 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.psi.KtFile + +internal sealed interface IndexCommand { + data object Stop : IndexCommand + data object SourceScanningComplete: IndexCommand + data object IndexingComplete: IndexCommand + data class ScanSourceFile(val vf: VirtualFile): IndexCommand + data class IndexModifiedFile(val ktFile: KtFile): IndexCommand + data class IndexSourceFile(val vf: VirtualFile): IndexCommand +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt new file mode 100644 index 0000000000..3be16fdecc --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt @@ -0,0 +1,101 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.itsaky.androidide.lsp.kotlin.compiler.read +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadata +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.PsiManager +import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory + +internal class IndexWorker( + private val project: Project, + private val queue: WorkerQueue, + private val fileIndex: KtFileMetadataIndex, + private val sourceIndex: JvmSymbolIndex, +) { + companion object { + private val logger = LoggerFactory.getLogger(IndexWorker::class.java) + } + + suspend fun start() { + var scanCount = 0 + var sourceIndexCount = 0 + + while (true) { + when (val command = queue.take()) { + is IndexCommand.IndexSourceFile -> { + if (command.vf.fileSystem.protocol != "file") { + logger.warn("Unknown source file protocol: {}", command.vf.path) + continue + } + + val ktFile = project.read { + PsiManager.getInstance(project) + .findFile(command.vf) as? KtFile + } + + if (ktFile == null) { + // probably a non-kotlin file + continue + } + + indexSourceFile(project, ktFile, fileIndex, sourceIndex) + sourceIndexCount++ + } + + is IndexCommand.IndexModifiedFile -> { + indexSourceFile(project, command.ktFile, fileIndex, sourceIndex) + sourceIndexCount++ + } + + IndexCommand.IndexingComplete -> { + logger.info( + "Indexing complete: scanned={}, sourceIndexCount={}", + scanCount, + sourceIndexCount, + ) + } + + is IndexCommand.ScanSourceFile -> { + val ktFile = project.read { + PsiManager.getInstance(project).findFile(command.vf) as? KtFile + } + ?: continue + + val newFile = ktFile.toMetadata(project, isIndexed = false) + val existingFile = fileIndex.get(newFile.filePath) + if (KtFileMetadata.shouldBeSkipped(existingFile, newFile)) { + continue + } + + fileIndex.upsert(newFile) + scanCount++ + } + + IndexCommand.SourceScanningComplete -> { + logger.info("Scanning complete. Found {} files to index.", scanCount) + } + + IndexCommand.Stop -> break + } + } + } + + suspend fun submitCommand(cmd: IndexCommand) { + when (cmd) { + is IndexCommand.ScanSourceFile, IndexCommand.SourceScanningComplete -> { + queue.putScanQueue(cmd) + } + + is IndexCommand.IndexModifiedFile -> { + queue.putEditQueue(cmd) + } + + else -> { + queue.putIndexQueue(cmd) + } + } + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt new file mode 100644 index 0000000000..822cc0c296 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt @@ -0,0 +1,141 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.github.benmanes.caffeine.cache.Caffeine +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.read +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.appdevforall.codeonthego.indexing.service.IndexKey +import org.checkerframework.checker.index.qual.NonNegative +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.psi.PsiManager +import org.jetbrains.kotlin.psi.KtFile +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap + +val KT_SOURCE_FILE_INDEX_KEY = IndexKey("kt-source-file-index") +val KT_SOURCE_FILE_META_INDEX_KEY = IndexKey("kt-source-file-meta-index") + +/** + * An index of symbols from Kotlin source files and JARs. + * + * NOTE: This index does not own the provided [fileIndex], [sourceIndex] and [libraryIndex]. + * Callers are responsible for closing the provided indexes. + */ +internal class KtSymbolIndex( + val project: Project, + modules: List, + val fileIndex: KtFileMetadataIndex, + val sourceIndex: JvmSymbolIndex, + val libraryIndex: JvmSymbolIndex, + cacheSize: @NonNegative Long = DEFAULT_CACHE_SIZE, + private val scope: CoroutineScope = CoroutineScope( + Dispatchers.Default + SupervisorJob() + CoroutineName( + "KtSymbolIndex" + ) + ) +) { + companion object { + const val DEFAULT_CACHE_SIZE = 100L + } + + private val workerQueue = WorkerQueue() + private val indexWorker = IndexWorker( + project = project, + queue = workerQueue, + fileIndex = fileIndex, + sourceIndex = sourceIndex, + ) + + private val scanningWorker = ScanningWorker( + sourceIndex = sourceIndex, + indexWorker = indexWorker, + modules = modules, + ) + + private var scanningJob: Job? = null + private var indexingJob: Job? = null + + private val ktFileCache = Caffeine + .newBuilder() + .maximumSize(cacheSize) + .build() + + private val openedFiles = ConcurrentHashMap() + + val openedKtFiles: Sequence> + get() = openedFiles.asSequence() + + fun syncIndexInBackground() { + // TODO: Figure out how to handle already-running scanning/indexing jobs. + + indexingJob = scope.launch { + indexWorker.start() + } + + scanningJob = scope.launch(Dispatchers.IO) { + scanningWorker.start() + } + } + + fun queueOnFileChangedAsync(ktFile: KtFile) { + scope.launch { + queueOnFileChanged(ktFile) + } + } + + suspend fun queueOnFileChanged(ktFile: KtFile) { + indexWorker.submitCommand(IndexCommand.IndexModifiedFile(ktFile)) + } + + fun openKtFile(path: Path, ktFile: KtFile) { + openedFiles[path] = ktFile + } + + fun closeKtFile(path: Path) { + openedFiles.remove(path) + } + + fun getOpenedKtFile(path: Path) = openedFiles[path] + + fun getKtFile(vf: VirtualFile): KtFile { + val path = vf.toNioPath() + + openedFiles[path]?.also { return it } + ktFileCache.getIfPresent(path)?.also { return it } + + val ktFile = loadKtFile(vf) + + ktFileCache.put(path, ktFile) + return ktFile + } + + private fun loadKtFile(vf: VirtualFile): KtFile = project.read { + PsiManager.getInstance(project) + .findFile(vf) as KtFile + } + + suspend fun close() { + scanningWorker.stop() + indexWorker.submitCommand(IndexCommand.Stop) + + scanningJob?.join() + indexingJob?.join() + } +} + +internal fun KtSymbolIndex.packageExistsInSource(packageFqn: String) = + fileIndex.packageExists(packageFqn) + +internal fun KtSymbolIndex.filesForPackage(packageFqn: String) = + fileIndex.getFilesForPackage(packageFqn) + +internal fun KtSymbolIndex.subpackageNames(packageFqn: String) = + fileIndex.getSubpackageNames(packageFqn) \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt new file mode 100644 index 0000000000..0f5a7c7db3 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt @@ -0,0 +1,59 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence +import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean + +internal class ScanningWorker( + private val sourceIndex: JvmSymbolIndex, + private val indexWorker: IndexWorker, + private val modules: List, +) { + + companion object { + private val logger = LoggerFactory.getLogger(ScanningWorker::class.java) + } + + private val isRunning = AtomicBoolean(false) + + suspend fun start() { + isRunning.set(true) + try { + scan() + } finally { + isRunning.set(false) + } + } + + private suspend fun scan() { + val sourceFiles = modules.asFlatSequence() + .filter { it.isSourceModule } + .flatMap { it.computeFiles(extended = true) } + .takeWhile { isRunning.get() } + .toList() + + sourceIndex.setActiveSources(sourceFiles.asSequence().map { it.path }.toSet()) + + for (sourceFile in sourceFiles) { + if (!isRunning.get()) return + indexWorker.submitCommand(IndexCommand.ScanSourceFile(sourceFile)) + } + + indexWorker.submitCommand(IndexCommand.SourceScanningComplete) + + sourceFiles.asSequence() + .takeWhile { isRunning.get() } + .forEach { sourceFile -> + indexWorker.submitCommand(IndexCommand.IndexSourceFile(sourceFile)) + } + + indexWorker.submitCommand(IndexCommand.IndexingComplete) + } + + fun stop() { + isRunning.set(false) + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt new file mode 100644 index 0000000000..95eaeb533f --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt @@ -0,0 +1,414 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.itsaky.androidide.lsp.kotlin.compiler.read +import org.appdevforall.codeonthego.indexing.jvm.JvmClassInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmFieldInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmFunctionInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmParameterInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmSourceLanguage +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolKind +import org.appdevforall.codeonthego.indexing.jvm.JvmTypeAliasInfo +import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility +import org.appdevforall.codeonthego.indexing.jvm.KotlinClassInfo +import org.appdevforall.codeonthego.indexing.jvm.KotlinFunctionInfo +import org.appdevforall.codeonthego.indexing.jvm.KotlinPropertyInfo +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadata +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaSession +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaPropertySymbol +import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol +import org.jetbrains.kotlin.analysis.api.symbols.typeParameters +import org.jetbrains.kotlin.analysis.api.types.KaClassType +import org.jetbrains.kotlin.analysis.api.types.KaType +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtModifierListOwner +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtParameter +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.KtTypeAlias +import java.time.Instant +import kotlin.io.path.pathString + +internal fun KtFile.toMetadata(project: Project, isIndexed: Boolean = false): KtFileMetadata = + project.read { + KtFileMetadata( + filePath = virtualFile.toNioPath().pathString, + packageFqName = packageFqName.asString(), + lastModified = Instant.ofEpochMilli(virtualFile.timeStamp), + modificationStamp = modificationStamp, + isIndexed = isIndexed, + symbolKeys = emptyList() + ) + } + +internal suspend fun indexSourceFile( + project: Project, + ktFile: KtFile, + fileIndex: KtFileMetadataIndex, + symbolsIndex: JvmSymbolIndex, +) { + val newFile = ktFile.toMetadata(project, isIndexed = true) + val existingFile = fileIndex.get(newFile.filePath) + if (KtFileMetadata.shouldBeSkipped(existingFile, newFile) && existingFile?.isIndexed == true) { + return + } + + // Remove stale symbols written during the previous indexing pass. + if (existingFile?.isIndexed == true) { + symbolsIndex.removeBySource(newFile.filePath) + } + + val symbols = project.read { + val list = mutableListOf() + ktFile.accept(object : KtTreeVisitorVoid() { + override fun visitDeclaration(dcl: KtDeclaration) { + val symbol = analyze(dcl) { + analyzeDeclaration(newFile.filePath, dcl) + } + symbol?.let { list.add(it) } + super.visitDeclaration(dcl) + } + }) + list + } + + symbolsIndex.insertAll(symbols.asSequence()) + fileIndex.upsert(newFile.copy(symbolKeys = symbols.map { it.key })) +} + +private fun KaSession.analyzeDeclaration(filePath: String, dcl: KtDeclaration): JvmSymbol? { + dcl.name ?: return null + return when (dcl) { + is KtNamedFunction -> analyzeFunction(filePath, dcl) + is KtClassOrObject -> analyzeClassOrObject(filePath, dcl) + is KtParameter -> analyzeParameter(filePath, dcl) + is KtProperty -> analyzeProperty(filePath, dcl) + is KtTypeAlias -> analyzeTypeAlias(filePath, dcl) + else -> null + } +} + +/** + * Slash-package / dollar-nesting internal name for this class. + * Returns null for anonymous/local classes that have no stable FQ name. + */ +private fun KtClassOrObject.internalName(): String? { + val pkg = containingKtFile.packageFqName.asString() + val fqName = fqName?.asString() ?: return null + val relative = if (pkg.isEmpty()) fqName else fqName.removePrefix("$pkg.") + return if (pkg.isEmpty()) relative.replace('.', '$') + else "${pkg.replace('.', '/')}/${relative.replace('.', '$')}" +} + +/** + * Walk the PSI parent chain to find the internal name of the nearest + * enclosing class or object. Returns null for top-level declarations. + */ +private fun KtDeclaration.containingClassInternalName(): String? { + var p = parent + while (p != null) { + if (p is KtClassOrObject) return p.internalName() + p = p.parent + } + return null +} + +private fun KtModifierListOwner.jvmVisibility(): JvmVisibility = when { + hasModifier(KtTokens.PRIVATE_KEYWORD) -> JvmVisibility.PRIVATE + hasModifier(KtTokens.PROTECTED_KEYWORD) -> JvmVisibility.PROTECTED + hasModifier(KtTokens.INTERNAL_KEYWORD) -> JvmVisibility.INTERNAL + else -> JvmVisibility.PUBLIC +} + +/** + * Slash-package / dollar-nesting internal name for a resolved [KaType]. + * Mirrors [KotlinMetadataScanner]'s `kmTypeToName`. + * Returns an empty string for unresolvable types (type parameters, errors). + */ +private fun KaSession.kaTypeInternalName(type: KaType): String { + if (type !is KaClassType) return "" + val classId = type.classId + val pkg = classId.packageFqName.asString() + val rel = classId.relativeClassName.asString() + return if (pkg.isEmpty()) rel.replace('.', '$') + else "${pkg.replace('.', '/')}/${rel.replace('.', '$')}" +} + +/** + * Short display name (last segment after '/' and '$'), with generic arguments + * and a trailing '?' for nullable types. + * Mirrors [KotlinMetadataScanner]'s `kmTypeToDisplayName`. + */ +private fun KaSession.kaTypeDisplayName(type: KaType): String { + if (type !is KaClassType) return "" + val base = kaTypeInternalName(type).substringAfterLast('/').substringAfterLast('$') + val args = type.typeArguments.mapNotNull { it.type?.let { t -> kaTypeDisplayName(t) } } + return buildString { + append(base) + if (args.isNotEmpty()) append("<${args.joinToString(", ")}>") + if (type.isMarkedNullable) append("?") + } +} + +private fun KaSession.analyzeFunction(filePath: String, dcl: KtNamedFunction): JvmSymbol? { + val fnName = dcl.name ?: return null + val visibility = dcl.jvmVisibility() + if (visibility == JvmVisibility.PRIVATE) return null + + val pkg = dcl.containingKtFile.packageFqName.asString() + val containingClass = dcl.containingClassInternalName() + + val fnSymbol = dcl.symbol as? KaNamedFunctionSymbol ?: return null + + val parameters = fnSymbol.valueParameters.map { param -> + JvmParameterInfo( + name = param.name.asString(), + typeName = kaTypeInternalName(param.returnType), + typeDisplayName = kaTypeDisplayName(param.returnType), + hasDefaultValue = param.hasDefaultValue, + isVararg = param.isVararg, + ) + } + + val receiverType = fnSymbol.receiverParameter?.returnType + val returnType = fnSymbol.returnType + + // Mirrors KotlinMetadataScanner.extractFunction key / name conventions. + val qualifiedName = if (containingClass != null) "$containingClass#$fnName" + else "$pkg#$fnName" + val key = "$qualifiedName(${parameters.joinToString(",") { it.typeFqName }})" + + val signatureDisplay = buildString { + append("(") + append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) + append("): ") + append(kaTypeDisplayName(returnType)) + } + + return JvmSymbol( + key = key, + sourceId = filePath, + name = qualifiedName, + shortName = fnName, + packageName = pkg, + kind = if (receiverType != null) JvmSymbolKind.EXTENSION_FUNCTION else JvmSymbolKind.FUNCTION, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmFunctionInfo( + containingClassName = containingClass ?: "", + returnTypeName = kaTypeInternalName(returnType), + returnTypeDisplayName = kaTypeDisplayName(returnType), + parameterCount = parameters.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + typeParameters = fnSymbol.typeParameters.map { it.name.asString() }, + kotlin = KotlinFunctionInfo( + receiverTypeName = receiverType?.let { kaTypeInternalName(it) } ?: "", + receiverTypeDisplayName = receiverType?.let { kaTypeDisplayName(it) } ?: "", + isSuspend = fnSymbol.isSuspend, + isInline = fnSymbol.isInline, + isInfix = fnSymbol.isInfix, + isOperator = fnSymbol.isOperator, + isTailrec = fnSymbol.isTailRec, + isExternal = fnSymbol.isExternal, + isExpect = fnSymbol.isExpect, + isReturnTypeNullable = returnType.isMarkedNullable, + ), + ), + ) +} + +@OptIn(KaExperimentalApi::class) +private fun KaSession.analyzeClassOrObject(filePath: String, dcl: KtClassOrObject): JvmSymbol? { + dcl.name ?: return null // anonymous objects have no stable name + val visibility = dcl.jvmVisibility() + if (visibility == JvmVisibility.PRIVATE) return null + + val internalName = dcl.internalName() ?: return null + val pkg = dcl.containingKtFile.packageFqName.asString() + val shortName = internalName.substringAfterLast('/').substringAfterLast('$') + val containingClass = dcl.containingClassInternalName() + + val clsSymbol = dcl.symbol as? KaClassSymbol ?: return null + + val kind = when (dcl) { + is KtObjectDeclaration if dcl.isCompanion() -> JvmSymbolKind.COMPANION_OBJECT + is KtObjectDeclaration -> JvmSymbolKind.OBJECT + is KtClass if dcl.isInterface() -> JvmSymbolKind.INTERFACE + is KtClass if dcl.isEnum() -> JvmSymbolKind.ENUM + is KtClass if dcl.isAnnotation() -> JvmSymbolKind.ANNOTATION_CLASS + is KtClass if dcl.isData() -> JvmSymbolKind.DATA_CLASS + is KtClass if dcl.hasModifier(KtTokens.VALUE_KEYWORD) -> JvmSymbolKind.VALUE_CLASS + is KtClass if dcl.hasModifier(KtTokens.SEALED_KEYWORD) -> JvmSymbolKind.SEALED_CLASS + else -> JvmSymbolKind.CLASS + } + + val supertypes = clsSymbol.superTypes.mapNotNull { st -> + if (st !is KaClassType) return@mapNotNull null + val sId = st.classId + val sPkg = sId.packageFqName.asString() + val sRel = sId.relativeClassName.asString() + val sInternal = if (sPkg.isEmpty()) sRel.replace('.', '$') + else "${sPkg.replace('.', '/')}/${sRel.replace('.', '$')}" + if (sInternal == "kotlin/Any") null else sInternal + } + + return JvmSymbol( + key = internalName, + sourceId = filePath, + name = internalName, + shortName = shortName, + packageName = pkg, + kind = kind, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmClassInfo( + internalName = internalName, + containingClassName = containingClass ?: "", + supertypeNames = supertypes, + typeParameters = clsSymbol.typeParameters.map { it.name.asString() }, + isAbstract = dcl.hasModifier(KtTokens.ABSTRACT_KEYWORD), + isFinal = dcl.hasModifier(KtTokens.FINAL_KEYWORD), + isInner = dcl is KtClass && dcl.isInner(), + isStatic = containingClass != null && !(dcl is KtClass && dcl.isInner()), + kotlin = KotlinClassInfo( + isData = dcl is KtClass && dcl.isData(), + isValue = dcl is KtClass && dcl.hasModifier(KtTokens.VALUE_KEYWORD), + isSealed = dcl is KtClass && dcl.hasModifier(KtTokens.SEALED_KEYWORD), + isFunInterface = dcl is KtClass && dcl.hasModifier(KtTokens.FUN_KEYWORD), + isExpect = dcl.hasModifier(KtTokens.EXPECT_KEYWORD), + isActual = dcl.hasModifier(KtTokens.ACTUAL_KEYWORD), + isExternal = dcl.hasModifier(KtTokens.EXTERNAL_KEYWORD), + ), + ), + ) +} + +private fun KaSession.analyzeProperty(filePath: String, dcl: KtProperty): JvmSymbol? { + val propName = dcl.name ?: return null + val visibility = dcl.jvmVisibility() + if (visibility == JvmVisibility.PRIVATE) return null + + val pkg = dcl.containingKtFile.packageFqName.asString() + val containingClass = dcl.containingClassInternalName() + + val propSymbol = dcl.symbol as? KaPropertySymbol ?: return null + val returnType = propSymbol.returnType + val receiverType = propSymbol.receiverParameter?.returnType + + val qualifiedName = if (containingClass != null) "$containingClass#$propName" + else "$pkg#$propName" + + return JvmSymbol( + key = qualifiedName, + sourceId = filePath, + name = qualifiedName, + shortName = propName, + packageName = pkg, + kind = if (receiverType != null) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmFieldInfo( + containingClassName = containingClass ?: "", + typeName = kaTypeInternalName(returnType), + typeDisplayName = kaTypeDisplayName(returnType), + kotlin = KotlinPropertyInfo( + receiverTypeName = receiverType?.let { kaTypeInternalName(it) } ?: "", + receiverTypeDisplayName = receiverType?.let { kaTypeDisplayName(it) } ?: "", + isConst = dcl.hasModifier(KtTokens.CONST_KEYWORD), + isLateinit = dcl.hasModifier(KtTokens.LATEINIT_KEYWORD), + hasGetter = dcl.getter != null, + hasSetter = dcl.setter != null, + isDelegated = dcl.delegateExpression != null, + isTypeNullable = returnType.isMarkedNullable, + isExpect = dcl.hasModifier(KtTokens.EXPECT_KEYWORD), + isActual = dcl.hasModifier(KtTokens.ACTUAL_KEYWORD), + isExternal = dcl.hasModifier(KtTokens.EXTERNAL_KEYWORD), + ), + ), + ) +} + +/** + * Constructor `val`/`var` parameters are indexed as properties so that + * they appear in completion and navigation just like explicitly declared + * properties. Plain constructor or function parameters are skipped. + */ +private fun KaSession.analyzeParameter(filePath: String, dcl: KtParameter): JvmSymbol? { + if (!dcl.hasValOrVar()) return null + + val propName = dcl.name ?: return null + val visibility = dcl.jvmVisibility() + if (visibility == JvmVisibility.PRIVATE) return null + + val pkg = dcl.containingKtFile.packageFqName.asString() + val containingClass = dcl.containingClassInternalName() + + val paramSymbol = dcl.symbol as? KaValueParameterSymbol ?: return null + val returnType = paramSymbol.returnType + + val qualifiedName = if (containingClass != null) "$containingClass#$propName" + else "$pkg#$propName" + + return JvmSymbol( + key = qualifiedName, + sourceId = filePath, + name = qualifiedName, + shortName = propName, + packageName = pkg, + kind = JvmSymbolKind.PROPERTY, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmFieldInfo( + containingClassName = containingClass ?: "", + typeName = kaTypeInternalName(returnType), + typeDisplayName = kaTypeDisplayName(returnType), + kotlin = KotlinPropertyInfo( + isTypeNullable = returnType.isMarkedNullable, + ), + ), + ) +} + +private fun KaSession.analyzeTypeAlias(filePath: String, dcl: KtTypeAlias): JvmSymbol? { + val aliasName = dcl.name ?: return null + val visibility = dcl.jvmVisibility() + if (visibility == JvmVisibility.PRIVATE) return null + + val pkg = dcl.containingKtFile.packageFqName.asString() + + val aliasSymbol = dcl.symbol + val expandedType = aliasSymbol.expandedType + + // Key convention mirrors KotlinMetadataScanner: dot-notation FQ name. + val fqName = if (pkg.isEmpty()) aliasName else "$pkg.$aliasName" + + return JvmSymbol( + key = fqName, + sourceId = filePath, + name = fqName, + shortName = aliasName, + packageName = pkg, + kind = JvmSymbolKind.TYPE_ALIAS, + language = JvmSourceLanguage.KOTLIN, + visibility = visibility, + data = JvmTypeAliasInfo( + expandedTypeName = kaTypeInternalName(expandedType), + expandedTypeDisplayName = kaTypeDisplayName(expandedType), + typeParameters = aliasSymbol.typeParameters.map { it.name.asString() }, + ), + ) +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt new file mode 100644 index 0000000000..55ce8374b9 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt @@ -0,0 +1,27 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.selects.select + +internal class WorkerQueue { + + private val scanChannel = Channel(capacity = 100) + private val editChannel = Channel(capacity = 20) + private val indexChannel = Channel(capacity = 100) + + suspend fun putScanQueue(item: T) = scanChannel.send(item) + suspend fun putEditQueue(item: T) = editChannel.send(item) + suspend fun putIndexQueue(item: T) = indexChannel.send(item) + + suspend fun take(): T { + scanChannel.tryReceive().getOrNull()?.let { return it } + editChannel.tryReceive().getOrNull()?.let { return it } + indexChannel.tryReceive().getOrNull()?.let { return it } + + return select { + scanChannel.onReceive { it } + editChannel.onReceive { it } + indexChannel.onReceive { it } + } + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt new file mode 100644 index 0000000000..b8bce90811 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt @@ -0,0 +1,29 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KaModuleBase +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope + +@OptIn(KaPlatformInterface::class) +internal abstract class AbstractKtModule( + override val project: Project, + override val directRegularDependencies: List, +) : KtModule, KaModuleBase() { + + private val baseSearchScope by lazy { + val files = computeFiles(extended = true) + .toList() + + GlobalSearchScope.filesScope(project, files) + } + + override val baseContentScope: GlobalSearchScope + get() = baseSearchScope + + override val directDependsOnDependencies: List + get() = emptyList() + + override val directFriendDependencies: List + get() = emptyList() +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt new file mode 100644 index 0000000000..0560662d8d --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt @@ -0,0 +1,9 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import org.jetbrains.kotlin.com.intellij.openapi.util.Key +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.UserDataProperty +import java.nio.file.Path + +private val KT_LSP_COMPLETION_BACKING_FILE = Key("KT_LSP_COMPLETION_BACKING_FILE") +var KtFile.backingFilePath by UserDataProperty(KT_LSP_COMPLETION_BACKING_FILE) \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt new file mode 100644 index 0000000000..85b7de142c --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt @@ -0,0 +1,156 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import com.itsaky.androidide.lsp.kotlin.compiler.DEFAULT_JVM_TARGET +import com.itsaky.androidide.lsp.kotlin.compiler.read +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils +import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibraryModule +import org.jetbrains.kotlin.analysis.api.projectStructure.KaLibrarySourceModule +import org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem +import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.openapi.module.Module +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.StandardFileSystems.JAR_PROTOCOL +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.library.KLIB_FILE_EXTENSION +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.slf4j.LoggerFactory +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +private val logger = LoggerFactory.getLogger("KtLibraryModule") + +@OptIn(KaPlatformInterface::class) +internal class KtLibraryModule( + project: Project, + override val id: String, + override val contentRoots: Set, + dependencies: List, + private val applicationEnvironment: CoreApplicationEnvironment, + override val isSdk: Boolean = false, + private val jvmTarget: JvmTarget = DEFAULT_JVM_TARGET, + override val librarySources: KaLibrarySourceModule? = null, +) : KaLibraryModule, + AbstractKtModule( + project, + dependencies + ) { + + class Builder( + private val project: Project, + private val applicationEnvironment: CoreApplicationEnvironment, + ) { + lateinit var id: String + private val contentRoots = mutableSetOf() + private val dependencies = mutableListOf() + var isSdk: Boolean = false + var jvmTarget: JvmTarget = DEFAULT_JVM_TARGET + var librarySources: KaLibrarySourceModule? = null + + fun addContentRoot(root: Path) { + contentRoots.add(root) + } + + fun addDependency(dep: KtModule) { + dependencies.add(dep) + } + + fun build(): KtLibraryModule = KtLibraryModule( + project = project, + id = id, + contentRoots = contentRoots.toSet(), + dependencies = dependencies.toList(), + applicationEnvironment = applicationEnvironment, + isSdk = isSdk, + jvmTarget = jvmTarget, + librarySources = librarySources, + ) + } + + @OptIn(KaImplementationDetail::class) + override fun computeFiles(extended: Boolean): Sequence { + val roots = if (isSdk) project.read { + LibraryUtils.findClassesFromJdkHome( + contentRoots.first(), + isJre = false + ) + } + else contentRoots + + val notExtendedFiles = roots + .asSequence() + .mapNotNull { getVirtualFileForLibraryRoot(it, applicationEnvironment, project) } + + if (!extended) return notExtendedFiles + + return notExtendedFiles + .flatMap { LibraryUtils.getAllVirtualFilesFromRoot(it, includeRoot = true) } + } + + @OptIn(KaExperimentalApi::class) + override val baseContentScope: GlobalSearchScope by lazy { + val virtualFileUrls = computeFiles(extended = true).map { it.url }.toSet() + object : GlobalSearchScope(project) { + override fun contains(vf: VirtualFile): Boolean { + return vf.url in virtualFileUrls + } + + override fun isSearchInModuleContent(module: Module): Boolean { + return false + } + + override fun isSearchInLibraries(): Boolean { + return true + } + } + } + + override val libraryName: String + get() = id + + override val binaryRoots: Collection + get() = contentRoots + + @KaExperimentalApi + override val binaryVirtualFiles: Collection + get() = emptyList() + + override val targetPlatform: TargetPlatform + get() = JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) +} + +internal fun buildKtLibraryModule( + project: Project, + applicationEnvironment: CoreApplicationEnvironment, + init: KtLibraryModule.Builder.() -> Unit, +): KtLibraryModule = KtLibraryModule.Builder(project, applicationEnvironment).apply(init).build() + +private const val JAR_SEPARATOR = "!/" +private fun getVirtualFileForLibraryRoot( + root: Path, + environment: CoreApplicationEnvironment, + project: Project, +): VirtualFile? { + val pathString = root.absolutePathString() + + // .jar or .klib files + if (pathString.endsWith(JAR_PROTOCOL) || pathString.endsWith(KLIB_FILE_EXTENSION)) { + return project.read { environment.jarFileSystem.findFileByPath(pathString + JAR_SEPARATOR) } + } + + // JDK classes + if (pathString.contains(JAR_SEPARATOR)) { + val (libHomePath, pathInImage) = CoreJrtFileSystem.splitPath(pathString) + val adjustedPath = libHomePath + JAR_SEPARATOR + "modules/$pathInImage" + return project.read { environment.jrtFileSystem?.findFileByPath(adjustedPath) } + } + + // Regular .class files + return project.read { VirtualFileManager.getInstance().findFileByNioPath(root) } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt new file mode 100644 index 0000000000..b12b7d31bb --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt @@ -0,0 +1,39 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import java.nio.file.Path + +@OptIn(KaPlatformInterface::class) +internal interface KtModule : KaModule { + + val id: String + + val contentRoots: Set + + override val directRegularDependencies: List + override val directDependsOnDependencies: List + override val directFriendDependencies: List + + fun computeFiles(extended: Boolean): Sequence +} + +internal val KtModule.isSourceModule: Boolean + get() = this is KtSourceModule + +internal fun List.asFlatSequence(): Sequence { + val processedModules = mutableSetOf() + return this.asSequence().flatMap { getModuleFlatSequence(it, processedModules) } +} + +private fun getModuleFlatSequence(ktModule: KtModule, processed: MutableSet): Sequence = sequence { + if (processed.contains(ktModule.id)) return@sequence + + yield(ktModule) + processed.add(ktModule.id) + + ktModule.directRegularDependencies.forEach { dependency -> + yieldAll(getModuleFlatSequence(dependency, processed)) + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt new file mode 100644 index 0000000000..44875b3ba0 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt @@ -0,0 +1,110 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import com.itsaky.androidide.lsp.kotlin.compiler.DEFAULT_JVM_TARGET +import com.itsaky.androidide.lsp.kotlin.compiler.DEFAULT_LANGUAGE_VERSION +import com.itsaky.androidide.lsp.kotlin.compiler.read +import com.itsaky.androidide.projects.api.ModuleProject +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.projectStructure.KaSourceModule +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.config.ApiVersion +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Paths +import kotlin.io.path.PathWalkOption +import kotlin.io.path.absolutePathString +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.pathString +import kotlin.io.path.walk + +@OptIn(KaPlatformInterface::class) +internal class KtSourceModule( + project: Project, + val module: ModuleProject, + directRegularDependencies: List, +) : KaSourceModule, AbstractKtModule(project, directRegularDependencies) { + + private val logger = LoggerFactory.getLogger(KtSourceModule::class.java) + + class Builder(private val project: Project) { + lateinit var module: ModuleProject + private val dependencies = mutableListOf() + + fun addDependency(dep: KtModule) { + dependencies.add(dep) + } + + fun build(): KtSourceModule = KtSourceModule(project, module, dependencies.toList()) + } + + override val id: String + get() = module.path + + override val contentRoots by lazy { + module.getSourceDirectories() + .asSequence() + .map { it.toPath() } + .toSet() + } + + private val versions by lazy { + val kotlinCompilerSettings = when { + module.hasJavaProject() -> module.javaProject + .kotlinCompilerSettings + + module.hasAndroidProject() -> module.androidProject + .kotlinCompilerSettings + + else -> null + } + + if (kotlinCompilerSettings == null) { + return@lazy DEFAULT_LANGUAGE_VERSION to DEFAULT_JVM_TARGET + } + + val apiVersion = LanguageVersion.fromVersionString(kotlinCompilerSettings.apiVersion) + ?: LanguageVersion.fromFullVersionString(kotlinCompilerSettings.apiVersion) + + val jvmTarget = JvmTarget.fromString(kotlinCompilerSettings.jvmTarget) + + (apiVersion ?: DEFAULT_LANGUAGE_VERSION) to (jvmTarget ?: DEFAULT_JVM_TARGET) + } + + override val name: String + get() = module.name + + override val languageVersionSettings: LanguageVersionSettings + get() = LanguageVersionSettingsImpl( + languageVersion = versions.first, + apiVersion = ApiVersion.createByLanguageVersion(versions.first), + ) + + override val targetPlatform: TargetPlatform + get() = JvmPlatforms.jvmPlatformByTargetVersion(versions.second) + + override fun computeFiles(extended: Boolean): Sequence = + contentRoots + .asSequence() + .flatMap { it.walk() } + .filter { !it.isDirectory() && (it.extension == "kt" || it.extension == "java") } + .mapNotNull { + project.read { + VirtualFileManager.getInstance().findFileByNioPath(it) + } + } + +} + +internal fun buildKtSourceModule( + project: Project, + init: KtSourceModule.Builder.() -> Unit, +): KtSourceModule = KtSourceModule.Builder(project).apply(init).build() \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt new file mode 100644 index 0000000000..0de0b4282c --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt @@ -0,0 +1,40 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.modules + +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.projectStructure.KaNotUnderContentRootModule +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import java.nio.file.Path + +@OptIn(KaPlatformInterface::class, KaExperimentalApi::class) +internal class NotUnderContentRootModule( + override val id: String, + project: Project, + override val moduleDescription: String, + directRegularDependencies: List = emptyList(), + override val targetPlatform: TargetPlatform = JvmPlatforms.defaultJvmPlatform, + override val file: PsiFile? = null, +) : KaNotUnderContentRootModule, AbstractKtModule( + project, directRegularDependencies +) { + override val name: String + get() = id + + override val baseContentScope: GlobalSearchScope + get() = if (file != null) GlobalSearchScope.fileScope(file) else GlobalSearchScope.EMPTY_SCOPE + + override val contentRoots: Set + get() = file?.virtualFile?.toNioPath()?.let(::setOf) ?: emptySet() + + override fun computeFiles(extended: Boolean): Sequence = sequence { + val vf = file?.virtualFile + if (vf != null) { + yield(vf) + } + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt new file mode 100644 index 0000000000..f3b0edbfa3 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspServiceRegistrar.kt @@ -0,0 +1,130 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.registrar + +import com.itsaky.androidide.lsp.kotlin.compiler.services.AnalysisPermissionOptions +import com.itsaky.androidide.lsp.kotlin.compiler.services.AnnotationsResolverFactory +import com.itsaky.androidide.lsp.kotlin.compiler.services.NoOpAsyncExecutionService +import com.itsaky.androidide.lsp.kotlin.compiler.services.DeclarationProviderFactory +import com.itsaky.androidide.lsp.kotlin.compiler.services.DeclarationProviderMerger +import com.itsaky.androidide.lsp.kotlin.compiler.services.ModificationTrackerFactory +import com.itsaky.androidide.lsp.kotlin.compiler.services.ModuleDependentsProvider +import com.itsaky.androidide.lsp.kotlin.compiler.services.PackagePartProviderFactory +import com.itsaky.androidide.lsp.kotlin.compiler.services.PackageProviderFactory +import com.itsaky.androidide.lsp.kotlin.compiler.services.PackageProviderMerger +import com.itsaky.androidide.lsp.kotlin.compiler.services.PlatformSettings +import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.platform.KotlinPlatformSettings +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderMerger +import org.jetbrains.kotlin.analysis.api.platform.lifetime.KotlinLifetimeTokenFactory +import org.jetbrains.kotlin.analysis.api.platform.lifetime.KotlinReadActionConfinementLifetimeTokenFactory +import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModificationTrackerFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackagePartProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderMerger +import org.jetbrains.kotlin.analysis.api.platform.permissions.KotlinAnalysisPermissionOptions +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinModuleDependentsProvider +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.AnalysisApiSimpleServiceRegistrar +import org.jetbrains.kotlin.analysis.api.standalone.base.projectStructure.PluginStructureProvider +import org.jetbrains.kotlin.analysis.decompiler.stub.file.ClsKotlinBinaryClassCache +import org.jetbrains.kotlin.analysis.decompiler.stub.file.DummyFileAttributeService +import org.jetbrains.kotlin.analysis.decompiler.stub.file.FileAttributeService +import org.jetbrains.kotlin.asJava.finder.JavaElementFinder +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.mock.MockApplication +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.Disposable +import org.jetbrains.kotlin.com.intellij.openapi.application.AsyncExecutionService +import org.jetbrains.kotlin.com.intellij.psi.PsiElementFinder +import org.jetbrains.kotlin.com.intellij.psi.PsiFile +import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeEvent +import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener +import org.jetbrains.kotlin.com.intellij.psi.SmartPointerManager +import org.jetbrains.kotlin.com.intellij.psi.SmartTypePointerManager +import org.jetbrains.kotlin.com.intellij.psi.impl.PsiElementFinderImpl +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl +import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointerManagerImpl +import org.jetbrains.kotlin.com.intellij.psi.impl.source.codeStyle.IndentHelper + +@OptIn(KaImplementationDetail::class) +internal object LspServiceRegistrar : AnalysisApiSimpleServiceRegistrar() { + + private const val PLUGIN_RELATIVE_PATH = "/META-INF/kt-lsp/kt-lsp.xml" + + override fun registerApplicationServices(application: MockApplication) { + PluginStructureProvider.registerApplicationServices(application, PLUGIN_RELATIVE_PATH) + + with(application) { + registerService(FileAttributeService::class.java, DummyFileAttributeService::class.java) + registerService( + KotlinAnalysisPermissionOptions::class.java, + AnalysisPermissionOptions::class.java + ) + registerService(ClsKotlinBinaryClassCache::class.java) + registerService(AsyncExecutionService::class.java, NoOpAsyncExecutionService::class.java) + } + } + + override fun registerProjectServices(project: MockProject) { + PluginStructureProvider.registerProjectServices(project, PLUGIN_RELATIVE_PATH) + + + with(project) { + registerService( + KotlinLifetimeTokenFactory::class.java, + KotlinReadActionConfinementLifetimeTokenFactory::class.java + ) + registerService(KotlinPlatformSettings::class.java, PlatformSettings::class.java) + registerService( + SmartTypePointerManager::class.java, + SmartTypePointerManagerImpl::class.java + ) + registerService(SmartPointerManager::class.java, SmartPointerManagerImpl::class.java) + registerService( + KotlinProjectStructureProvider::class.java, + ProjectStructureProvider::class.java + ) + registerService( + KotlinModuleDependentsProvider::class.java, + ModuleDependentsProvider::class.java + ) + registerService( + KotlinModificationTrackerFactory::class.java, + ModificationTrackerFactory::class.java + ) + registerService( + KotlinAnnotationsResolverFactory::class.java, + AnnotationsResolverFactory::class.java + ) + registerService( + KotlinDeclarationProviderFactory::class.java, + DeclarationProviderFactory::class.java + ) + registerService( + KotlinDeclarationProviderMerger::class.java, + DeclarationProviderMerger::class.java + ) + registerService( + KotlinPackageProviderFactory::class.java, + PackageProviderFactory::class.java + ) + registerService(KotlinPackageProviderMerger::class.java, PackageProviderMerger::class.java) + registerService( + KotlinPackagePartProviderFactory::class.java, + PackagePartProviderFactory::class.java + ) + } + } + + @OptIn(KaExperimentalApi::class) + @Suppress("TestOnlyProblems") + override fun registerProjectModelServices(project: MockProject, disposable: Disposable) { + with(PsiElementFinder.EP.getPoint(project)) { + registerExtension(JavaElementFinder(project), disposable) + registerExtension(PsiElementFinderImpl(project), disposable) + } + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt new file mode 100644 index 0000000000..56ff769c31 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt @@ -0,0 +1,8 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.analysis.api.platform.permissions.KotlinAnalysisPermissionOptions + +class AnalysisPermissionOptions : KotlinAnalysisPermissionOptions { + override val defaultIsAnalysisAllowedOnEdt: Boolean get() = false + override val defaultIsAnalysisAllowedInWriteAction: Boolean get() = true +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt new file mode 100644 index 0000000000..0d04ab5888 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt @@ -0,0 +1,165 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolver +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinAnnotationsResolverFactory +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProvider +import org.jetbrains.kotlin.analysis.api.platform.declarations.createDeclarationProvider +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.com.intellij.psi.search.impl.VirtualFileEnumeration +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.parentOrNull +import org.jetbrains.kotlin.psi.KtAnnotated +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtFunction +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtTypeReference +import org.jetbrains.kotlin.psi.KtUserType +import org.jetbrains.kotlin.psi.declarationRecursiveVisitor +import org.jetbrains.kotlin.util.collectionUtils.filterIsInstanceAnd + +internal class AnnotationsResolverFactory : KtLspService, KotlinAnnotationsResolverFactory { + + private lateinit var project: Project + private lateinit var index: KtSymbolIndex + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.project = project + this.index = index + } + + override fun createAnnotationResolver(searchScope: GlobalSearchScope): KotlinAnnotationsResolver { + return AnnotationsResolver(project, searchScope, index) + } +} + +@Suppress("UnstableApiUsage") +internal class AnnotationsResolver( + project: Project, + private val scope: GlobalSearchScope, + private val index: KtSymbolIndex, +) : KotlinAnnotationsResolver { + + private val declarationProvider by lazy { + project.createDeclarationProvider(scope, contextualModule = null) + } + + private fun allDeclarations(): List { + val virtualFiles = VirtualFileEnumeration.extract(scope) ?: return emptyList() + + val filesInScope = virtualFiles + .filesIfCollection + .orEmpty() + .asSequence() + .filter { it in scope } + .mapNotNull { index.getKtFile(it) } + + return buildList { + val visitor = declarationRecursiveVisitor visit@{ + val isLocal = when (it) { + is KtClassOrObject -> it.isLocal + is KtFunction -> it.isLocal + is KtProperty -> it.isLocal + else -> return@visit + } + + if (!isLocal) { + add(it) + } + } + + filesInScope.forEach { it.accept(visitor) } + } + } + + override fun declarationsByAnnotation(annotationClassId: ClassId): Set { + return allDeclarations() + .asSequence() + .filter { annotationClassId in annotationsOnDeclaration(it) } + .toSet() + } + + override fun annotationsOnDeclaration(declaration: KtAnnotated): Set { + return declaration + .annotationEntries + .asSequence() + .flatMap { it.typeReference?.resolveAnnotationClassIds(declarationProvider).orEmpty() } + .toSet() + } +} + +private fun KtTypeReference.resolveAnnotationClassIds( + declarationProvider: KotlinDeclarationProvider, + candidates: MutableSet = mutableSetOf() +): Set { + val annotationTypeElement = typeElement as? KtUserType + val referencedName = annotationTypeElement?.referencedFqName ?: return emptySet() + if (referencedName.isRoot) return emptySet() + + if (!referencedName.parent().isRoot) { + return buildSet { referencedName.resolveToClassIds(this, declarationProvider) } + } + + val targetName = referencedName.shortName() + for (import in containingKtFile.importDirectives) { + val importedName = import.importedFqName ?: continue + when { + import.isAllUnder -> importedName.child(targetName).resolveToClassIds(candidates, declarationProvider) + importedName.shortName() == targetName -> importedName.resolveToClassIds(candidates, declarationProvider) + } + } + + containingKtFile.packageFqName.child(targetName).resolveToClassIds(candidates, declarationProvider) + return candidates +} + +private val KtUserType.referencedFqName: FqName? + get() { + val allTypes = generateSequence(this) { it.qualifier }.toList().asReversed() + val allQualifiers = allTypes.map { it.referencedName ?: return null } + return FqName.fromSegments(allQualifiers) + } + + +private fun FqName.resolveToClassIds(to: MutableSet, declarationProvider: KotlinDeclarationProvider) { + toClassIdSequence().mapNotNullTo(to) { classId -> + val classes = declarationProvider.getAllClassesByClassId(classId) + val typeAliases = declarationProvider.getAllTypeAliasesByClassId(classId) + typeAliases.singleOrNull()?.getTypeReference()?.resolveAnnotationClassIds(declarationProvider, to) + + val annotations = classes.filterIsInstanceAnd { it.isAnnotation() } + annotations.singleOrNull()?.let { + classId + } + } +} + +private fun FqName.toClassIdSequence(): Sequence { + var currentName = shortNameOrSpecial() + if (currentName.isSpecial) return emptySequence() + var currentParent = parentOrNull() ?: return emptySequence() + var currentRelativeName = currentName.asString() + + return sequence { + while (true) { + yield(ClassId(currentParent, FqName(currentRelativeName), isLocal = false)) + currentName = currentParent.shortNameOrSpecial() + if (currentName.isSpecial) break + currentParent = currentParent.parentOrNull() ?: break + currentRelativeName = "${currentName.asString()}.$currentRelativeName" + } + } +} + diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt new file mode 100644 index 0000000000..1b61343589 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt @@ -0,0 +1,189 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.index.filesForPackage +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.read +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinCompositeDeclarationProvider +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProvider +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDeclarationProviderMerger +import org.jetbrains.kotlin.analysis.api.platform.declarations.createDeclarationProvider +import org.jetbrains.kotlin.analysis.api.platform.mergeSpecificProviders +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.fileClasses.javaFileFacadeFqName +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtCallableDeclaration +import org.jetbrains.kotlin.psi.KtClassLikeDeclaration +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedFunction +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.KtScript +import org.jetbrains.kotlin.psi.KtTypeAlias +import org.jetbrains.kotlin.psi.psiUtil.isTopLevelKtOrJavaMember +import java.nio.file.Paths + +internal class DeclarationProviderFactory : KtLspService, KotlinDeclarationProviderFactory { + + private lateinit var project: Project + private lateinit var index: KtSymbolIndex + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.project = project + this.index = index + } + + override fun createDeclarationProvider( + scope: GlobalSearchScope, + contextualModule: KaModule? + ): KotlinDeclarationProvider { + return DeclarationProvider(scope, project, index) + } +} + +class DeclarationProviderMerger(private val project: Project) : KotlinDeclarationProviderMerger { + override fun merge(providers: List): KotlinDeclarationProvider = + providers.mergeSpecificProviders<_, DeclarationProvider>(KotlinCompositeDeclarationProvider.factory) { targetProviders -> + val combinedScope = GlobalSearchScope.union(targetProviders.map { it.scope }) + project.createDeclarationProvider(combinedScope, contextualModule = null).apply { + check(this is DeclarationProvider) { + "`DeclarationProvider` can only be merged into a combined declaration provider of the same type." + } + } + } +} + + +internal class DeclarationProvider( + val scope: GlobalSearchScope, + private val project: Project, + private val index: KtSymbolIndex +) : KotlinDeclarationProvider { + + private val KtElement.inScope: Boolean + get() = containingKtFile.virtualFile in scope + + override val hasSpecificCallablePackageNamesComputation: Boolean + get() = false + override val hasSpecificClassifierPackageNamesComputation: Boolean + get() = false + + override fun findFilesForFacade(facadeFqName: FqName): Collection { + if (facadeFqName.shortNameOrSpecial().isSpecial) return emptyList() + // According to standalone platform, this does not work with classes with @JvmPackageName + return findFilesForFacadeByPackage(facadeFqName.parent()) + .filter { it.javaFileFacadeFqName == facadeFqName } + } + + override fun findInternalFilesForFacade(facadeFqName: FqName): Collection = + // We don't deserialize libraries from stubs so we can return empty here safely + // We don't take the KaBuiltinsModule into account for simplicity, + // that means we expect the kotlin stdlib to be included on the project + emptyList() + + override fun findFilesForFacadeByPackage(packageFqName: FqName): Collection = + ktFilesForPackage(packageFqName).toList() + + override fun findFilesForScript(scriptFqName: FqName): Collection = + ktFilesForPackage(scriptFqName).mapNotNull { it.script }.toList() + + override fun getAllClassesByClassId(classId: ClassId): Collection = + ktFilesForPackage(classId.packageFqName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtClassOrObject::class.java).asSequence() + } + } + .filter { it.getClassId() == classId } + .toList() + + override fun getAllTypeAliasesByClassId(classId: ClassId): Collection = + ktFilesForPackage(classId.packageFqName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtTypeAlias::class.java).asSequence() + } + } + .filter { it.getClassId() == classId } + .toList() + + override fun getClassLikeDeclarationByClassId(classId: ClassId): KtClassLikeDeclaration? = + getAllClassesByClassId(classId).firstOrNull() + ?: getAllTypeAliasesByClassId(classId).firstOrNull() + + override fun getTopLevelCallableFiles(callableId: CallableId): Collection = + buildSet { + getTopLevelProperties(callableId).mapTo(this) { it.containingKtFile } + getTopLevelFunctions(callableId).mapTo(this) { it.containingKtFile } + } + + override fun getTopLevelFunctions(callableId: CallableId): Collection = + ktFilesForPackage(callableId.packageName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtNamedFunction::class.java) + .asSequence() + } + } + .filter { it.isTopLevel } + .filter { it.nameAsName == callableId.callableName } + .toList() + + override fun getTopLevelKotlinClassLikeDeclarationNamesInPackage(packageFqName: FqName): Set = + ktFilesForPackage(packageFqName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtClassLikeDeclaration::class.java) + .asSequence() + } + } + .filter { it.isTopLevelKtOrJavaMember() } + .mapNotNull { it.nameAsName } + .toSet() + + override fun getTopLevelCallableNamesInPackage(packageFqName: FqName): Set = + ktFilesForPackage(packageFqName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtCallableDeclaration::class.java) + .asSequence() + } + } + .filter { it.isTopLevelKtOrJavaMember() } + .mapNotNull { it.nameAsName } + .toSet() + + override fun getTopLevelProperties(callableId: CallableId): Collection = + ktFilesForPackage(callableId.packageName) + .flatMap { + project.read { + PsiTreeUtil.collectElementsOfType(it, KtProperty::class.java).asSequence() + } + } + .filter { it.isTopLevel } + .filter { it.nameAsName == callableId.callableName } + .toList() + + private fun ktFilesForPackage(fqName: FqName): Sequence { + return index.filesForPackage(fqName.asString()) + .map { VirtualFileManager.getInstance().findFileByNioPath(Paths.get(it.filePath))!! } + .filter { it in scope } + .map { index.getKtFile(it) } + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt new file mode 100644 index 0000000000..7036744413 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt @@ -0,0 +1,172 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence +import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule +import org.jetbrains.kotlin.analysis.api.KaImplementationDetail +import org.jetbrains.kotlin.analysis.api.fir.utils.isSubclassOf +import org.jetbrains.kotlin.analysis.api.platform.declarations.KotlinDirectInheritorsProvider +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProvider +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals +import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.LLFirSessionCache +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.fir.declarations.FirClass +import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider +import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtNullableType +import org.jetbrains.kotlin.psi.KtTreeVisitorVoid +import org.jetbrains.kotlin.psi.KtTypeAlias +import org.jetbrains.kotlin.psi.KtTypeElement +import org.jetbrains.kotlin.psi.KtUserType +import org.jetbrains.kotlin.psi.psiUtil.contains +import org.jetbrains.kotlin.psi.psiUtil.getImportedSimpleNameByImportAlias +import org.jetbrains.kotlin.psi.psiUtil.getSuperNames + +internal class DirectInheritorsProvider: KtLspService, KotlinDirectInheritorsProvider { + private lateinit var index: KtSymbolIndex + private lateinit var modules: List + private lateinit var project: Project + + private val classesBySupertypeName = mutableMapOf>() + private val inheritableTypeAliasesByAliasedName = mutableMapOf>() + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.project = project + this.index = index + this.modules = modules + } + + @OptIn(SymbolInternals::class) + override fun getDirectKotlinInheritors( + ktClass: KtClass, + scope: GlobalSearchScope, + includeLocalInheritors: Boolean + ): Iterable { + computeIndex() + + val classId = ktClass.getClassId() ?: return emptyList() + val baseModule = KotlinProjectStructureProvider.getModule(project, ktClass, useSiteModule = null) + val baseFirClass = classId.toFirSymbol(baseModule)?.fir as? FirClass ?: return emptyList() + + val baseClassNames = mutableSetOf(classId.shortClassName) + calculateAliases(classId.shortClassName, baseClassNames) + + val possibleInheritors = baseClassNames.flatMap { classesBySupertypeName[it].orEmpty() } + if (possibleInheritors.isEmpty()) { + return emptyList() + } + + return possibleInheritors.filter { isValidInheritor(it, baseFirClass, scope, includeLocalInheritors) } + } + + // Let's say this operation is not frequently called, if we discover it's not the case we should cache it + private fun computeIndex() { + classesBySupertypeName.clear() + inheritableTypeAliasesByAliasedName.clear() + + modules + .asFlatSequence() + .filter { it.isSourceModule }.flatMap { it.computeFiles(extended = true) } + .map { index.getKtFile(it) } + .forEach { ktFile -> + ktFile.accept(object : KtTreeVisitorVoid() { + override fun visitClassOrObject(classOrObject: KtClassOrObject) { + classOrObject.getSuperNames().forEach { superName -> + classesBySupertypeName + .computeIfAbsent(Name.identifier(superName)) { mutableSetOf() } + .add(classOrObject) + } + super.visitClassOrObject(classOrObject) + } + + override fun visitTypeAlias(typeAlias: KtTypeAlias) { + val typeElement = typeAlias.getTypeReference()?.typeElement ?: return + + findInheritableSimpleNames(typeElement).forEach { expandedName -> + inheritableTypeAliasesByAliasedName + .computeIfAbsent(Name.identifier(expandedName)) { mutableSetOf() } + .add(typeAlias) + } + + super.visitTypeAlias(typeAlias) + } + }) + } + } + + private fun calculateAliases(aliasedName: Name, aliases: MutableSet) { + inheritableTypeAliasesByAliasedName[aliasedName].orEmpty().forEach { alias -> + val aliasName = alias.nameAsSafeName + val isNewAliasName = aliases.add(aliasName) + if (isNewAliasName) { + calculateAliases(aliasName, aliases) + } + } + } + + @OptIn(KaImplementationDetail::class, SymbolInternals::class) + private fun isValidInheritor( + candidate: KtClassOrObject, + baseFirClass: FirClass, + scope: GlobalSearchScope, + includeLocalInheritors: Boolean, + ): Boolean { + if (!includeLocalInheritors && candidate.isLocal) { + return false + } + + if (!scope.contains(candidate)) { + return false + } + + val candidateClassId = candidate.getClassId() ?: return false + val candidateModule = KotlinProjectStructureProvider.getModule(project, candidate, useSiteModule = null) + val candidateFirSymbol = candidateClassId.toFirSymbol(candidateModule) ?: return false + val candidateFirClass = candidateFirSymbol.fir as? FirClass ?: return false + + return isSubclassOf(candidateFirClass, baseFirClass, candidateFirClass.moduleData.session, allowIndirectSubtyping = false) + } + + @OptIn(LLFirInternals::class) + private fun ClassId.toFirSymbol(module: KaModule): FirClassLikeSymbol<*>? { + val session = LLFirSessionCache.getInstance(project).getSession(module, preferBinary = true) + return session.symbolProvider.getClassLikeSymbolByClassId(this) + } +} + +private fun findInheritableSimpleNames(typeElement: KtTypeElement): List { + return when (typeElement) { + is KtUserType -> { + val referenceName = typeElement.referencedName ?: return emptyList() + + buildList { + add(referenceName) + + val ktFile = typeElement.containingKtFile + if (!ktFile.isCompiled) { + val name = getImportedSimpleNameByImportAlias(typeElement.containingKtFile, referenceName) + if (name != null) { + add(name) + } + } + } + } + is KtNullableType -> typeElement.innerType?.let(::findInheritableSimpleNames) ?: emptyList() + else -> emptyList() + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt new file mode 100644 index 0000000000..c6dff05a08 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt @@ -0,0 +1,33 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.analysis.api.platform.java.KotlinJavaModuleAccessibilityChecker +import org.jetbrains.kotlin.analysis.api.platform.java.KotlinJavaModuleAccessibilityError +import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleResolver +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.resolve.jvm.modules.JavaModuleResolver + +class JavaModuleAccessibilityChecker( + private val javaModuleResolver: CliJavaModuleResolver, +): KotlinJavaModuleAccessibilityChecker { + override fun checkAccessibility( + useSiteFile: VirtualFile?, + referencedFile: VirtualFile, + referencedPackage: FqName? + ): KotlinJavaModuleAccessibilityError? { + val accessError = javaModuleResolver.checkAccessibility(useSiteFile, referencedFile, referencedPackage) + return accessError?.let(::convertAccessError) + } + + private fun convertAccessError(accessError: JavaModuleResolver.AccessError): KotlinJavaModuleAccessibilityError = + when (accessError) { + is JavaModuleResolver.AccessError.ModuleDoesNotReadUnnamedModule -> + KotlinJavaModuleAccessibilityError.ModuleDoesNotReadUnnamedModule + + is JavaModuleResolver.AccessError.ModuleDoesNotReadModule -> + KotlinJavaModuleAccessibilityError.ModuleDoesNotReadModule(accessError.dependencyModuleName) + + is JavaModuleResolver.AccessError.ModuleDoesNotExportPackage -> + KotlinJavaModuleAccessibilityError.ModuleDoesNotExportPackage(accessError.dependencyModuleName) + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt new file mode 100644 index 0000000000..c1a05127e3 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt @@ -0,0 +1,16 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.analysis.api.KaNonPublicApi +import org.jetbrains.kotlin.analysis.api.platform.java.KotlinJavaModuleJavaAnnotationsProvider +import org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleResolver +import org.jetbrains.kotlin.load.java.structure.JavaAnnotation +import org.jetbrains.kotlin.name.ClassId + +@OptIn(KaNonPublicApi::class) +class JavaModuleAnnotationsProvider( + private val javaModuleResolver: CliJavaModuleResolver, +): KotlinJavaModuleJavaAnnotationsProvider { + override fun getAnnotationsForModuleOwnerOfClass(classId: ClassId): List? { + return javaModuleResolver.getAnnotationsForModuleOwnerOfClass(classId) + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt new file mode 100644 index 0000000000..2ead27bcdb --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt @@ -0,0 +1,16 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject + +internal interface KtLspService { + + fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List, + ) +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt new file mode 100644 index 0000000000..d661755914 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt @@ -0,0 +1,9 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.config.ApiVersion +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl + +val latestLanguageVersionSettings: LanguageVersionSettings = + LanguageVersionSettingsImpl(LanguageVersion.LATEST_STABLE, ApiVersion.LATEST) \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt new file mode 100644 index 0000000000..167b968eb0 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt @@ -0,0 +1,6 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModificationTrackerByEventFactoryBase +import org.jetbrains.kotlin.com.intellij.openapi.project.Project + +class ModificationTrackerFactory(project: Project): KotlinModificationTrackerByEventFactoryBase(project) \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt new file mode 100644 index 0000000000..5b057064c8 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt @@ -0,0 +1,67 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinModuleDependentsProviderBase +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.analysis.api.projectStructure.allDirectDependencies +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.util.containers.ContainerUtil.createConcurrentSoftMap + +internal class ModuleDependentsProvider : KtLspService, KotlinModuleDependentsProviderBase() { + + private lateinit var modules: List + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.modules = modules + } + + private val directDependentsByKtModule by lazy { + modules.asSequence() + .map { module -> + buildDependentsMap(module, module.allDirectDependencies()) + } + .reduce { acc, value -> acc + value } + } + + private val transitiveDependentsByKtModule = createConcurrentSoftMap>() + private val refinementDependentsByKtModule by lazy { + modules + .asSequence() + .map { buildDependentsMap(it, it.transitiveDependsOnDependencies.asSequence()) } + .reduce { acc, map -> acc + map } + } + + override fun getDirectDependents(module: KaModule): Set { + return directDependentsByKtModule[module].orEmpty() + } + + override fun getRefinementDependents(module: KaModule): Set { + return refinementDependentsByKtModule[module].orEmpty() + } + + override fun getTransitiveDependents(module: KaModule): Set { + return transitiveDependentsByKtModule.computeIfAbsent(module) { key -> + computeTransitiveDependents( + key + ) + } + } +} + +private fun buildDependentsMap( + module: KaModule, + dependencies: Sequence, +): Map> = buildMap { + dependencies.forEach { dependency -> + if (dependency == module) return@forEach + val dependents = computeIfAbsent(dependency) { mutableSetOf() } + dependents.add(module) + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt new file mode 100644 index 0000000000..bc88cd8739 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt @@ -0,0 +1,157 @@ +@file:Suppress("UnstableApiUsage") + +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.concurrency.CancellablePromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.kotlin.com.intellij.openapi.Disposable +import org.jetbrains.kotlin.com.intellij.openapi.application.AppUIExecutor +import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager +import org.jetbrains.kotlin.com.intellij.openapi.application.AsyncExecutionService +import org.jetbrains.kotlin.com.intellij.openapi.application.ExpirableExecutor +import org.jetbrains.kotlin.com.intellij.openapi.application.ModalityState +import org.jetbrains.kotlin.com.intellij.openapi.application.NonBlockingReadAction +import org.jetbrains.kotlin.com.intellij.openapi.progress.ProgressIndicator +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.util.Function +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.TimeUnit +import java.util.function.BooleanSupplier +import java.util.function.Consumer + +/** + * No-op [AsyncExecutionService] for standalone (non-IDE) environments. + * + * The real implementation requires an IDE event-dispatch / write-thread infrastructure that does + * not exist in our standalone setup. Submitted tasks are run asynchronously on the common + * ForkJoin pool so that stub rebuilds triggered by structural PSI changes don't block the + * analysis thread or deadlock against the project read/write lock. + */ +internal class NoOpAsyncExecutionService : AsyncExecutionService() { + + private val executor: AppUIExecutor = NoOpAppUIExecutor() + + override fun createExecutor(backgroundExecutor: Executor): ExpirableExecutor = + NoOpExpirableExecutor(backgroundExecutor) + + override fun createUIExecutor(modalityState: ModalityState): AppUIExecutor = executor + + override fun createWriteThreadExecutor(modalityState: ModalityState): AppUIExecutor = executor + + override fun buildNonBlockingReadAction(callable: Callable): NonBlockingReadAction = + NoOpNonBlockingReadAction(callable) +} + +private class NoOpAppUIExecutor : AppUIExecutor { + override fun later(): AppUIExecutor = this + override fun withDocumentsCommitted(project: Project): AppUIExecutor = this + override fun inSmartMode(project: Project): AppUIExecutor = this + override fun expireWith(disposable: Disposable): AppUIExecutor = this + + override fun execute(runnable: Runnable) { + ApplicationManager.getApplication().invokeLater(runnable) + } + + override fun submit(callable: Callable): CancellablePromise { + val future = CompletableFuture() + ApplicationManager.getApplication().invokeLater { + try { future.complete(callable.call()) } + catch (e: Throwable) { future.completeExceptionally(e) } + } + return future.asCancellablePromise() + } + + override fun submit(runnable: Runnable): CancellablePromise<*> { + val future = CompletableFuture() + ApplicationManager.getApplication().invokeLater { + try { runnable.run(); future.complete(null) } + catch (e: Throwable) { future.completeExceptionally(e) } + } + return future.asCancellablePromise() + } +} + +private class NoOpExpirableExecutor(private val exec: Executor) : ExpirableExecutor { + override fun expireWith(disposable: Disposable): ExpirableExecutor = this + + override fun execute(runnable: Runnable) = exec.execute(runnable) + + override fun submit(callable: Callable): CancellablePromise = + CompletableFuture.supplyAsync({ callable.call() }, exec).asCancellablePromise() + + override fun submit(runnable: Runnable): CancellablePromise<*> = + CompletableFuture.runAsync(runnable, exec).thenApply { null }.asCancellablePromise() +} + +private class NoOpNonBlockingReadAction(private val callable: Callable) : NonBlockingReadAction { + override fun inSmartMode(project: Project): NonBlockingReadAction = this + override fun withDocumentsCommitted(project: Project): NonBlockingReadAction = this + override fun expireWhen(condition: BooleanSupplier): NonBlockingReadAction = this + override fun wrapProgress(indicator: ProgressIndicator): NonBlockingReadAction = this + override fun expireWith(disposable: Disposable): NonBlockingReadAction = this + override fun finishOnUiThread(modalityState: ModalityState, uiThreadAction: Consumer): NonBlockingReadAction = this + override fun coalesceBy(vararg equality: Any): NonBlockingReadAction = this + + override fun submit(backgroundThreadExecutor: Executor): CancellablePromise = + CompletableFuture.supplyAsync({ callable.call() }, backgroundThreadExecutor) + .asCancellablePromise() + + override fun executeSynchronously(): T = callable.call() +} + +private fun CompletableFuture.asCancellablePromise(): CancellablePromise = + CompletableFutureCancellablePromise(this) + +private class CompletableFutureCancellablePromise( + private val future: CompletableFuture +) : CancellablePromise { + + // Future + override fun cancel(mayInterruptIfRunning: Boolean): Boolean = future.cancel(mayInterruptIfRunning) + override fun isCancelled(): Boolean = future.isCancelled + override fun isDone(): Boolean = future.isDone + override fun get(): T = future.get() + override fun get(timeout: Long, unit: TimeUnit): T = future.get(timeout, unit) + + // CancellablePromise + override fun cancel() { future.cancel(true) } + + override fun onSuccess(handler: Consumer): CancellablePromise { + future.thenAccept(handler) + return this + } + + override fun onError(handler: Consumer): CancellablePromise { + future.exceptionally { e -> handler.accept(e); null } + return this + } + + override fun onProcessed(handler: Consumer): CancellablePromise { + future.whenComplete { value, _ -> handler.accept(value) } + return this + } + + // Promise + override fun getState(): Promise.State = when { + future.isCancelled || future.isCompletedExceptionally -> Promise.State.REJECTED + future.isDone -> Promise.State.SUCCEEDED + else -> Promise.State.PENDING + } + + override fun processed(child: Promise): Promise = this + + override fun blockingGet(timeout: Int, unit: TimeUnit): T = future.get(timeout.toLong(), unit) + + override fun then(handler: Function): Promise = + future.thenApply { handler.`fun`(it) }.asCancellablePromise() + + override fun thenAsync( + handler: Function> + ): Promise = future.thenCompose { value -> + @Suppress("UNCHECKED_CAST") + (handler.`fun`(value) as CompletableFutureCancellablePromise).future + }.asCancellablePromise() +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt new file mode 100644 index 0000000000..0a65305983 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt @@ -0,0 +1,85 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.index.packageExistsInSource +import com.itsaky.androidide.lsp.kotlin.compiler.index.subpackageNames +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import org.jetbrains.kotlin.analysis.api.platform.mergeSpecificProviders +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinCompositePackageProvider +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackagePartProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProvider +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderBase +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderFactory +import org.jetbrains.kotlin.analysis.api.platform.packages.KotlinPackageProviderMerger +import org.jetbrains.kotlin.analysis.api.platform.packages.createPackageProvider +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.load.kotlin.PackagePartProvider +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +internal class PackageProviderFactory: KtLspService, KotlinPackageProviderFactory { + private lateinit var project: Project + private lateinit var index: KtSymbolIndex + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.project = project + this.index = index + } + + override fun createPackageProvider(searchScope: GlobalSearchScope): KotlinPackageProvider = PackageProvider(project, searchScope, index) +} + +private class PackageProvider( + project: Project, + searchScope: GlobalSearchScope, + private val index: KtSymbolIndex +): KotlinPackageProviderBase(project, searchScope) { + override fun doesKotlinOnlyPackageExist(packageFqName: FqName): Boolean { + return packageFqName.isRoot || index.packageExistsInSource(packageFqName.asString()) + } + + override fun getKotlinOnlySubpackageNames(packageFqName: FqName): Set { + return index.subpackageNames(packageFqName.asString()).map { Name.identifier(it) }.toSet() + } +} + +internal class PackageProviderMerger(private val project: Project) : KotlinPackageProviderMerger { + override fun merge(providers: List): KotlinPackageProvider = + providers.mergeSpecificProviders<_, PackageProvider>(KotlinCompositePackageProvider.factory) { targetProviders -> + val combinedScope = GlobalSearchScope.union(targetProviders.map { it.searchScope }) + project.createPackageProvider(combinedScope).apply { + check(this is PackageProvider) { + "`${PackageProvider::class.simpleName}` can only be merged into a combined package provider of the same type." + } + } + } +} + +internal class PackagePartProviderFactory: KtLspService, KotlinPackagePartProviderFactory { + private lateinit var allLibraryRoots: List + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.allLibraryRoots = libraryRoots + } + + override fun createPackagePartProvider(scope: GlobalSearchScope): PackagePartProvider { + return JvmPackagePartProvider(latestLanguageVersionSettings, scope).apply { + addRoots(allLibraryRoots, MessageCollector.NONE) + } + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt new file mode 100644 index 0000000000..d297e756a1 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt @@ -0,0 +1,9 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.analysis.api.platform.KotlinDeserializedDeclarationsOrigin +import org.jetbrains.kotlin.analysis.api.platform.KotlinPlatformSettings + +class PlatformSettings : KotlinPlatformSettings { + override val deserializedDeclarationsOrigin: KotlinDeserializedDeclarationsOrigin + get() = KotlinDeserializedDeclarationsOrigin.BINARIES +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt new file mode 100644 index 0000000000..f663cc0e57 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt @@ -0,0 +1,168 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.NotUnderContentRootModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.backingFilePath +import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.KaPlatformInterface +import org.jetbrains.kotlin.analysis.api.platform.projectStructure.KotlinProjectStructureProviderBase +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule +import org.jetbrains.kotlin.analysis.api.projectStructure.KaNotUnderContentRootModule +import org.jetbrains.kotlin.analysis.low.level.api.fir.util.originalKtFile +import org.jetbrains.kotlin.cli.jvm.index.JavaRoot +import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtFile +import org.slf4j.LoggerFactory +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.pathString + +internal class ProjectStructureProvider : KtLspService, KotlinProjectStructureProviderBase() { + + companion object { + private val logger = LoggerFactory.getLogger(ProjectStructureProvider::class.java) + } + + private lateinit var modules: List + private lateinit var project: Project + + private val inMemoryVfToModule = ConcurrentHashMap() + private val pathToInMemoryVf = ConcurrentHashMap() + + fun registerInMemoryFile(sourcePath: String, vf: VirtualFile) { + pathToInMemoryVf.remove(sourcePath)?.let { inMemoryVfToModule.remove(it) } + + val module = findModuleForSourceId(sourcePath) ?: return + inMemoryVfToModule[vf] = module + pathToInMemoryVf[sourcePath] = vf + } + + fun unregisterInMemoryFile(sourcePath: String) { + pathToInMemoryVf.remove(sourcePath)?.let { inMemoryVfToModule.remove(it) } + } + + private val notUnderContentRootModuleWithoutPsiFile by lazy { + NotUnderContentRootModule( + id = "unnamed-outside-content-root", + moduleDescription = "unnamed-outside-content-root", + project = project, + ) + } + + override fun setupWith( + project: MockProject, + index: KtSymbolIndex, + modules: List, + libraryRoots: List + ) { + this.modules = modules + this.project = project + } + + override fun getModule( + element: PsiElement, + useSiteModule: KaModule? + ): KaModule { + val virtualFile = element.containingFile?.virtualFile + ?: return notUnderContentRootModuleWithoutPsiFile + + // Fast path: in-memory file registered by onFileContentChanged. + inMemoryVfToModule[virtualFile]?.let { return it } + + val visited = mutableSetOf() + + val backingFilePath = (element.containingFile as? KtFile)?.let { + it.backingFilePath ?: it.originalKtFile?.backingFilePath + } + + if (backingFilePath != null) { + findModuleForSourceId(backingFilePath.pathString)?.let { return it } + } + + // If the caller supplies a use-site module, search its dependency tree first. + // This covers the common case (element is in the same module or one of its direct + // library dependencies) without scanning every top-level module. + if (useSiteModule != null) { + searchVirtualFileInModule(virtualFile, useSiteModule, visited)?.let { return it } + } + + // Full scan: search every top-level module and their transitive dependencies. + // The shared `visited` set avoids re-visiting what we already searched above, + // but still reaches modules that are NOT in useSiteModule's dependency tree + // (e.g. a library module that is a sibling of useSiteModule, not a child of it). + modules.forEach { module -> + searchVirtualFileInModule(virtualFile, module, visited)?.let { return it } + } + + // Path-based fallback for in-memory LightVirtualFiles created by onFileContentChanged. + findModuleForSourceId(virtualFile.path)?.let { return it } + + return NotUnderContentRootModule( + id = "unnamed-outside-content-root", + moduleDescription = "unnamed-outside-content-root module with a PSI file.", + project = project, + file = element.containingFile, + ) + } + + /** + * Find the [KaModule] that owns the given [sourceId]. + * + * - For library JARs, [sourceId] is the JAR path — matched against [KtModule.contentRoots] exactly. + * - For source files, [sourceId] is the `.kt` file path — matched by checking whether the path + * falls under any source root in [KtModule.contentRoots]. + * + * The search is recursive: if the top-level modules do not match, their transitive dependencies + * are checked as well. + * + * @return The declaring [KaModule], or `null` if none is found. + */ + @OptIn(KaExperimentalApi::class) + fun findModuleForSourceId(sourceId: String): KaModule? { + val path = Paths.get(sourceId) + val visited = mutableSetOf() + + fun search(module: KaModule): KaModule? { + if (!visited.add(module.moduleDescription)) return null + if (module is KtModule) { + val roots = module.contentRoots + if (roots.contains(path) || roots.any { path.startsWith(it) }) return module + } + return module.directRegularDependencies.firstNotNullOfOrNull { search(it) } + } + + return modules.firstNotNullOfOrNull { search(it) } + } + + override fun getImplementingModules(module: KaModule): List { + // TODO: needs to be implemented when we want to support KMP + return emptyList() + } + + @OptIn(KaPlatformInterface::class) + override fun getNotUnderContentRootModule(project: Project): KaNotUnderContentRootModule { + return notUnderContentRootModuleWithoutPsiFile + } + + private fun searchVirtualFileInModule( + vf: VirtualFile, + module: KaModule, + visited: MutableSet + ): KaModule? { + if (visited.contains(module)) return null + if (module.contentScope.contains(vf)) return module + + visited.add(module) + module.directRegularDependencies + .forEach { dependency -> + val submodule = searchVirtualFileInModule(vf, dependency, visited) + if (submodule != null) return submodule + } + + return null + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt new file mode 100644 index 0000000000..bc9bfd9a48 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt @@ -0,0 +1,11 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.kotlin.com.intellij.openapi.editor.Document +import org.jetbrains.kotlin.com.intellij.openapi.editor.impl.DocumentWriteAccessGuard + +@Suppress("UnstableApiUsage") +class WriteAccessGuard: DocumentWriteAccessGuard() { + override fun isWritable(p0: Document): Result { + return success() + } +} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt index c63a908733..d3a32d82d6 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt @@ -1,10 +1,10 @@ package com.itsaky.androidide.lsp.kotlin.completion -import com.itsaky.androidide.lsp.kotlin.KtFileManager import com.itsaky.androidide.lsp.kotlin.utils.AnalysisContext import com.itsaky.androidide.lsp.models.CompletionItem import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.widget.CodeEditor +import org.jetbrains.kotlin.psi.KtFile import org.slf4j.LoggerFactory internal abstract class AdvancedKotlinEditHandler( @@ -23,7 +23,7 @@ internal abstract class AdvancedKotlinEditHandler( column: Int, index: Int ) { - val managedFile = analysisContext.env.fileManager.getOpenFile(analysisContext.file) + val managedFile = analysisContext.env.ktSymbolIndex.getOpenedKtFile(analysisContext.file) if (managedFile == null) { logger.error("Unable to perform edit. File not open.") return @@ -36,7 +36,7 @@ internal abstract class AdvancedKotlinEditHandler( } abstract fun performEdits( - managedFile: KtFileManager.ManagedFile, + ktFile: KtFile, editor: CodeEditor, item: CompletionItem ) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt index d58b2950ee..66d3b3fb93 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinClassImportEditHandler.kt @@ -1,18 +1,18 @@ package com.itsaky.androidide.lsp.kotlin.completion -import com.itsaky.androidide.lsp.kotlin.KtFileManager import com.itsaky.androidide.lsp.kotlin.utils.AnalysisContext import com.itsaky.androidide.lsp.kotlin.utils.insertImport import com.itsaky.androidide.lsp.models.ClassCompletionData import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.util.RewriteHelper import io.github.rosemoe.sora.widget.CodeEditor +import org.jetbrains.kotlin.psi.KtFile internal class KotlinClassImportEditHandler( analysisContext: AnalysisContext, ) : AdvancedKotlinEditHandler(analysisContext) { override fun performEdits( - managedFile: KtFileManager.ManagedFile, + ktFile: KtFile, editor: CodeEditor, item: CompletionItem ) { diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt index 409f85f946..be58063f7b 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt @@ -2,6 +2,7 @@ package com.itsaky.androidide.lsp.kotlin.completion import com.itsaky.androidide.lsp.api.describeSnippet import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.compiler.read import com.itsaky.androidide.lsp.kotlin.utils.AnalysisContext import com.itsaky.androidide.lsp.kotlin.utils.ContextKeywords import com.itsaky.androidide.lsp.kotlin.utils.ModifierFilter @@ -49,6 +50,7 @@ import org.jetbrains.kotlin.analysis.api.symbols.name import org.jetbrains.kotlin.analysis.api.symbols.receiverType import org.jetbrains.kotlin.analysis.api.types.KaClassType import org.jetbrains.kotlin.analysis.api.types.KaType +import org.jetbrains.kotlin.analysis.low.level.api.fir.util.originalKtFile import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName @@ -60,6 +62,7 @@ import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.types.Variance import org.slf4j.LoggerFactory +import kotlin.io.path.name private const val KT_COMPLETION_PLACEHOLDER = "KT_COMPLETION_PLACEHOLDER" @@ -73,9 +76,9 @@ private val logger = LoggerFactory.getLogger("KotlinCompletions") * @return The completion result. */ internal fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult { - val managedFile = fileManager.getOpenFile(params.file) - if (managedFile == null) { - logger.warn("No managed file for {}", params.file) + val ktFile = ktSymbolIndex.getOpenedKtFile(params.file) + if (ktFile == null) { + logger.warn("File {} is not open", params.file) return CompletionResult.EMPTY } @@ -94,37 +97,41 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi append(originalText, completionOffset, originalText.length) } - val completionKtFile = - managedFile.createInMemoryFileWithContent( - psiFactory = parser, - content = textWithPlaceholder - ) + val completionKtFile = project.read { + parser.createFile( + fileName = params.file.name, + text = textWithPlaceholder + ).apply { + originalFile = ktFile + originalKtFile = ktFile + } + } return try { - analyzeCopy( - useSiteElement = completionKtFile, - resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, - ) { - val ctx = - resolveAnalysisContext( - env = this@complete, - file = params.file, - ktFile = completionKtFile, - offset = completionOffset, - partial = partial - ) - - if (ctx == null) { - logger.error( - "Unable to determine context at offset {} in file {}", - completionOffset, - params.file - ) - return@analyzeCopy CompletionResult.EMPTY - } + project.read { + analyzeCopy( + useSiteElement = completionKtFile, + resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF, + ) { + val ctx = + resolveAnalysisContext( + env = this@complete, + file = params.file, + ktFile = completionKtFile, + offset = completionOffset, + partial = partial + ) + + if (ctx == null) { + logger.error( + "Unable to determine context at offset {} in file {}", + completionOffset, + params.file + ) + return@analyzeCopy CompletionResult.EMPTY + } - context(ctx) { - runBlocking { + context(ctx) { val items = mutableListOf() val completionContext = determineCompletionContext(ctx.psiElement) when (completionContext) { @@ -145,7 +152,7 @@ internal fun CompilationEnvironment.complete(params: CompletionParams): Completi throw e } - logger.warn("An error occurred while computing completions for {}", params.file) + logger.warn("An error occurred while computing completions for {}", params.file, e) return CompletionResult.EMPTY } } @@ -236,7 +243,7 @@ private fun KaSession.collectExtensionFunctions( } context(env: CompilationEnvironment, ctx: AnalysisContext) -private suspend fun KaSession.collectScopeCompletions( +private fun KaSession.collectScopeCompletions( to: MutableList, ) { val ktElement = ctx.ktElement @@ -273,95 +280,112 @@ private suspend fun KaSession.collectScopeCompletions( } context(env: CompilationEnvironment, ctx: AnalysisContext) -private suspend fun KaSession.collectUnimportedSymbols( +private fun KaSession.collectUnimportedSymbols( to: MutableList ) { + val currentPackage = ctx.ktElement.containingKtFile.packageDirective?.name + val useSiteModule = this.useSiteModule + + // Library symbols: JAR-based, use full SymbolVisibilityChecker val visibilityChecker = env.symbolVisibilityChecker - if (visibilityChecker == null) { - logger.warn("No visibility checker found") - return - } + env.libraryIndex?.findByPrefix(ctx.partial, limit = 0) + ?.forEach { symbol -> + val isVisible = visibilityChecker.isVisible( + symbol = symbol, + useSiteModule = useSiteModule, + useSitePackage = currentPackage, + ) + if (!isVisible) return@forEach + buildUnimportedSymbolItem(symbol)?.let { to += it } + } - val librarySymbolIndex = env.libraryIndex - if (librarySymbolIndex == null) { - logger.warn("Unable to find JVM library symbol index") - return - } + // Source symbols: project .kt files — skip private and same-package symbols + env.sourceIndex?.findByPrefix(ctx.partial, limit = 0) + ?.forEach { symbol -> + if (symbol.packageName == currentPackage) return@forEach - val useSiteModule = this.useSiteModule - librarySymbolIndex.findByPrefix(ctx.partial) - .collect { symbol -> val isVisible = visibilityChecker.isVisible( symbol = symbol, useSiteModule = useSiteModule, - useSitePackage = ctx.ktElement.containingKtFile.packageDirective?.name + useSitePackage = currentPackage ) - if (!isVisible) return@collect + if (!isVisible) return@forEach - if (symbol.kind.isCallable && !symbol.isTopLevel && !symbol.isExtension) { - // member-level, non-imported callable symbols should not be - // completed in scope completions - return@collect - } - - if (symbol.isExtension) { - val receiverTypeName = symbol.receiverTypeName - if (receiverTypeName != null) { - val receiverClassId = internalNameToClassId(receiverTypeName) - val receiverType = findClass(receiverClassId) - if (receiverType != null) { - val satisfiesImplicitReceivers = ctx.scopeContext.implicitReceivers.any { receiver -> - receiver.type.isSubtypeOf(receiverType) - } - - // the extension property/function's receiver type - // is not available in current context, so ignore this sym - if (!satisfiesImplicitReceivers) return@collect - } else return@collect - } - } + buildUnimportedSymbolItem(symbol)?.let { to += it } + } - val item = ktCompletionItem( - name = symbol.shortName, - kind = kindOf(symbol), - ) + // Generated symbols: R.jar etc. — all public by construction, no visibility check needed. + env.generatedIndex?.findByPrefix(ctx.partial, limit = 0) + ?.forEach { symbol -> + if (symbol.packageName == currentPackage) return@forEach + buildUnimportedSymbolItem(symbol)?.let { to += it } + } +} - item.overrideTypeText = symbol.returnTypeDisplay - when (symbol.kind) { - JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { - val data = symbol.data as JvmFunctionInfo - item.detail = data.signatureDisplay - item.setInsertTextForFunction( - name = symbol.shortName, - hasParams = data.parameterCount > 0, - ) +context(ctx: AnalysisContext) +private fun KaSession.buildUnimportedSymbolItem(symbol: JvmSymbol): CompletionItem? { + if (symbol.kind.isCallable && !symbol.isTopLevel && !symbol.isExtension) { + // member-level, non-extension callable symbols should not be + // completed in scope completions + return null + } - if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { - item.overrideTypeText = symbol.shortName + if (symbol.isExtension) { + val receiverTypeName = symbol.receiverTypeName + if (receiverTypeName != null) { + val receiverClassId = internalNameToClassId(receiverTypeName) + val receiverType = findClass(receiverClassId) + if (receiverType != null) { + val satisfiesImplicitReceivers = + ctx.scopeContext.implicitReceivers.any { receiver -> + receiver.type.isSubtypeOf(receiverType) } - } - - JvmSymbolKind.TYPE_ALIAS -> { - item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName - } + // the extension property/function's receiver type + // is not available in current context, so ignore this sym + if (!satisfiesImplicitReceivers) return null + } else return null + } + } - in JvmSymbolKind.CLASSIFIER_KINDS -> { - val classInfo = symbol.data as JvmClassInfo - item.detail = symbol.name - item.setClassCompletionData( - className = symbol.name, - isNested = classInfo.isInner, - topLevelClass = classInfo.containingClassFqName, - ) - } + val item = ktCompletionItem( + name = symbol.shortName, + kind = kindOf(symbol), + ) - else -> {} + item.overrideTypeText = symbol.returnTypeDisplay + when (symbol.kind) { + JvmSymbolKind.FUNCTION, JvmSymbolKind.CONSTRUCTOR -> { + val data = symbol.data as JvmFunctionInfo + item.detail = data.signatureDisplay + item.setInsertTextForFunction( + name = symbol.shortName, + hasParams = data.parameterCount > 0, + ) + if (symbol.kind == JvmSymbolKind.CONSTRUCTOR) { + item.overrideTypeText = symbol.shortName } + } - logger.debug("Adding completion item: {}", item) - to += item + JvmSymbolKind.TYPE_ALIAS -> { + item.detail = (symbol.data as JvmTypeAliasInfo).expandedTypeFqName } + + in JvmSymbolKind.CLASSIFIER_KINDS -> { + val classInfo = symbol.data as JvmClassInfo + item.detail = symbol.fqName + item.setClassCompletionData( + className = symbol.fqName, + isNested = classInfo.isInner, + topLevelClass = classInfo.containingClassFqName, + ) + } + + else -> {} + } + + logger.debug("Adding completion item: {}", item) + return item } private fun internalNameToClassId(internalName: String): ClassId { diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt deleted file mode 100644 index 010b187e41..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/SymbolVisibilityChecker.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.completion - -import com.itsaky.androidide.lsp.kotlin.compiler.ModuleResolver -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol -import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility -import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule -import org.jetbrains.kotlin.analysis.api.projectStructure.allDirectDependencies -import java.util.concurrent.ConcurrentHashMap - -internal class SymbolVisibilityChecker( - private val moduleResolver: ModuleResolver, -) { - // visibility check cache, for memoization - // useSiteModule -> list of modules visible from useSiteModule - private val moduleVisibilityCache = ConcurrentHashMap>() - - fun isVisible( - symbol: JvmSymbol, - useSiteModule: KaModule, - useSitePackage: String? = null, - ): Boolean { - val declaringModule = moduleResolver.findDeclaringModule(symbol.sourceId) - ?: return false - - if (!isReachable(useSiteModule, declaringModule)) return false - if (!arePlatformCompatible(useSiteModule, declaringModule)) return false - if (!isDeclarationVisible(symbol, useSiteModule, declaringModule, useSitePackage)) return false - - return true - } - - fun isReachable(useSiteModule: KaModule, declaringModule: KaModule): Boolean { - if (useSiteModule == declaringModule) return true - if (moduleVisibilityCache[useSiteModule]?.contains(declaringModule) == true) return true - - // walk the dependency graph - val visited = mutableSetOf() - val queue = ArrayDeque() - queue.add(useSiteModule) - - while (queue.isNotEmpty()) { - val current = queue.removeFirst() - if (!visited.add(current)) continue - if (current == declaringModule) return true - - queue.addAll(current.allDirectDependencies()) - } - - return false - } - - fun arePlatformCompatible(useSiteModule: KaModule, declaringModule: KaModule): Boolean { - val usePlatform = useSiteModule.targetPlatform - val declPlatform = declaringModule.targetPlatform - - // the declaring platform must be a superset of, or equal to the use - // site platform - return declPlatform.componentPlatforms.all { declComp -> - usePlatform.componentPlatforms.any { useComp -> - useComp == declComp || useComp.platformName == declComp.platformName - } - } - } - - fun isDeclarationVisible( - symbol: JvmSymbol, - useSiteModule: KaModule, - declaringModule: KaModule, - useSitePackage: String? = null, - ): Boolean { - val isSamePackage = useSitePackage != null && useSitePackage == symbol.packageName - - // TODO(itsaky): this should check whether the use-site element - // is contained in a class that is a descendant of the - // class declaring the given symbol. - // For now, we assume true in all cases. - val isDescendant = true - - return when (symbol.visibility) { - JvmVisibility.PUBLIC -> true - JvmVisibility.PRIVATE -> false - JvmVisibility.INTERNAL -> useSiteModule == declaringModule - JvmVisibility.PROTECTED -> isSamePackage || isDescendant - JvmVisibility.PACKAGE_PRIVATE -> isSamePackage - } - } -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt index fce0b8c721..72a3ff2aba 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt @@ -1,25 +1,20 @@ package com.itsaky.androidide.lsp.kotlin.diagnostic import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.compiler.read import com.itsaky.androidide.lsp.kotlin.utils.toRange import com.itsaky.androidide.lsp.models.DiagnosticItem import com.itsaky.androidide.lsp.models.DiagnosticResult import com.itsaky.androidide.lsp.models.DiagnosticSeverity -import com.itsaky.androidide.models.Position -import com.itsaky.androidide.models.Range -import com.itsaky.androidide.projects.FileManager import kotlinx.coroutines.CancellationException import org.jetbrains.kotlin.analysis.api.KaExperimentalApi +import org.jetbrains.kotlin.analysis.api.analyze import org.jetbrains.kotlin.analysis.api.components.KaDiagnosticCheckerFilter import org.jetbrains.kotlin.analysis.api.diagnostics.KaDiagnosticWithPsi import org.jetbrains.kotlin.analysis.api.diagnostics.KaSeverity -import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange -import org.jetbrains.kotlin.com.intellij.psi.PsiDocumentManager -import org.jetbrains.kotlin.com.intellij.psi.PsiFile import org.slf4j.LoggerFactory import java.nio.file.Path -import kotlin.time.Clock -import kotlin.time.toKotlinInstant +import kotlin.math.log private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") @@ -37,31 +32,30 @@ internal fun CompilationEnvironment.collectDiagnosticsFor(file: Path): Diagnosti @OptIn(KaExperimentalApi::class) private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { - val managed = fileManager.getOpenFile(file) - if (managed == null) { - logger.warn("Attempt to analyze non-open file: {}", file) - return DiagnosticResult.NO_UPDATE + var ktFile = ktSymbolIndex.getOpenedKtFile(file) + if (ktFile == null) { + onFileOpen(file) + ktFile = ktSymbolIndex.getOpenedKtFile(file) } - val analyzedAt = managed.analyzeTimestamp - val modifiedAt = FileManager.getLastModified(file) - if (analyzedAt > modifiedAt.toKotlinInstant()) { - logger.debug("Skipping analysis. File unmodified.") + if (ktFile == null) { + logger.warn("File {} is not accessible", file) return DiagnosticResult.NO_UPDATE } - val rawDiagnostics = managed.analyze { ktFile -> - ktFile.collectDiagnostics(filter = KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + val diagnostics = project.read { + analyze(ktFile) { + ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) + .map { it.toDiagnosticItem() } + } } - logger.info("Found {} diagnostics", rawDiagnostics.size) + logger.info("Found {} diagnostics", diagnostics.size) return DiagnosticResult( - file = file, diagnostics = rawDiagnostics.map { rawDiagnostic -> - rawDiagnostic.toDiagnosticItem() - }).also { - managed.analyzeTimestamp = Clock.System.now() - } + file = file, + diagnostics = diagnostics + ) } private fun KaDiagnosticWithPsi<*>.toDiagnosticItem(): DiagnosticItem { diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt index c8d0398ff6..69c4c612c2 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt @@ -1,15 +1,20 @@ package com.itsaky.androidide.lsp.kotlin.utils -import com.itsaky.androidide.lsp.kotlin.compiler.ModuleResolver +import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol import org.appdevforall.codeonthego.indexing.jvm.JvmVisibility import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule import org.jetbrains.kotlin.analysis.api.projectStructure.allDirectDependencies +import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap internal class SymbolVisibilityChecker( - private val moduleResolver: ModuleResolver, + private val structureProvider: ProjectStructureProvider, ) { + companion object { + private val logger = LoggerFactory.getLogger(SymbolVisibilityChecker::class.java) + } + // visibility check cache, for memoization // useSiteModule -> list of modules visible from useSiteModule private val moduleVisibilityCache = ConcurrentHashMap>() @@ -19,7 +24,7 @@ internal class SymbolVisibilityChecker( useSiteModule: KaModule, useSitePackage: String? = null, ): Boolean { - val declaringModule = moduleResolver.findDeclaringModule(symbol.sourceId) + val declaringModule = structureProvider.findModuleForSourceId(symbol.sourceId) ?: return false if (!isReachable(useSiteModule, declaringModule)) return false diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt new file mode 100644 index 0000000000..2232db0561 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt @@ -0,0 +1,7 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import java.nio.file.Path + +fun VirtualFile.toNioPathOrNull(): Path? = + runCatching { toNioPath() }.getOrNull() \ No newline at end of file diff --git a/lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml b/lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml new file mode 100644 index 0000000000..ae91693bae --- /dev/null +++ b/lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/subprojects/kotlin-analysis-api/build.gradle.kts b/subprojects/kotlin-analysis-api/build.gradle.kts index 2e4f08710f..5b6a20674c 100644 --- a/subprojects/kotlin-analysis-api/build.gradle.kts +++ b/subprojects/kotlin-analysis-api/build.gradle.kts @@ -12,7 +12,7 @@ android { val ktAndroidRepo = "https://github.com/appdevforall/kotlin-android" val ktAndroidVersion = "2.3.255" -val ktAndroidTag = "v${ktAndroidVersion}-f047b07" +val ktAndroidTag = "v${ktAndroidVersion}-1e59a8b" val ktAndroidJarName = "analysis-api-standalone-embeddable-for-ide-${ktAndroidVersion}-SNAPSHOT.jar" externalAssets { @@ -21,7 +21,7 @@ externalAssets { source = AssetSource.External( url = uri("$ktAndroidRepo/releases/download/$ktAndroidTag/$ktAndroidJarName"), - sha256Checksum = "c9897c94ae1431fadeb4fa5b05dd4d478a60c4589f38f801e07c72405a7b34b1", + sha256Checksum = "9d7d60f30169da932f21c130f3955016b165d45215564a1fb883021e59528835", ) } } diff --git a/subprojects/project-models/src/main/proto/android.proto b/subprojects/project-models/src/main/proto/android.proto index b2b8f71da4..8f3e9f2600 100644 --- a/subprojects/project-models/src/main/proto/android.proto +++ b/subprojects/project-models/src/main/proto/android.proto @@ -276,4 +276,8 @@ message AndroidProject { // The variant dependencies of this project. VariantDependencies variantDependencies = 10; + + // The compiler settings for Kotlin sources. + optional KotlinCompilerSettings kotlinCompilerSettings = 11; + } \ No newline at end of file diff --git a/subprojects/project-models/src/main/proto/common.proto b/subprojects/project-models/src/main/proto/common.proto index e150991e30..f01d284264 100644 --- a/subprojects/project-models/src/main/proto/common.proto +++ b/subprojects/project-models/src/main/proto/common.proto @@ -13,6 +13,15 @@ message JavaCompilerSettings { string targetCompatibility = 2; } +// Kotlin compiler settings +message KotlinCompilerSettings { + // The Kotlin API version. + string apiVersion = 1; + + // The target JVM version. + string jvmTarget = 2; +} + // Info about an external library. message LibraryInfo { diff --git a/subprojects/project-models/src/main/proto/java.proto b/subprojects/project-models/src/main/proto/java.proto index 196e47b0f6..ec23a375b2 100644 --- a/subprojects/project-models/src/main/proto/java.proto +++ b/subprojects/project-models/src/main/proto/java.proto @@ -71,4 +71,7 @@ message JavaProject { // The Java compiler settings for this project. JavaCompilerSettings javaCompilerSettings = 3; + + // The Kotlin compiler settings. + optional KotlinCompilerSettings kotlinCompilerSettings = 4; } \ No newline at end of file diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/AndroidProjectExts.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/AndroidProjectExts.kt index 95d03336db..389bc92309 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/AndroidProjectExts.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/AndroidProjectExts.kt @@ -61,6 +61,7 @@ fun createAndroidProjectProtoModel( }, mainSourceSet = basicAndroidProject.mainSourceSet?.asProtoModel(), javaCompilerSettings = androidProject.javaCompileOptions?.asProtoModel(), + kotlinCompilerSettings = null, // TODO: Read kotlin compiler settings viewBindingOptions = androidProject.viewBindingOptions?.asProtoModel(), bootClassPathsList = basicAndroidProject.bootClasspath.map { file -> file.absolutePath }, variantList = diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/JavaProjectExts.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/JavaProjectExts.kt index ec708bc867..be15aab5ab 100644 --- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/JavaProjectExts.kt +++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/serial/JavaProjectExts.kt @@ -26,6 +26,7 @@ fun createJavaProjectProtoModel( contentRootList = ideaModule.contentRoots.map { it.asProtoModel() }, dependencyList = ideaModule.dependencies.map { it.asProtoModel(moduleNameToPath) }, javaCompilerSettings = createCompilerSettings(ideaProject, ideaModule), + kotlinCompilerSettings = null, // TODO: read kotlin compiler settings ) private fun createCompilerSettings(