Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1b069cb
fix: add ability to re-write class name references in desugar plugin
itsaky-adfa Mar 19, 2026
a70fd66
feat: integrate Kotlin analysis API
itsaky-adfa Mar 19, 2026
4ca1e97
Merge branch 'stage' into fix/ADFA-3366-include-analysis-api-as-depen…
itsaky-adfa Mar 19, 2026
43286a6
fix: update kotlin-android to latest version
itsaky-adfa Mar 23, 2026
f1fe62f
fix: remove UnsafeImpl
itsaky-adfa Mar 25, 2026
6e6d8b3
fix: update kotlin-android to latest version
itsaky-adfa Mar 25, 2026
b208658
fix: replace usages of Unsafe with UnsafeImpl
itsaky-adfa Mar 24, 2026
c844ad2
fix: make Kotlin LSP no-op
itsaky-adfa Mar 23, 2026
58db2cb
feat: configure K2 standalone session when setting up LSP
itsaky-adfa Mar 24, 2026
bf7acd1
fix: JvmTarget resolution fails for input "21"
itsaky-adfa Mar 24, 2026
dc62a51
fix: do not early-init VirtualFileSystem
itsaky-adfa Mar 24, 2026
4b1c8e4
fix: remove replaceClass desugar instruction for Unsafe
itsaky-adfa Mar 25, 2026
b14f6ee
fix: ensure boot class path is added as dependency to Android modules
itsaky-adfa Mar 26, 2026
54ca7a9
feat: add diagnostic provider for Kotlin
itsaky-adfa Mar 26, 2026
6e4d458
fix: remove unnecessary log statement
itsaky-adfa Mar 26, 2026
5d841a3
fix: update to latest kotlin-android release
itsaky-adfa Mar 26, 2026
4b7b0f2
fix: always re-initialize K2 session on setupWithProject
itsaky-adfa Mar 26, 2026
e2f137a
fix: diagnostics are always collected from the on-disk file
itsaky-adfa Mar 26, 2026
09fc9ea
feat: add the ability to incrementally invalidate source roots on pro…
itsaky-adfa Mar 30, 2026
dd3d519
fix: dispatch build-related events from GradleBuildService
itsaky-adfa Mar 31, 2026
d6defa6
feat: introduct KtFileManager
itsaky-adfa Mar 31, 2026
4666893
fix: add initial K2-backed scope code completions
itsaky-adfa Apr 1, 2026
2f982a7
feat: add member completions backed by K2
itsaky-adfa Apr 2, 2026
4129a47
feat: suggest local and imported extension functions
itsaky-adfa Apr 2, 2026
7d736fa
fix: do not suggest extension functions for scope completions
itsaky-adfa Apr 2, 2026
87234f8
feat: add scope-sensitive keyword completions
itsaky-adfa Apr 3, 2026
7d78db1
feat: add indexing api and service implementation
itsaky-adfa Apr 6, 2026
c35aa9f
fix: metadata version is sometimes not parsed
itsaky-adfa Apr 7, 2026
bede4db
fix: make JvmSymbolIndex exclusive to external libraries
itsaky-adfa Apr 8, 2026
9eb4ffe
feat: add module resolver to resolve library modules from source path
itsaky-adfa Apr 8, 2026
1fdd5b3
feat: add support for completing non-imported symbols
itsaky-adfa Apr 8, 2026
c78d5ab
fix: use Kotlin context receivers feature
itsaky-adfa Apr 8, 2026
47d1738
fix: auto-import un-imported classes
itsaky-adfa Apr 9, 2026
eed2d03
fix: use internal name repr for library index
itsaky-adfa Apr 9, 2026
e6fcf6e
fix: filter-out ext syms with inapplicable receivers
itsaky-adfa Apr 9, 2026
9fa3d4a
feat: add Kotlin source file index
itsaky-adfa Apr 9, 2026
ab5c7d9
fix: infinite loop when converting type names to display names
itsaky-adfa Apr 10, 2026
0ec7b4b
fix: update ModuleResolver to resolve KaSourceModule from file path
itsaky-adfa Apr 10, 2026
bf9b2da
fix: update KotlinSourceScanner to use internal names in index
itsaky-adfa Apr 10, 2026
49e7efe
feat: add custom implementations of analysis API services
itsaky-adfa Apr 13, 2026
a33427d
fix: use StandaloneProjectFactory to create MockProject
itsaky-adfa Apr 13, 2026
51d0686
feat: add support for indexing classes using VirtualFile
itsaky-adfa Apr 13, 2026
e77df48
feat: create in-memory KtFile instances for modified files
itsaky-adfa Apr 14, 2026
340b08b
fix: IndexWorker always re-index libraries
itsaky-adfa Apr 14, 2026
a1d999e
feat: add KtFile.backingFilePath for better resolution of declaring m…
itsaky-adfa Apr 14, 2026
49542ad
fix: remove library indexing logic for kt index worker
itsaky-adfa Apr 14, 2026
888193f
feat: add index for generated JARs
itsaky-adfa Apr 14, 2026
f3997a5
fix: java source files are not recognized by analysis API
itsaky-adfa Apr 14, 2026
bcbbba3
chore: trigger ci
itsaky-adfa Apr 15, 2026
d109866
Merge branch 'stage' into feat/ADFA-3320-non-imported-ext-func-comple…
itsaky-adfa Apr 20, 2026
083388b
Merge branch 'feat/ADFA-3320-non-imported-ext-func-completions' into …
itsaky-adfa Apr 20, 2026
26406cc
Merge branch 'stage' into feat/ADFA-3581-source-index
itsaky-adfa Apr 20, 2026
81f403d
fix: remove unused duplicate file
itsaky-adfa Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -103,6 +104,9 @@ class IDEApplication :
private set

init {
System.setProperty("java.awt.headless", "true")
setupIdeaStandaloneExecution()

@Suppress("Deprecation")
Shell.setDefaultBuilder(
Shell.Builder
Expand Down
2 changes: 2 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ android {
}

dependencies {
compileOnly(libs.composite.javac)

api(platform(libs.sora.bom))
api(libs.common.editor)
api(libs.common.lang3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -77,13 +75,12 @@ open class FilteredIndex<T : Indexable>(
suspend fun isCached(sourceId: String): Boolean =
backing.containsSource(sourceId)

override fun query(query: IndexQuery): Flow<T> {
// If the query already specifies a sourceId, check if it's active
override fun query(query: IndexQuery): Sequence<T> {
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? {
Expand All @@ -95,7 +92,7 @@ open class FilteredIndex<T : Indexable>(
return sourceId in activeSources && backing.containsSource(sourceId)
}

override fun distinctValues(fieldName: String): Flow<String> {
override fun distinctValues(fieldName: String): Sequence<String> {
// 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
Expand All @@ -109,4 +106,4 @@ open class FilteredIndex<T : Indexable>(
activeSources.clear()
if (backing is Closeable) backing.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -54,35 +52,22 @@ class InMemoryIndex<T : Indexable>(
}
}

override fun query(query: IndexQuery): Flow<T> = flow {
override fun query(query: IndexQuery): Sequence<T> {
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]

override suspend fun containsSource(sourceId: String): Boolean =
sourceMap.containsKey(sourceId)

override fun distinctValues(fieldName: String): Flow<String> = flow {
val fieldMap = fieldMaps[fieldName] ?: return@flow
lock.read {
for (value in fieldMap.keys) {
emit(value)
}
}
}

override suspend fun insert(entries: Flow<T>) {
entries.collect { entry -> insertSingle(entry) }
override fun distinctValues(fieldName: String): Sequence<String> {
val fieldMap = fieldMaps[fieldName] ?: return emptySequence()
return lock.read { fieldMap.keys.toList() }.asSequence()
}

override suspend fun insertAll(entries: Sequence<T>) {
Expand All @@ -93,7 +78,7 @@ class InMemoryIndex<T : Indexable>(
}
}

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
Expand Down Expand Up @@ -176,10 +161,6 @@ class InMemoryIndex<T : Indexable>(
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand All @@ -21,28 +21,17 @@ class MergedIndex<T : Indexable>(

constructor(vararg indexes: ReadableIndex<T>) : this(indexes.toList())

override fun query(query: IndexQuery): Flow<T> = channelFlow {
val seen = ConcurrentHashMap.newKeySet<String>()
override fun query(query: IndexQuery): Sequence<T> = 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<String>()
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++
}
}
}
Expand All @@ -61,15 +50,11 @@ class MergedIndex<T : Indexable>(
return indexes.any { it.containsSource(sourceId) }
}

override fun distinctValues(fieldName: String): Flow<String> = channelFlow {
val seen = ConcurrentHashMap.newKeySet<String>()
override fun distinctValues(fieldName: String): Sequence<String> = sequence {
val seen = mutableSetOf<String>()
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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
* ```
Expand All @@ -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<T : Indexable>(
class SQLiteIndex<T : Indexable>(
override val descriptor: IndexDescriptor<T>,
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<T> {

Expand Down Expand Up @@ -99,18 +102,18 @@ class PersistentIndex<T : Indexable>(
createTable(db)
}

override fun query(query: IndexQuery): Flow<T> = flow {
override fun query(query: IndexQuery): Sequence<T> {
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(
Expand All @@ -133,41 +136,17 @@ class PersistentIndex<T : Indexable>(
cursor.use { it.moveToFirst() }
}

override fun distinctValues(fieldName: String): Flow<String> = flow {
override fun distinctValues(fieldName: String): Sequence<String> {
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<T>) = withContext(Dispatchers.IO) {
val batch = mutableListOf<T>()
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<T>) = withContext(Dispatchers.IO) {
Expand Down
Loading
Loading