Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
1586b8f
fix: remove unused onSourcesChanged func
itsaky-adfa Apr 16, 2026
85807ca
fix: remove IncrementalModificationTracker
itsaky-adfa Apr 16, 2026
abecf1a
fix: remove unused Gradle build events
itsaky-adfa Apr 16, 2026
68e316a
Merge branch 'stage' into feat/ADFA-3320-session-source-files-invalid…
itsaky-adfa Apr 18, 2026
4988c26
Merge branch 'stage' into feat/ADFA-3320-session-source-files-invalid…
itsaky-adfa Apr 20, 2026
d54e63a
Merge branch 'feat/ADFA-3320-session-source-files-invalidation' into …
itsaky-adfa Apr 20, 2026
e8f9144
Merge branch 'feat/ADFA-3320-KtFileManager' into feat/ADFA-3320-code-…
itsaky-adfa Apr 20, 2026
064aa61
Merge branch 'stage' into feat/ADFA-3320-code-completions
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ import io.github.rosemoe.sora.widget.CodeEditor
*/
open class BaseJavaEditHandler : DefaultEditHandler() {

override fun executeCommand(editor: CodeEditor, command: Command?) {
if (editor is ILspEditor) {
editor.executeCommand(command)
return
}
super.executeCommand(editor, command)
}
override fun executeCommand(editor: CodeEditor, command: Command?) {
if (editor is ILspEditor) {
editor.executeCommand(command)
return
}
super.executeCommand(editor, command)
}
}
1 change: 1 addition & 0 deletions lsp/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {

implementation(projects.lsp.api)
implementation(projects.lsp.models)
implementation(projects.editorApi)
implementation(projects.eventbusEvents)
implementation(projects.subprojects.kotlinAnalysisApi)
implementation(projects.shared)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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.completion.complete
import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
Expand Down Expand Up @@ -160,7 +161,14 @@ class KotlinLanguageServer : ILanguageServer {
}

override fun complete(params: CompletionParams?): CompletionResult {
return CompletionResult.EMPTY
if (params == null) {
logger.warn("Cannot complete for null params")
return CompletionResult.EMPTY
}

logger.debug("complete(position={}, file={})", params.position, params.file)
return compiler?.compilationEnvironmentFor(params.file)?.complete(params)
?: CompletionResult.EMPTY
}

override suspend fun findReferences(params: ReferenceParams): ReferenceResult {
Expand Down Expand Up @@ -268,7 +276,6 @@ class KotlinLanguageServer : ILanguageServer {

compiler?.compilationEnvironmentFor(event.changedFile)?.apply {
val content = FileManager.getDocumentContents(event.changedFile)
logger.info("Notifying KtFileManager for file {} with contents {}", event.changedFile, content)
fileManager.onFileContentChanged(event.changedFile, content)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.itsaky.androidide.lsp.kotlin.completion

import com.itsaky.androidide.editor.api.ILspEditor
import com.itsaky.androidide.lsp.edits.DefaultEditHandler
import com.itsaky.androidide.lsp.models.Command
import io.github.rosemoe.sora.widget.CodeEditor

/**
* Implementation of [DefaultEditHandler] which avoids reflection in
* [DefaultEditHandler.executeCommand].
*
* @author Akash Yadav
*/
open class BaseKotlinEditHandler : DefaultEditHandler() {

override fun executeCommand(editor: CodeEditor, command: Command?) {
if (editor is ILspEditor) {
editor.executeCommand(command)
return
}
super.executeCommand(editor, command)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.itsaky.androidide.lsp.kotlin.completion

/**
* The context for the providing code completions in a file.
*/
enum class CompletionContext {

/**
* Scope completions (local variables, parameters, etc.)
*/
Scope,

/**
* Member completions (properties, member functions, extension functions, etc.)
*/
Member,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.itsaky.androidide.lsp.kotlin.completion

import org.jetbrains.kotlin.lexer.KtKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens.*

/**
*
*/
object ContextKeywords {

/** Hard keywords valid as *statement starters* inside a function body */
val STATEMENT_KEYWORDS = setOf(
IF_KEYWORD, ELSE_KEYWORD, WHEN_KEYWORD, WHILE_KEYWORD, DO_KEYWORD, FOR_KEYWORD,
TRY_KEYWORD, RETURN_KEYWORD, THROW_KEYWORD, BREAK_KEYWORD, CONTEXT_KEYWORD,
VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD,// local declarations
OBJECT_KEYWORD,// anonymous / local object
CLASS_KEYWORD,// local class (rare but legal)
)

/** Declaration starters at top-level / class body */
val DECLARATION_KEYWORDS = setOf(
VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD, CLASS_KEYWORD, INTERFACE_KEYWORD, OBJECT_KEYWORD,
TYPE_ALIAS_KEYWORD, CONSTRUCTOR_KEYWORD, INIT_KEYWORD,
)

val TOP_LEVEL_ONLY = setOf(PACKAGE_KEYWORD, IMPORT_KEYWORD)

/**
* Resolve valid keywords for the given declaration context.
*
* @param ctx The declaration context.
* @return The keyword tokens for the declaration context.
*/
fun keywordsFor(ctx: DeclarationContext): Set<KtKeywordToken> = when (ctx) {
DeclarationContext.TOP_LEVEL,
DeclarationContext.SCRIPT_TOP_LEVEL -> TOP_LEVEL_ONLY + DECLARATION_KEYWORDS

DeclarationContext.CLASS_BODY -> DECLARATION_KEYWORDS +
setOf(INIT_KEYWORD, CONSTRUCTOR_KEYWORD)

DeclarationContext.INTERFACE_BODY -> setOf(
VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD, CLASS_KEYWORD,
INTERFACE_KEYWORD, OBJECT_KEYWORD, TYPE_ALIAS_KEYWORD
)

DeclarationContext.OBJECT_BODY,
DeclarationContext.ENUM_BODY -> DECLARATION_KEYWORDS - setOf(CONSTRUCTOR_KEYWORD)

DeclarationContext.ANNOTATION_BODY -> setOf(VAL_KEYWORD) // annotation params only

DeclarationContext.FUNCTION_BODY -> STATEMENT_KEYWORDS
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package com.itsaky.androidide.lsp.kotlin.completion

import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.components.KaScopeContext
import org.jetbrains.kotlin.analysis.api.scopes.KaScope
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
import org.jetbrains.kotlin.lexer.KtTokens.MODIFIER_KEYWORDS
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtConstructor
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtModifierList
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtTypeAlias
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.parents
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.slf4j.LoggerFactory

/**
* Defines context at the cursor position.
*/
data class CursorContext(
val psiElement: PsiElement,
val ktFile: KtFile,
val ktElement: KtElement,
val scopeContext: KaScopeContext,
val compositeScope: KaScope,
val completionContext: CompletionContext,
val declarationContext: DeclarationContext,
val declarationKind: DeclarationKind,
val existingModifiers: Set<KtModifierKeywordToken>,
val isInsideModifierList: Boolean,
)


private val logger = LoggerFactory.getLogger("ContextResolver")

/**
* Resolves [CursorContext] at the given offset in the given [KtFile].
*/
fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int): CursorContext? {
val psiElement = ktFile.findElementAt(offset)
if (psiElement == null) {
logger.error("Unable to find PSI element at offset {} in file {}", offset, ktFile)
return null
}

val completionContext = determineCompletionContext(psiElement)
val ktElement = psiElement.getParentOfType<KtElement>(strict = false)
if (ktElement == null) {
logger.error("Cannot find parent of element {}", psiElement)
return null
}

val scopeContext = ktFile.scopeContext(ktElement)
val compositeScope = scopeContext.compositeScope()

// The element is typically a KtModifierList, an error node,
// or the incomplete declaration itself.
val modifierList = ktElement.getParentOfType<KtModifierList>(strict = false)
val existingModifiers = modifierList
?.node?.getChildren(MODIFIER_KEYWORDS)
?.mapNotNull { it.elementType as? KtModifierKeywordToken }
?.toSet()
?: emptySet()

val declarationKind = resolveDeclarationKind(ktElement)
val declarationContext = resolveDeclarationContext(ktElement)

return CursorContext(
psiElement = psiElement,
ktFile = ktFile,
ktElement = ktElement,
scopeContext = scopeContext,
compositeScope = compositeScope,
completionContext = completionContext,
declarationContext = declarationContext,
declarationKind = declarationKind,
existingModifiers = existingModifiers,
isInsideModifierList = modifierList != null,
)
}

private fun determineCompletionContext(element: PsiElement): CompletionContext {
// Walk up to find a qualified expression where we're the selector
val dotExpr = element.getParentOfType<KtDotQualifiedExpression>(strict = false)
if (dotExpr != null && isInSelectorPosition(element, dotExpr)) {
return CompletionContext.Member
}

val safeExpr = element.getParentOfType<KtSafeQualifiedExpression>(strict = false)
if (safeExpr != null && isInSelectorPosition(element, safeExpr)) {
return CompletionContext.Member
}

return CompletionContext.Scope
}

private fun isInSelectorPosition(
element: PsiElement,
qualifiedExpr: KtQualifiedExpression,
): Boolean {
val selector = qualifiedExpr.selectorExpression ?: return false
val elementOffset = element.startOffset
return elementOffset >= selector.startOffset
}

private fun resolveDeclarationContext(element: KtElement): DeclarationContext {
for (ancestor in element.parents) {
when (ancestor) {
is KtClassBody -> {
return when (val owner = ancestor.parent) {
is KtClass -> when {
owner.isInterface() -> DeclarationContext.INTERFACE_BODY
owner.isEnum() -> DeclarationContext.ENUM_BODY
owner.isAnnotation() -> DeclarationContext.ANNOTATION_BODY
else -> DeclarationContext.CLASS_BODY
}

is KtObjectDeclaration -> DeclarationContext.OBJECT_BODY
else -> DeclarationContext.CLASS_BODY
}
}

is KtBlockExpression -> return DeclarationContext.FUNCTION_BODY
is KtFile -> return if (ancestor.isScript())
DeclarationContext.SCRIPT_TOP_LEVEL
else
DeclarationContext.TOP_LEVEL
}
}
return DeclarationContext.TOP_LEVEL
}

private fun resolveDeclarationKind(element: KtElement): DeclarationKind {
// Walk up to the nearest declaration owning this modifier list / position
return when (val declaration = element.getNonStrictParentOfType<KtDeclaration>()) {
is KtClass -> when {
declaration.isInterface() -> DeclarationKind.INTERFACE
declaration.isEnum() -> DeclarationKind.ENUM_CLASS
declaration.isAnnotation() -> DeclarationKind.ANNOTATION_CLASS
else -> DeclarationKind.CLASS
}

is KtObjectDeclaration -> DeclarationKind.OBJECT
is KtNamedFunction -> DeclarationKind.FUN
is KtProperty -> if (declaration.isVar) DeclarationKind.PROPERTY_VAR
else DeclarationKind.PROPERTY_VAL

is KtTypeAlias -> DeclarationKind.TYPEALIAS
is KtConstructor<*> -> DeclarationKind.CONSTRUCTOR
null -> DeclarationKind.UNKNOWN // pure modifier list, no keyword yet
else -> DeclarationKind.UNKNOWN
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.itsaky.androidide.lsp.kotlin.completion

/**
* Defines the possible declaration contexts of the element at cursor position.
*/
enum class DeclarationContext {
TOP_LEVEL,
CLASS_BODY,
INTERFACE_BODY,
OBJECT_BODY, // includes companion object
ENUM_BODY,
FUNCTION_BODY, // local declarations & statements
SCRIPT_TOP_LEVEL,
ANNOTATION_BODY,
}

/**
* Defines declaration kinds for element at cursor.
*/
enum class DeclarationKind {
CLASS, INTERFACE, OBJECT, ENUM_CLASS, ANNOTATION_CLASS,
FUN, PROPERTY_VAL, PROPERTY_VAR,
TYPEALIAS, CONSTRUCTOR,
UNKNOWN // e.g. modifier typed before any keyword yet
}
Loading
Loading