Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 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
a0e3075
fix: remove unused kotlin lsp modules
itsaky-adfa Apr 15, 2026
fbab307
fix: add basic test cases for indexing and keyword completions
itsaky-adfa Apr 15, 2026
ea6c1ca
feat: add snippets for Kotlin sources
itsaky-adfa Apr 16, 2026
f29e83a
feat: make comment/uncomment line actions generic to LSP implementations
itsaky-adfa Apr 16, 2026
9e18834
fix: register comment/uncomment line actions in Kotlin LSP
itsaky-adfa Apr 16, 2026
c569cb6
fix: comment/uncomment actions are overridden by language servers
itsaky-adfa Apr 16, 2026
429e500
fix: invalid label for uncomment line action
itsaky-adfa Apr 16, 2026
ffd785e
fix: collect and report Kotlin syntax errors
itsaky-adfa Apr 16, 2026
7a9b0e9
feat: add 'Add import' action for kotlin source files
itsaky-adfa Apr 17, 2026
3fdbe67
fix: un-imported extension symbols are not shown in completion items
itsaky-adfa Apr 18, 2026
4889377
feat: add cancellation support for Kotlin diagnostics
itsaky-adfa Apr 18, 2026
a7c76ce
fix: cancel completion when partial candidate is empty
itsaky-adfa Apr 18, 2026
759d6cc
fix: inline Collection.ifNotEmpty ext func
itsaky-adfa Apr 20, 2026
b6fd00a
Merge branch 'stage' into fix/ADFA-3727
itsaky-adfa Apr 21, 2026
c255c52
fix: remove unused class
itsaky-adfa Apr 21, 2026
43dc186
Merge branch 'fix/ADFA-3727' into fix/ADFA-3731
itsaky-adfa Apr 21, 2026
df37a6e
Merge branch 'stage' into fix/ADFA-3731
itsaky-adfa Apr 21, 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
1 change: 1 addition & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {

api(libs.androidx.core.ktx)
api(libs.common.kotlin)
api(libs.kotlinx.coroutines.core)

api(projects.buildInfo)
api(projects.eventbusAndroid)
Expand Down
13 changes: 13 additions & 0 deletions common/src/main/java/com/itsaky/androidide/tasks/coroutineUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import java.io.InterruptedIOException
import kotlin.coroutines.CoroutineContext
Expand All @@ -44,8 +46,17 @@ class JobCancelChecker @JvmOverloads constructor(
job = null
super.cancel()
}

override fun abortIfCancelled() {
job?.ensureActive()
}
}

/**
* Create an [ICancelChecker] for the current [Job].
*/
suspend fun createJobCancelChecker() = JobCancelChecker(currentCoroutineContext()[Job])

/**
* Calls [CoroutineScope.cancel] only if a job is active in the scope.
*
Expand All @@ -66,6 +77,8 @@ fun CoroutineScope.cancelIfActive(exception: CancellationException? = null) {
job?.cancel(exception)
}

suspend fun ensureCoroutineContextActive() = currentCoroutineContext().ensureActive()

/**
* Launches a new coroutine without blocking the current thread. This method shows a progress
* indicator using [Flashbar] while the [action] is being executed. The [Flashbar] is automatically
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.itsaky.androidide.utils

inline fun <E, T: Collection<E>> T.ifNotEmpty(crossinline action: T.() -> Unit) {
if (isNotEmpty()) action()
Comment thread
itsaky-adfa marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.itsaky.androidide.utils

import com.itsaky.androidide.progress.ICancelChecker
import com.itsaky.androidide.tasks.JobCancelChecker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

class KeyedDebouncingAction<T: Any>(
private val scope: CoroutineScope,
private val debounceDuration: Duration = DEBOUNCE_DURATION_DEFAULT,
private val actionContext: CoroutineContext = Dispatchers.Default,
private val action: suspend (T, ICancelChecker) -> Unit,
) {

private data class ActionEntry<T>(
val channel: Channel<T>,
val job: Job,
) {
fun cancel() {
channel.close()
job.cancel()
}
}

private val pending = ConcurrentHashMap<T, ActionEntry<T>>()

companion object {
val DEBOUNCE_DURATION_DEFAULT = 400.milliseconds
}

fun cancelPending(key: T) {
pending.remove(key)?.cancel()
}

fun schedule(key: T) {
val entry = pending.computeIfAbsent(key) { createEntry() }
entry.channel.trySend(key)
}

private fun createEntry(): ActionEntry<T> {
val channel = Channel<T>(Channel.CONFLATED)
val job = scope.launch(actionContext) {
for (latestKey in channel) {
delay(debounceDuration)
ensureActive()

val cancelChecker = JobCancelChecker(currentCoroutineContext()[Job])
action(latestKey, cancelChecker)
}
}

return ActionEntry(channel, job)
}

fun cancelAll() {
pending.values.forEach { it.cancel() }
pending.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
public class LogUtils {

public static final int MAX_TAG_LENGTH = 23;
public static final String PATTERN_LAYOUT_MESSAGE_PATTERN = "%msg%n";
public static final String PATTERN_LAYOUT_MESSAGE_PATTERN = "[%thread] %msg%n";

public static boolean isJvm() {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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.DocumentSelectedEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.api.IServerSettings
Expand All @@ -48,17 +48,14 @@ import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.api.Workspace
import com.itsaky.androidide.tasks.createJobCancelChecker
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ifNotEmpty
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.appdevforall.codeonthego.indexing.jvm.JvmLibraryIndexingService
import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex
import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex
Expand All @@ -69,23 +66,19 @@ import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.config.LanguageVersion
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.time.Duration.Companion.milliseconds

class KotlinLanguageServer : ILanguageServer {

private var _client: ILanguageClient? = null
private var _settings: IServerSettings? = null
private var selectedFile: Path? = null
private var initialized = false

private val scope =
CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!))
private var projectModel: KotlinProjectModel? = null
private var compiler: Compiler? = null
private var analyzeJob: Job? = null

override val serverId: String = SERVER_ID

Expand All @@ -96,7 +89,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"
private val logger = LoggerFactory.getLogger(KotlinLanguageServer::class.java)
Expand All @@ -121,6 +113,7 @@ class KotlinLanguageServer : ILanguageServer {

override fun connectClient(client: ILanguageClient?) {
this._client = client
this.compiler?.updateLanguageClient(client)
}

override fun applySettings(settings: IServerSettings?) {
Expand Down Expand Up @@ -184,13 +177,22 @@ class KotlinLanguageServer : ILanguageServer {
languageVersion = LanguageVersion.LATEST_STABLE,
)

compiler.updateLanguageClient(client)
this.compiler = compiler
} else {
logger.info("Updating project model")

projectModel?.update(workspace, jvmPlatform)
}

// Open already open files
// we won't get an event for these
FileManager.activeDocuments.ifNotEmpty {
forEach { document ->
compiler?.compilationEnvironmentFor(document.file)
?.openFileIfNeeded(document.file)
}
}

initialized = true
logger.info("Kotlin project initialized")
}
Expand Down Expand Up @@ -262,7 +264,8 @@ class KotlinLanguageServer : ILanguageServer {
return DiagnosticResult.NO_UPDATE
}

return compiler?.compilationEnvironmentFor(file)?.collectDiagnosticsFor(file)
return compiler?.compilationEnvironmentFor(file)
?.let { context(it) { collectDiagnosticsFor(file, createJobCancelChecker()) } }
?: DiagnosticResult.NO_UPDATE
}

Expand All @@ -273,32 +276,8 @@ class KotlinLanguageServer : ILanguageServer {
return
}

compiler?.compilationEnvironmentFor(event.openedFile)?.apply {
onFileOpen(event.openedFile)
}

selectedFile = event.openedFile
debouncingAnalyze()
}

private fun debouncingAnalyze() {
analyzeJob?.cancel()
analyzeJob = scope.launch(Dispatchers.Default) {
delay(ANALYZE_DEBOUNCE_DELAY)
analyzeSelected()
}
}

private suspend fun analyzeSelected() {
val file = selectedFile ?: return
val client = _client ?: return

if (!Files.exists(file)) return

val result = analyze(file)
withContext(Dispatchers.Main) {
client.publishDiagnostics(result)
}
compiler?.compilationEnvironmentFor(event.openedFile)
?.onFileOpen(event.openedFile)
}

@Subscribe(threadMode = ThreadMode.ASYNC)
Expand All @@ -308,11 +287,9 @@ class KotlinLanguageServer : ILanguageServer {
return
}

compiler?.compilationEnvironmentFor(event.changedFile)?.apply {
onFileContentChanged(event.changedFile)
}
compiler?.compilationEnvironmentFor(event.changedFile)
?.onFileContentChanged(event.changedFile)

debouncingAnalyze()
}

@Subscribe(threadMode = ThreadMode.ASYNC)
Expand All @@ -322,26 +299,19 @@ class KotlinLanguageServer : ILanguageServer {
return
}

compiler?.compilationEnvironmentFor(event.closedFile)?.apply {
onFileClosed(event.closedFile)
}
compiler?.compilationEnvironmentFor(event.closedFile)
?.onFileClosed(event.closedFile)

if (FileManager.getActiveDocumentCount() == 0) {
selectedFile = null
analyzeJob?.cancel("No active files")
}
}

@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onDocumentSelected(event: DocumentSelectedEvent) {
if (!DocumentUtils.isKotlinFile(event.selectedFile)) {
fun onDocumentSaved(event: DocumentSaveEvent) {
if (!DocumentUtils.isKotlinFile(event.savedFile)) {
return
}

selectedFile = event.selectedFile
val uri = event.selectedFile.toUri().toString()

logger.debug("onDocumentSelected: uri={}", uri)
compiler?.compilationEnvironmentFor(event.savedFile)
?.onFileSaved(event.savedFile)
}
}
Loading
Loading